freeboard/js/freeboard+plugins.js

4301 lines
112 KiB
JavaScript

DatasourceModel = function(theFreeboardModel, datasourcePlugins) {
var self = this;
function disposeDatasourceInstance()
{
if(!_.isUndefined(self.datasourceInstance))
{
if(_.isFunction(self.datasourceInstance.onDispose))
{
self.datasourceInstance.onDispose();
}
self.datasourceInstance = undefined;
}
}
this.name = ko.observable();
this.latestData = ko.observable();
this.settings = ko.observable({});
this.settings.subscribe(function(newValue)
{
if(!_.isUndefined(self.datasourceInstance) && _.isFunction(self.datasourceInstance.onSettingsChanged))
{
self.datasourceInstance.onSettingsChanged(newValue);
}
});
this.updateCallback = function(newData)
{
theFreeboardModel.processDatasourceUpdate(self, newData);
self.latestData(newData);
var now = new Date();
self.last_updated(now.toLocaleTimeString());
}
this.type = ko.observable();
this.type.subscribe(function(newValue)
{
disposeDatasourceInstance();
if((newValue in datasourcePlugins) && _.isFunction(datasourcePlugins[newValue].newInstance))
{
var datasourceType = datasourcePlugins[newValue];
function finishLoad()
{
datasourceType.newInstance(self.settings(), function(datasourceInstance)
{
self.datasourceInstance = datasourceInstance;
datasourceInstance.updateNow();
}, self.updateCallback);
}
// Do we need to load any external scripts?
if(datasourceType.external_scripts)
{
head.js(datasourceType.external_scripts.slice(0), finishLoad); // Need to clone the array because head.js adds some weird functions to it
}
else
{
finishLoad();
}
}
});
this.last_updated = ko.observable("never");
this.last_error = ko.observable();
this.serialize = function()
{
return {
name : self.name(),
type : self.type(),
settings: self.settings()
};
}
this.deserialize = function(object)
{
self.settings(object.settings);
self.name(object.name);
self.type(object.type);
}
this.getDataRepresentation = function(dataPath)
{
var valueFunction = new Function("data", "return " + dataPath + ";");
return valueFunction.call(undefined, self.latestData());
}
this.updateNow = function()
{
if(!_.isUndefined(self.datasourceInstance) && _.isFunction(self.datasourceInstance.updateNow))
{
self.datasourceInstance.updateNow();
}
}
this.dispose = function()
{
disposeDatasourceInstance();
}
}
DeveloperConsole = function(theFreeboardModel)
{
function showDeveloperConsole()
{
var pluginScriptsInputs = [];
var container = $('<div></div>');
var addScript = $('<div class="table-operation text-button">ADD</div>');
var table = $('<table class="table table-condensed sub-table"></table>');
table.append($('<thead style=""><tr><th>Plugin Script URL</th></tr></thead>'));
var tableBody = $("<tbody></tbody>");
table.append(tableBody);
container.append($("<p>Here you can add references to other scripts to load datasource or widget plugins.</p>"))
.append(table)
.append(addScript)
.append('<p>To learn how to build plugins for freeboard, please visit <a target="_blank" href="http://freeboard.github.io/freeboard/docs/plugin_example.html">http://freeboard.github.io/freeboard/docs/plugin_example.html</a></p>');
function refreshScript(scriptURL)
{
$('script[src="' + scriptURL + '"]').remove();
}
function addNewScriptRow(scriptURL)
{
var tableRow = $('<tr></tr>');
var tableOperations = $('<ul class="board-toolbar"></ul>');
var scriptInput = $('<input class="table-row-value" style="width:100%;" type="text">');
var deleteOperation = $('<li><i class="icon-trash icon-white"></i></li>').click(function(e){
pluginScriptsInputs = _.without(pluginScriptsInputs, scriptInput);
tableRow.remove();
});
pluginScriptsInputs.push(scriptInput);
if(scriptURL)
{
scriptInput.val(scriptURL);
}
tableOperations.append(deleteOperation);
tableBody
.append(tableRow
.append($('<td></td>').append(scriptInput))
.append($('<td class="table-row-operation">').append(tableOperations)));
}
_.each(theFreeboardModel.plugins(), function(pluginSource){
addNewScriptRow(pluginSource);
});
addScript.click(function(e)
{
addNewScriptRow();
});
new DialogBox(container, "Developer Console", "OK", null, function(){
// Unload our previous scripts
_.each(theFreeboardModel.plugins(), function(pluginSource){
$('script[src^="' + pluginSource + '"]').remove();
});
theFreeboardModel.plugins.removeAll();
_.each(pluginScriptsInputs, function(scriptInput){
var scriptURL = scriptInput.val();
if(scriptURL && scriptURL.length > 0)
{
theFreeboardModel.addPluginSource(scriptURL);
// Load the script with a cache buster
head.js(scriptURL + "?" + Date.now());
}
});
});
}
// Public API
return {
showDeveloperConsole : function()
{
showDeveloperConsole();
}
}
}
function DialogBox(contentElement, title, okTitle, cancelTitle, okCallback)
{
var modal_width = 900;
// Initialize our modal overlay
var overlay = $('<div id="modal_overlay" style="display:none;"></div>');
var modalDialog = $('<div class="modal"></div>');
function closeModal()
{
overlay.fadeOut(200, function()
{
$(this).remove();
});
}
// Create our header
modalDialog.append('<header><h2 class="title">' + title + "</h2></header>");
$('<section></section>').appendTo(modalDialog).append(contentElement);
// Create our footer
var footer = $('<footer></footer>').appendTo(modalDialog);
if(okTitle)
{
$('<span id="dialog-ok" class="text-button">' + okTitle + '</span>').appendTo(footer).click(function()
{
var hold = false;
if(_.isFunction(okCallback))
{
hold = okCallback();
}
if(!hold)
{
closeModal();
}
});
}
if(cancelTitle)
{
$('<span id="dialog-cancel" class="text-button">' + cancelTitle + '</span>').appendTo(footer).click(function()
{
closeModal();
});
}
overlay.append(modalDialog);
$("body").append(overlay);
overlay.fadeIn(200);
}
function FreeboardModel(datasourcePlugins, widgetPlugins, freeboardUI)
{
var self = this;
var SERIALIZATION_VERSION = 1;
this.version = 0;
this.isEditing = ko.observable(false);
this.allow_edit = ko.observable(false);
this.allow_edit.subscribe(function(newValue)
{
if(newValue)
{
$("#main-header").show();
}
else
{
$("#main-header").hide();
}
});
this.header_image = ko.observable();
this.plugins = ko.observableArray();
this.datasources = ko.observableArray();
this.panes = ko.observableArray();
this.datasourceData = {};
this.processDatasourceUpdate = function(datasourceModel, newData)
{
var datasourceName = datasourceModel.name();
self.datasourceData[datasourceName] = newData;
_.each(self.panes(), function(pane)
{
_.each(pane.widgets(), function(widget)
{
widget.processDatasourceUpdate(datasourceName);
});
});
}
this._datasourceTypes = ko.observable();
this.datasourceTypes = ko.computed({
read: function()
{
self._datasourceTypes();
var returnTypes = [];
_.each(datasourcePlugins, function(datasourcePluginType)
{
var typeName = datasourcePluginType.type_name;
var displayName = typeName;
if(!_.isUndefined(datasourcePluginType.display_name))
{
displayName = datasourcePluginType.display_name;
}
returnTypes.push({
name : typeName,
display_name: displayName
});
});
return returnTypes;
}
});
this._widgetTypes = ko.observable();
this.widgetTypes = ko.computed({
read: function()
{
self._widgetTypes();
var returnTypes = [];
_.each(widgetPlugins, function(widgetPluginType)
{
var typeName = widgetPluginType.type_name;
var displayName = typeName;
if(!_.isUndefined(widgetPluginType.display_name))
{
displayName = widgetPluginType.display_name;
}
returnTypes.push({
name : typeName,
display_name: displayName
});
});
return returnTypes;
}
});
this.addPluginSource = function(pluginSource)
{
if(pluginSource && self.plugins.indexOf(pluginSource) == -1)
{
self.plugins.push(pluginSource);
}
}
this.serialize = function()
{
var panes = [];
_.each(self.panes(), function(pane)
{
panes.push(pane.serialize());
});
var datasources = [];
_.each(self.datasources(), function(datasource)
{
datasources.push(datasource.serialize());
});
return {
version : SERIALIZATION_VERSION,
header_image: self.header_image(),
allow_edit : self.allow_edit(),
plugins : self.plugins(),
panes : panes,
datasources : datasources,
columns : freeboardUI.getUserColumns()
};
}
this.deserialize = function(object, finishedCallback)
{
self.clearDashboard();
function finishLoad()
{
freeboardUI.setUserColumns(object.columns);
if(!_.isUndefined(object.allow_edit))
{
self.allow_edit(object.allow_edit);
}
else
{
self.allow_edit(true);
}
self.version = object.version || 0;
self.header_image(object.header_image);
_.each(object.datasources, function(datasourceConfig)
{
var datasource = new DatasourceModel(self, datasourcePlugins);
datasource.deserialize(datasourceConfig);
self.addDatasource(datasource);
});
var sortedPanes = _.sortBy(object.panes, function(pane){
return freeboardUI.getPositionForScreenSize(pane).row;
});
_.each(sortedPanes, function(paneConfig)
{
var pane = new PaneModel(self, widgetPlugins);
pane.deserialize(paneConfig);
self.panes.push(pane);
});
if(self.allow_edit() && self.panes().length == 0)
{
self.setEditing(true);
}
if(_.isFunction(finishedCallback))
{
finishedCallback();
}
freeboardUI.processResize(true);
}
// This could have been self.plugins(object.plugins), but for some weird reason head.js was causing a function to be added to the list of plugins.
_.each(object.plugins, function(plugin)
{
self.addPluginSource(plugin);
});
// Load any plugins referenced in this definition
if(_.isArray(object.plugins) && object.plugins.length > 0)
{
head.js(object.plugins, function()
{
finishLoad();
});
}
else
{
finishLoad();
}
}
this.clearDashboard = function()
{
freeboardUI.removeAllPanes();
_.each(self.datasources(), function(datasource)
{
datasource.dispose();
});
_.each(self.panes(), function(pane)
{
pane.dispose();
});
self.plugins.removeAll();
self.datasources.removeAll();
self.panes.removeAll();
}
this.loadDashboard = function(dashboardData, callback)
{
freeboardUI.showLoadingIndicator(true);
self.deserialize(dashboardData, function()
{
freeboardUI.showLoadingIndicator(false);
if(_.isFunction(callback))
{
callback();
}
freeboard.emit("dashboard_loaded");
});
}
this.loadDashboardFromLocalFile = function()
{
// Check for the various File API support.
if(window.File && window.FileReader && window.FileList && window.Blob)
{
var input = document.createElement('input');
input.type = "file";
$(input).on("change", function(event)
{
var files = event.target.files;
if(files && files.length > 0)
{
var file = files[0];
var reader = new FileReader();
reader.addEventListener("load", function(fileReaderEvent)
{
var textFile = fileReaderEvent.target;
var jsonObject = JSON.parse(textFile.result);
self.loadDashboard(jsonObject);
self.setEditing(false);
});
reader.readAsText(file);
}
});
$(input).trigger("click");
}
else
{
alert('Unable to load a file in this browser.');
}
}
this.saveDashboard = function()
{
var contentType = 'application/octet-stream';
var a = document.createElement('a');
var blob = new Blob([JSON.stringify(self.serialize())], {'type': contentType});
document.body.appendChild(a);
a.href = window.URL.createObjectURL(blob);
a.download = "dashboard.json";
a.target="_self";
a.click();
}
this.addDatasource = function(datasource)
{
self.datasources.push(datasource);
}
this.deleteDatasource = function(datasource)
{
delete self.datasourceData[datasource.name()];
datasource.dispose();
self.datasources.remove(datasource);
}
this.createPane = function()
{
var newPane = new PaneModel(self, widgetPlugins);
self.addPane(newPane);
}
this.addGridColumnLeft = function()
{
freeboardUI.addGridColumnLeft();
}
this.addGridColumnRight = function()
{
freeboardUI.addGridColumnRight();
}
this.subGridColumnLeft = function()
{
freeboardUI.subGridColumnLeft();
}
this.subGridColumnRight = function()
{
freeboardUI.subGridColumnRight();
}
this.addPane = function(pane)
{
self.panes.push(pane);
}
this.deletePane = function(pane)
{
pane.dispose();
self.panes.remove(pane);
}
this.deleteWidget = function(widget)
{
ko.utils.arrayForEach(self.panes(), function(pane)
{
pane.widgets.remove(widget);
});
widget.dispose();
}
this.setEditing = function(editing, animate)
{
// Don't allow editing if it's not allowed
if(!self.allow_edit() && editing)
{
return;
}
self.isEditing(editing);
if(_.isUndefined(animate))
{
animate = true;
}
var animateLength = (animate) ? 250 : 0;
var barHeight = $("#admin-bar").outerHeight();
if(!editing)
{
$("#toggle-header-icon").addClass("icon-wrench").removeClass("icon-chevron-up");
$(".gridster .gs_w").css({cursor: "default"});
$("#main-header").animate({"top": "-" + barHeight + "px"}, animateLength);
$("#board-content").animate({"top": "20"}, animateLength);
$("#main-header").data().shown = false;
$(".sub-section").unbind();
freeboardUI.disableGrid();
}
else
{
$("#toggle-header-icon").addClass("icon-chevron-up").removeClass("icon-wrench");
$(".gridster .gs_w").css({cursor: "pointer"});
$("#main-header").animate({"top": "0px"}, animateLength);
$("#board-content").animate({"top": (barHeight + 20) + "px"}, animateLength);
$("#main-header").data().shown = true;
freeboardUI.attachWidgetEditIcons($(".sub-section"));
freeboardUI.enableGrid();
}
freeboardUI.showPaneEditIcons(editing, animate);
}
this.toggleEditing = function()
{
var editing = !self.isEditing();
self.setEditing(editing);
}
}
function FreeboardUI()
{
var PANE_MARGIN = 10;
var PANE_WIDTH = 300;
var MIN_COLUMNS = 3;
var COLUMN_WIDTH = PANE_MARGIN + PANE_WIDTH + PANE_MARGIN;
var userColumns = MIN_COLUMNS;
var loadingIndicator = $('<div class="wrapperloading"><div class="loading up" ></div><div class="loading down"></div></div>');
var grid;
function processResize(layoutWidgets)
{
var maxDisplayableColumns = getMaxDisplayableColumnCount();
var repositionFunction = function(){};
if(layoutWidgets)
{
repositionFunction = function(index)
{
var paneElement = this;
var paneModel = ko.dataFor(paneElement);
var newPosition = getPositionForScreenSize(paneModel);
$(paneElement).attr("data-sizex", Math.min(paneModel.col_width(),
maxDisplayableColumns, grid.cols))
.attr("data-row", newPosition.row)
.attr("data-col", newPosition.col);
paneModel.processSizeChange();
}
}
updateGridWidth(Math.min(maxDisplayableColumns, userColumns));
repositionGrid(repositionFunction);
updateGridColumnControls();
}
function addGridColumn(shift)
{
var num_cols = grid.cols + 1;
if(updateGridWidth(num_cols))
{
repositionGrid(function() {
var paneElement = this;
var paneModel = ko.dataFor(paneElement);
var prevColumnIndex = grid.cols > 1 ? grid.cols - 1 : 1;
var prevCol = paneModel.col[prevColumnIndex];
var prevRow = paneModel.row[prevColumnIndex];
var newPosition;
if(shift)
{
leftPreviewCol = true;
var newCol = prevCol < grid.cols ? prevCol + 1 : grid.cols;
newPosition = {row: prevRow, col: newCol};
}
else
{
rightPreviewCol = true;
newPosition = {row: prevRow, col: prevCol};
}
$(paneElement).attr("data-sizex", Math.min(paneModel.col_width(), grid.cols))
.attr("data-row", newPosition.row)
.attr("data-col", newPosition.col);
});
}
updateGridColumnControls();
userColumns = grid.cols;
}
function subtractGridColumn(shift)
{
var num_cols = grid.cols - 1;
if(updateGridWidth(num_cols))
{
repositionGrid(function() {
var paneElement = this;
var paneModel = ko.dataFor(paneElement);
var prevColumnIndex = grid.cols + 1;
var prevCol = paneModel.col[prevColumnIndex];
var prevRow = paneModel.row[prevColumnIndex];
var newPosition;
if(shift)
{
var newCol = prevCol > 1 ? prevCol - 1 : 1;
newPosition = {row: prevRow, col: newCol};
}
else
{
var newCol = prevCol <= grid.cols ? prevCol : grid.cols;
newPosition = {row: prevRow, col: newCol};
}
$(paneElement).attr("data-sizex", Math.min(paneModel.col_width(), grid.cols))
.attr("data-row", newPosition.row)
.attr("data-col", newPosition.col);
});
}
updateGridColumnControls();
userColumns = grid.cols;
}
function updateGridColumnControls()
{
var col_controls = $(".column-tool");
var available_width = $("#board-content").width();
var max_columns = Math.floor(available_width / COLUMN_WIDTH);
if(grid.cols <= MIN_COLUMNS)
{
col_controls.addClass("min");
}
else
{
col_controls.removeClass("min");
}
if(grid.cols >= max_columns)
{
col_controls.addClass("max");
}
else
{
col_controls.removeClass("max");
}
}
function getMaxDisplayableColumnCount()
{
var available_width = $("#board-content").width();
return Math.floor(available_width / COLUMN_WIDTH);
}
function updateGridWidth(newCols)
{
if(newCols === undefined || newCols < MIN_COLUMNS)
{
newCols = MIN_COLUMNS;
}
var max_columns = getMaxDisplayableColumnCount();
if(newCols > max_columns)
{
newCols = max_columns;
}
// +newCols to account for scaling on zoomed browsers
var new_width = (COLUMN_WIDTH * newCols) + newCols;
$(".responsive-column-width").css("max-width", new_width);
if(newCols === grid.cols)
{
return false;
}
else
{
return true;
}
}
function repositionGrid(repositionFunction)
{
var rootElement = grid.$el;
rootElement.find("> li").unbind().removeData();
$(".responsive-column-width").css("width", "");
grid.generate_grid_and_stylesheet();
rootElement.find("> li").each(repositionFunction);
grid.init();
$(".responsive-column-width").css("width", grid.cols * PANE_WIDTH + (grid.cols * PANE_MARGIN * 2));
}
function getUserColumns()
{
return userColumns;
}
function setUserColumns(numCols)
{
userColumns = Math.max(MIN_COLUMNS, numCols);
}
ko.bindingHandlers.grid = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
{
// Initialize our grid
grid = $(element).gridster({
widget_margins : [PANE_MARGIN, PANE_MARGIN],
widget_base_dimensions: [PANE_WIDTH, 10],
resize: {
enabled : false,
axes : "x"
}
}).data("gridster");
processResize(false)
grid.disable();
}
}
function addPane(element, viewModel, isEditing)
{
var position = getPositionForScreenSize(viewModel);
var col = position.col;
var row = position.row;
var width = Number(viewModel.width());
var height = Number(viewModel.getCalculatedHeight());
grid.add_widget(element, width, height, col, row);
if(isEditing)
{
showPaneEditIcons(true);
}
updatePositionForScreenSize(viewModel, row, col);
$(element).attrchange({
trackValues: true,
callback : function(event)
{
if(event.attributeName == "data-row")
{
updatePositionForScreenSize(viewModel, Number(event.newValue), undefined);
}
else if(event.attributeName == "data-col")
{
updatePositionForScreenSize(viewModel, undefined, Number(event.newValue));
}
}
});
}
function updatePane(element, viewModel)
{
// If widget has been added or removed
var calculatedHeight = viewModel.getCalculatedHeight();
var elementHeight = Number($(element).attr("data-sizey"));
var elementWidth = Number($(element).attr("data-sizex"));
if(calculatedHeight != elementHeight || viewModel.col_width() != elementWidth)
{
grid.resize_widget($(element), viewModel.col_width(), calculatedHeight, function(){
grid.set_dom_grid_height();
});
}
}
function updatePositionForScreenSize(paneModel, row, col)
{
var displayCols = grid.cols;
if(!_.isUndefined(row)) paneModel.row[displayCols] = row;
if(!_.isUndefined(col)) paneModel.col[displayCols] = col;
}
function showLoadingIndicator(show)
{
if(show)
{
loadingIndicator.fadeOut(0).appendTo("body").fadeIn(500);
}
else
{
loadingIndicator.fadeOut(500).remove();
}
}
function showPaneEditIcons(show, animate)
{
if(_.isUndefined(animate))
{
animate = true;
}
var animateLength = (animate) ? 250 : 0;
if(show)
{
$(".pane-tools").fadeIn(animateLength);//.css("display", "block").animate({opacity: 1.0}, animateLength);
$("#column-tools").fadeIn(animateLength);
}
else
{
$(".pane-tools").fadeOut(animateLength);//.animate({opacity: 0.0}, animateLength).css("display", "none");//, function()
$("#column-tools").fadeOut(animateLength);
}
}
function attachWidgetEditIcons(element)
{
$(element).hover(function()
{
showWidgetEditIcons(this, true);
}, function()
{
showWidgetEditIcons(this, false);
});
}
function showWidgetEditIcons(element, show)
{
if(show)
{
$(element).find(".sub-section-tools").fadeIn(250);
}
else
{
$(element).find(".sub-section-tools").fadeOut(250);
}
}
function getPositionForScreenSize(paneModel)
{
var cols = grid.cols;
if(_.isNumber(paneModel.row) && _.isNumber(paneModel.col)) // Support for legacy format
{
var obj = {};
obj[cols] = paneModel.row;
paneModel.row = obj;
obj = {};
obj[cols] = paneModel.col;
paneModel.col = obj;
}
var newColumnIndex = 1;
var columnDiff = 1000;
for(var columnIndex in paneModel.col)
{
if(columnIndex == cols) // If we already have a position defined for this number of columns, return that position
{
return {row: paneModel.row[columnIndex], col: paneModel.col[columnIndex]};
}
else if(paneModel.col[columnIndex] > cols) // If it's greater than our display columns, put it in the last column
{
newColumnIndex = cols;
}
else // If it's less than, pick whichever one is closest
{
var delta = cols - columnIndex;
if(delta < columnDiff)
{
newColumnIndex = columnIndex;
columnDiff = delta;
}
}
}
if(newColumnIndex in paneModel.col && newColumnIndex in paneModel.row)
{
return {row: paneModel.row[newColumnIndex], col: paneModel.col[newColumnIndex]};
}
return {row:1,col:newColumnIndex};
}
// Public Functions
return {
showLoadingIndicator : function(show)
{
showLoadingIndicator(show);
},
showPaneEditIcons : function(show, animate)
{
showPaneEditIcons(show, animate);
},
attachWidgetEditIcons : function(element)
{
attachWidgetEditIcons(element);
},
getPositionForScreenSize : function(paneModel)
{
return getPositionForScreenSize(paneModel);
},
processResize : function(layoutWidgets)
{
processResize(layoutWidgets);
},
disableGrid : function()
{
grid.disable();
},
enableGrid : function()
{
grid.enable();
},
addPane : function(element, viewModel, isEditing)
{
addPane(element, viewModel, isEditing);
},
updatePane : function(element, viewModel)
{
updatePane(element, viewModel);
},
removePane : function(element)
{
grid.remove_widget(element);
},
removeAllPanes : function()
{
grid.remove_all_widgets();
},
addGridColumnLeft : function()
{
addGridColumn(true);
},
addGridColumnRight : function()
{
addGridColumn(false);
},
subGridColumnLeft : function()
{
subtractGridColumn(true);
},
subGridColumnRight : function()
{
subtractGridColumn(false);
},
getUserColumns : function()
{
return getUserColumns();
},
setUserColumns : function(numCols)
{
setUserColumns(numCols);
}
}
}
JSEditor = function () {
var assetRoot = ""
function setAssetRoot(_assetRoot) {
assetRoot = _assetRoot;
}
function displayJSEditor(value, callback) {
var exampleText = "// Example: Convert temp from C to F and truncate to 2 decimal places.\n// return (datasources[\"MyDatasource\"].sensor.tempInF * 1.8 + 32).toFixed(2);";
// If value is empty, go ahead and suggest something
if (!value) {
value = exampleText;
}
var codeWindow = $('<div class="code-window"></div>');
var codeMirrorWrapper = $('<div class="code-mirror-wrapper"></div>');
var codeWindowFooter = $('<div class="code-window-footer"></div>');
var codeWindowHeader = $('<div class="code-window-header cm-s-ambiance">This javascript will be re-evaluated any time a datasource referenced here is updated, and the value you <code><span class="cm-keyword">return</span></code> will be displayed in the widget. You can assume this javascript is wrapped in a function of the form <code><span class="cm-keyword">function</span>(<span class="cm-def">datasources</span>)</code> where datasources is a collection of javascript objects (keyed by their name) corresponding to the most current data in a datasource.</div>');
codeWindow.append([codeWindowHeader, codeMirrorWrapper, codeWindowFooter]);
$("body").append(codeWindow);
var codeMirrorEditor = CodeMirror(codeMirrorWrapper.get(0),
{
value: value,
mode: "javascript",
theme: "ambiance",
indentUnit: 4,
lineNumbers: true,
matchBrackets: true,
autoCloseBrackets: true
}
);
var closeButton = $('<span id="dialog-cancel" class="text-button">Close</span>').click(function () {
if (callback) {
var newValue = codeMirrorEditor.getValue();
if (newValue === exampleText) {
newValue = "";
}
callback(newValue);
codeWindow.remove();
}
});
codeWindowFooter.append(closeButton);
}
// Public API
return {
displayJSEditor: function (value, callback) {
displayJSEditor(value, callback);
},
setAssetRoot: function (assetRoot) {
setAssetRoot(assetRoot)
}
}
}
function PaneModel(theFreeboardModel, widgetPlugins) {
var self = this;
this.title = ko.observable();
this.width = ko.observable(1);
this.row = {};
this.col = {};
this.col_width = ko.observable(1);
this.col_width.subscribe(function(newValue)
{
self.processSizeChange();
});
this.widgets = ko.observableArray();
this.addWidget = function (widget) {
this.widgets.push(widget);
}
this.widgetCanMoveUp = function (widget) {
return (self.widgets.indexOf(widget) >= 1);
}
this.widgetCanMoveDown = function (widget) {
var i = self.widgets.indexOf(widget);
return (i < self.widgets().length - 1);
}
this.moveWidgetUp = function (widget) {
if (self.widgetCanMoveUp(widget)) {
var i = self.widgets.indexOf(widget);
var array = self.widgets();
self.widgets.splice(i - 1, 2, array[i], array[i - 1]);
}
}
this.moveWidgetDown = function (widget) {
if (self.widgetCanMoveDown(widget)) {
var i = self.widgets.indexOf(widget);
var array = self.widgets();
self.widgets.splice(i, 2, array[i + 1], array[i]);
}
}
this.processSizeChange = function()
{
// Give the animation a moment to complete. Really hacky.
// TODO: Make less hacky. Also, doesn't work when screen resizes.
setTimeout(function(){
_.each(self.widgets(), function (widget) {
widget.processSizeChange();
});
}, 1000);
}
this.getCalculatedHeight = function () {
var sumHeights = _.reduce(self.widgets(), function (memo, widget) {
return memo + widget.height();
}, 0);
sumHeights *= 6;
sumHeights += 3;
sumHeights *= 10;
var rows = Math.ceil((sumHeights + 20) / 30);
return Math.max(4, rows);
}
this.serialize = function () {
var widgets = [];
_.each(self.widgets(), function (widget) {
widgets.push(widget.serialize());
});
return {
title: self.title(),
width: self.width(),
row: self.row,
col: self.col,
col_width: self.col_width(),
widgets: widgets
};
}
this.deserialize = function (object) {
self.title(object.title);
self.width(object.width);
self.row = object.row;
self.col = object.col;
self.col_width(object.col_width || 1);
_.each(object.widgets, function (widgetConfig) {
var widget = new WidgetModel(theFreeboardModel, widgetPlugins);
widget.deserialize(widgetConfig);
self.widgets.push(widget);
});
}
this.dispose = function () {
_.each(self.widgets(), function (widget) {
widget.dispose();
});
}
}
PluginEditor = function(jsEditor, valueEditor)
{
function _displayValidationError(settingName, errorMessage)
{
var errorElement = $('<div class="validation-error"></div>').html(errorMessage);
$("#setting-value-container-" + settingName).append(errorElement);
}
function _removeSettingsRows()
{
if($("#setting-row-instance-name").length)
{
$("#setting-row-instance-name").nextAll().remove();
}
else
{
$("#setting-row-plugin-types").nextAll().remove();
}
}
function _isNumerical(n)
{
return !isNaN(parseFloat(n)) && isFinite(n);
}
function createPluginEditor(title, pluginTypes, currentTypeName, currentSettingsValues, settingsSavedCallback)
{
var newSettings = {
type : currentTypeName,
settings: {}
};
function createSettingRow(name, displayName)
{
var tr = $('<div id="setting-row-' + name + '" class="form-row"></div>').appendTo(form);
tr.append('<div class="form-label"><label class="control-label">' + displayName + '</label></div>');
return $('<div id="setting-value-container-' + name + '" class="form-value"></div>').appendTo(tr);
}
var selectedType;
var form = $('<div></div>');
var pluginDescriptionElement = $('<div id="plugin-description"></div>').hide();
form.append(pluginDescriptionElement);
function createSettingsFromDefinition(settingsDefs)
{
_.each(settingsDefs, function(settingDef)
{
// Set a default value if one doesn't exist
if(!_.isUndefined(settingDef.default_value) && _.isUndefined(currentSettingsValues[settingDef.name]))
{
currentSettingsValues[settingDef.name] = settingDef.default_value;
}
var displayName = settingDef.name;
if(!_.isUndefined(settingDef.display_name))
{
displayName = settingDef.display_name;
}
var valueCell = createSettingRow(settingDef.name, displayName);
switch (settingDef.type)
{
case "array":
{
var subTableDiv = $('<div class="form-table-value-subtable"></div>').appendTo(valueCell);
var subTable = $('<table class="table table-condensed sub-table"></table>').appendTo(subTableDiv);
var subTableHead = $("<thead></thead>").hide().appendTo(subTable);
var subTableHeadRow = $("<tr></tr>").appendTo(subTableHead);
var subTableBody = $('<tbody></tbody>').appendTo(subTable);
var currentSubSettingValues = [];
// Create our headers
_.each(settingDef.settings, function(subSettingDef)
{
var subsettingDisplayName = subSettingDef.name;
if(!_.isUndefined(subSettingDef.display_name))
{
subsettingDisplayName = subSettingDef.display_name;
}
$('<th>' + subsettingDisplayName + '</th>').appendTo(subTableHeadRow);
});
if(settingDef.name in currentSettingsValues)
{
currentSubSettingValues = currentSettingsValues[settingDef.name];
}
function processHeaderVisibility()
{
if(newSettings.settings[settingDef.name].length > 0)
{
subTableHead.show();
}
else
{
subTableHead.hide();
}
}
function createSubsettingRow(subsettingValue)
{
var subsettingRow = $('<tr></tr>').appendTo(subTableBody);
var newSetting = {};
if(!_.isArray(newSettings.settings[settingDef.name]))
{
newSettings.settings[settingDef.name] = [];
}
newSettings.settings[settingDef.name].push(newSetting);
_.each(settingDef.settings, function(subSettingDef)
{
var subsettingCol = $('<td></td>').appendTo(subsettingRow);
var subsettingValueString = "";
if(!_.isUndefined(subsettingValue[subSettingDef.name]))
{
subsettingValueString = subsettingValue[subSettingDef.name];
}
newSetting[subSettingDef.name] = subsettingValueString;
$('<input class="table-row-value" type="text">').appendTo(subsettingCol).val(subsettingValueString).change(function()
{
newSetting[subSettingDef.name] = $(this).val();
});
});
subsettingRow.append($('<td class="table-row-operation"></td>').append($('<ul class="board-toolbar"></ul>').append($('<li></li>').append($('<i class="icon-trash icon-white"></i>').click(function()
{
var subSettingIndex = newSettings.settings[settingDef.name].indexOf(newSetting);
if(subSettingIndex != -1)
{
newSettings.settings[settingDef.name].splice(subSettingIndex, 1);
subsettingRow.remove();
processHeaderVisibility();
}
})))));
subTableDiv.scrollTop(subTableDiv[0].scrollHeight);
processHeaderVisibility();
}
$('<div class="table-operation text-button">ADD</div>').appendTo(valueCell).click(function()
{
var newSubsettingValue = {};
_.each(settingDef.settings, function(subSettingDef)
{
newSubsettingValue[subSettingDef.name] = "";
});
createSubsettingRow(newSubsettingValue);
});
// Create our rows
_.each(currentSubSettingValues, function(currentSubSettingValue, subSettingIndex)
{
createSubsettingRow(currentSubSettingValue);
});
break;
}
case "boolean":
{
newSettings.settings[settingDef.name] = currentSettingsValues[settingDef.name];
var onOffSwitch = $('<div class="onoffswitch"><label class="onoffswitch-label" for="' + settingDef.name + '-onoff"><div class="onoffswitch-inner"><span class="on">YES</span><span class="off">NO</span></div><div class="onoffswitch-switch"></div></label></div>').appendTo(valueCell);
var input = $('<input type="checkbox" name="onoffswitch" class="onoffswitch-checkbox" id="' + settingDef.name + '-onoff">').prependTo(onOffSwitch).change(function()
{
newSettings.settings[settingDef.name] = this.checked;
});
if(settingDef.name in currentSettingsValues)
{
input.prop("checked", currentSettingsValues[settingDef.name]);
}
break;
}
case "option":
{
var defaultValue = currentSettingsValues[settingDef.name];
var input = $('<select></select>').appendTo($('<div class="styled-select"></div>').appendTo(valueCell)).change(function()
{
newSettings.settings[settingDef.name] = $(this).val();
});
_.each(settingDef.options, function(option)
{
var optionName;
var optionValue;
if(_.isObject(option))
{
optionName = option.name;
optionValue = option.value;
}
else
{
optionName = option;
}
if(_.isUndefined(optionValue))
{
optionValue = optionName;
}
if(_.isUndefined(defaultValue))
{
defaultValue = optionValue;
}
$("<option></option>").text(optionName).attr("value", optionValue).appendTo(input);
});
newSettings.settings[settingDef.name] = defaultValue;
if(settingDef.name in currentSettingsValues)
{
input.val(currentSettingsValues[settingDef.name]);
}
break;
}
default:
{
newSettings.settings[settingDef.name] = currentSettingsValues[settingDef.name];
if(settingDef.type == "calculated")
{
var input = $('<textarea></textarea>').appendTo(valueCell).change(function()
{
newSettings.settings[settingDef.name] = $(this).val();
});
if(settingDef.name in currentSettingsValues)
{
input.val(currentSettingsValues[settingDef.name]);
}
valueEditor.createValueEditor(input);
var datasourceToolbox = $('<ul class="board-toolbar datasource-input-suffix"></ul>');
var datasourceTool = $('<li><i class="icon-plus icon-white"></i><label>DATASOURCE</label></li>').mousedown(function(e)
{
e.preventDefault();
$(input).focus();
$(input).insertAtCaret("datasources[\"");
$(input).trigger("freeboard-eval");
});
var jsEditorTool = $('<li><i class="icon-fullscreen icon-white"></i><label>.JS EDITOR</label></li>').mousedown(function(e)
{
e.preventDefault();
jsEditor.displayJSEditor(input.val(), function(result){
input.val(result);
input.change();
});
});
$(valueCell).append(datasourceToolbox.append([datasourceTool, jsEditorTool]));
}
else
{
var input = $('<input type="text">').appendTo(valueCell).change(function()
{
if(settingDef.type == "number")
{
newSettings.settings[settingDef.name] = Number($(this).val());
}
else
{
newSettings.settings[settingDef.name] = $(this).val();
}
});
if(settingDef.name in currentSettingsValues)
{
input.val(currentSettingsValues[settingDef.name]);
}
}
break;
}
}
if(!_.isUndefined(settingDef.suffix))
{
valueCell.append($('<div class="input-suffix">' + settingDef.suffix + '</div>'));
}
if(!_.isUndefined(settingDef.description))
{
valueCell.append($('<div class="setting-description">' + settingDef.description + '</div>'));
}
});
}
new DialogBox(form, title, "Save", "Cancel", function()
{
$(".validation-error").remove();
// Loop through each setting and validate it
for(var index = 0; index < selectedType.settings.length; index++)
{
var settingDef = selectedType.settings[index];
if(settingDef.required && (_.isUndefined(newSettings.settings[settingDef.name]) || newSettings.settings[settingDef.name] == ""))
{
_displayValidationError(settingDef.name, "This is required.");
return true;
}
else if(settingDef.type == "number" && !_isNumerical(newSettings.settings[settingDef.name]))
{
_displayValidationError(settingDef.name, "Must be a number.");
return true;
}
}
if(_.isFunction(settingsSavedCallback))
{
settingsSavedCallback(newSettings);
}
});
// Create our body
var pluginTypeNames = _.keys(pluginTypes);
var typeSelect;
if(pluginTypeNames.length > 1)
{
var typeRow = createSettingRow("plugin-types", "Type");
typeSelect = $('<select></select>').appendTo($('<div class="styled-select"></div>').appendTo(typeRow));
typeSelect.append($("<option>Select a type...</option>").attr("value", "undefined"));
_.each(pluginTypes, function(pluginType)
{
typeSelect.append($("<option></option>").text(pluginType.display_name).attr("value", pluginType.type_name));
});
typeSelect.change(function()
{
newSettings.type = $(this).val();
newSettings.settings = {};
// Remove all the previous settings
_removeSettingsRows();
selectedType = pluginTypes[typeSelect.val()];
if(_.isUndefined(selectedType))
{
$("#setting-row-instance-name").hide();
$("#dialog-ok").hide();
}
else
{
$("#setting-row-instance-name").show();
if(selectedType.description && selectedType.description.length > 0)
{
pluginDescriptionElement.html(selectedType.description).show();
}
else
{
pluginDescriptionElement.hide();
}
$("#dialog-ok").show();
createSettingsFromDefinition(selectedType.settings);
}
});
}
else if(pluginTypeNames.length == 1)
{
selectedType = pluginTypes[pluginTypeNames[0]];
newSettings.type = selectedType.type_name;
newSettings.settings = {};
createSettingsFromDefinition(selectedType.settings);
}
if(typeSelect)
{
if(_.isUndefined(currentTypeName))
{
$("#setting-row-instance-name").hide();
$("#dialog-ok").hide();
}
else
{
$("#dialog-ok").show();
typeSelect.val(currentTypeName).trigger("change");
}
}
}
// Public API
return {
createPluginEditor : function(
title,
pluginTypes,
currentInstanceName,
currentTypeName,
currentSettingsValues,
settingsSavedCallback)
{
createPluginEditor(title, pluginTypes, currentInstanceName, currentTypeName, currentSettingsValues, settingsSavedCallback);
}
}
}
ValueEditor = function(theFreeboardModel)
{
var _veDatasourceRegex = new RegExp(".*datasources\\[\"([^\"]*)(\"\\]\\[\")?(.*)$");
var _autocompleteOptions = [];
var _autocompleteReplacementString;
function _resizeValueEditor(element)
{
var lineBreakCount = ($(element).val().match(/\n/g) || []).length;
var newHeight = Math.min(200, 20 * (lineBreakCount + 1));
$(element).css({height: newHeight + "px"});
}
function _autocompleteFromDatasource(inputString, datasources)
{
var match = _veDatasourceRegex.exec(inputString);
var options = [];
var replacementString;
if(match)
{
if(match[1] == "") // List all datasources
{
_.each(datasources, function(datasource)
{
options.push({value: datasource.name(), follow_char: "\"][\""});
});
}
else if(match[1] != "" && _.isUndefined(match[2])) // List partial datasources
{
replacementString = match[1];
_.each(datasources, function(datasource)
{
var dsName = datasource.name();
if(dsName != replacementString && dsName.indexOf(replacementString) == 0)
{
options.push({value: dsName, follow_char: "\"][\""});
}
});
}
else
{
var datasource = _.find(datasources, function(datasource)
{
return (datasource.name() === match[1]);
});
if(!_.isUndefined(datasource))
{
var dataPath = "";
if(!_.isUndefined(match[2]))
{
dataPath = match[2] + match[3];
}
var dataPathItems = dataPath.split("\"][\"");
dataPath = "data";
for(var index = 1; index < dataPathItems.length - 1; index++)
{
if(dataPathItems[index] != "")
{
dataPathItem = "[\"" + dataPathItems[index] + "\"]";
dataPath = dataPath + dataPathItem;
}
}
var lastPathObject = _.last(dataPathItems);
// If the last character is a ", then ignore it
if(lastPathObject.charAt(lastPathObject.length - 1) == "\"")
{
lastPathObject = lastPathObject.replace(/\[\"?$/, "");
dataPath = dataPath + "[\"" + lastPathObject + "\"]";
}
var dataValue = datasource.getDataRepresentation(dataPath);
if(_.isArray(dataValue))
{
for(var index = 0; index < dataValue.length; index++)
{
var followChar = "\"]";
if(_.isObject(dataValue[index]))
{
followChar = followChar + "\"][\"";
}
else if(_.isArray(dataValue[index]))
{
followChar = followChar + "\"][";
}
options.push({value: index, follow_char: followChar});
}
}
else if(_.isObject(dataValue))
{
replacementString = lastPathObject;
if(_.keys(dataValue).indexOf(replacementString) == -1)
{
_.each(dataValue, function(value, name)
{
if(name != lastPathObject && name.indexOf(lastPathObject) == 0)
{
var followChar = "\"]";
if(_.isArray(value))
{
followChar = "\"][";
}
else if(_.isObject(value))
{
followChar = "\"][\"";
}
options.push({value: name, follow_char: followChar});
}
});
}
}
}
}
}
_autocompleteOptions = options;
_autocompleteReplacementString = replacementString;
}
function createValueEditor(element)
{
var dropdown = null;
var selectedOptionIndex = 0;
$(element).addClass("calculated-value-input").bind("keyup mouseup freeboard-eval",function(event)
{
// Ignore arrow keys and enter keys
if(dropdown && event.type == "keyup" && (event.keyCode == 38 || event.keyCode == 40 || event.keyCode == 13))
{
event.preventDefault();
return;
}
var inputString = $(element).val().substring(0, $(element).getCaretPosition());
inputString = inputString.replace(String.fromCharCode(160), " "); // Weird issue where the textarea box was putting in ASCII (non breaking space) for spaces.
_autocompleteFromDatasource(inputString, theFreeboardModel.datasources());
if(_autocompleteOptions.length > 0)
{
if(!dropdown)
{
dropdown = $('<ul id="value-selector" class="value-dropdown"></ul>').insertAfter(element).width($(element).outerWidth() - 2).css("left", $(element).position().left).css("top", $(element).position().top + $(element).outerHeight() - 1);
}
dropdown.empty();
dropdown.scrollTop(0);
var selected = true;
selectedOptionIndex = 0;
var currentIndex = 0;
_.each(_autocompleteOptions, function(option)
{
var li = $('<li>' + option.value + '</li>').appendTo(dropdown).mouseenter(function()
{
$(this).trigger("freeboard-select");
}).mousedown(function(event)
{
$(this).trigger("freeboard-insertValue");
event.preventDefault();
}).data("freeboard-optionIndex", currentIndex).data("freeboard-optionValue", option.value).bind("freeboard-insertValue",function()
{
var optionValue = option.value;
if(!_.isUndefined(option.follow_char))
{
optionValue = optionValue + option.follow_char;
}
if(!_.isUndefined(_autocompleteReplacementString))
{
var replacementIndex = inputString.lastIndexOf(_autocompleteReplacementString);
if(replacementIndex != -1)
{
$(element).replaceTextAt(replacementIndex, replacementIndex + _autocompleteReplacementString.length, optionValue);
}
}
else
{
$(element).insertAtCaret(optionValue);
}
$(element).triggerHandler("mouseup");
}).bind("freeboard-select", function()
{
$(this).parent().find("li.selected").removeClass("selected");
$(this).addClass("selected");
selectedOptionIndex = $(this).data("freeboard-optionIndex");
});
if(selected)
{
$(li).addClass("selected");
selected = false;
}
currentIndex++;
});
}
else
{
$(element).next("ul#value-selector").remove();
dropdown = null;
selectedOptionIndex = -1;
}
}).focus(function()
{
$(element).css({"z-index" : 3001});
_resizeValueEditor(element);
}).focusout(function()
{
$(element).css({
"height": "",
"z-index" : 3000
});
$(element).next("ul#value-selector").remove();
dropdown = null;
selectedOptionIndex = -1;
}).bind("keydown", function(event)
{
if(dropdown)
{
if(event.keyCode == 38 || event.keyCode == 40) // Handle Arrow keys
{
event.preventDefault();
var optionItems = $(dropdown).find("li");
if(event.keyCode == 38) // Up Arrow
{
selectedOptionIndex--;
}
else if(event.keyCode == 40) // Down Arrow
{
selectedOptionIndex++;
}
if(selectedOptionIndex < 0)
{
selectedOptionIndex = optionItems.size() - 1;
}
else if(selectedOptionIndex >= optionItems.size())
{
selectedOptionIndex = 0;
}
var optionElement = $(optionItems).eq(selectedOptionIndex);
optionElement.trigger("freeboard-select");
$(dropdown).scrollTop($(optionElement).position().top);
}
else if(event.keyCode == 13) // Handle enter key
{
event.preventDefault();
if(selectedOptionIndex != -1)
{
$(dropdown).find("li").eq(selectedOptionIndex).trigger("freeboard-insertValue");
}
}
}
});
}
// Public API
return {
createValueEditor : function(element)
{
createValueEditor(element);
}
}
}
function WidgetModel(theFreeboardModel, widgetPlugins) {
function disposeWidgetInstance() {
if (!_.isUndefined(self.widgetInstance)) {
if (_.isFunction(self.widgetInstance.onDispose)) {
self.widgetInstance.onDispose();
}
self.widgetInstance = undefined;
}
}
var self = this;
this.datasourceRefreshNotifications = {};
this.calculatedSettingScripts = {};
this.title = ko.observable();
this.fillSize = ko.observable(false);
this.type = ko.observable();
this.type.subscribe(function (newValue) {
disposeWidgetInstance();
if ((newValue in widgetPlugins) && _.isFunction(widgetPlugins[newValue].newInstance)) {
var widgetType = widgetPlugins[newValue];
function finishLoad() {
widgetType.newInstance(self.settings(), function (widgetInstance) {
self.fillSize((widgetType.fill_size === true));
self.widgetInstance = widgetInstance;
self.shouldRender(true);
self._heightUpdate.valueHasMutated();
});
}
// Do we need to load any external scripts?
if (widgetType.external_scripts) {
head.js(widgetType.external_scripts.slice(0), finishLoad); // Need to clone the array because head.js adds some weird functions to it
}
else {
finishLoad();
}
}
});
this.settings = ko.observable({});
this.settings.subscribe(function (newValue) {
if (!_.isUndefined(self.widgetInstance) && _.isFunction(self.widgetInstance.onSettingsChanged)) {
self.widgetInstance.onSettingsChanged(newValue);
}
self.updateCalculatedSettings();
self._heightUpdate.valueHasMutated();
});
this.processDatasourceUpdate = function (datasourceName) {
var refreshSettingNames = self.datasourceRefreshNotifications[datasourceName];
if (_.isArray(refreshSettingNames)) {
_.each(refreshSettingNames, function (settingName) {
self.processCalculatedSetting(settingName);
});
}
}
this.callValueFunction = function (theFunction) {
return theFunction.call(undefined, theFreeboardModel.datasourceData);
}
this.processSizeChange = function () {
if (!_.isUndefined(self.widgetInstance) && _.isFunction(self.widgetInstance.onSizeChanged)) {
self.widgetInstance.onSizeChanged();
}
}
this.processCalculatedSetting = function (settingName) {
if (_.isFunction(self.calculatedSettingScripts[settingName])) {
var returnValue = undefined;
try {
returnValue = self.callValueFunction(self.calculatedSettingScripts[settingName]);
}
catch (e) {
var rawValue = self.settings()[settingName];
// If there is a reference error and the value just contains letters and numbers, then
if (e instanceof ReferenceError && (/^\w+$/).test(rawValue)) {
returnValue = rawValue;
}
}
if (!_.isUndefined(self.widgetInstance) && _.isFunction(self.widgetInstance.onCalculatedValueChanged) && !_.isUndefined(returnValue)) {
try {
self.widgetInstance.onCalculatedValueChanged(settingName, returnValue);
}
catch (e) {
console.log(e.toString());
}
}
}
}
this.updateCalculatedSettings = function () {
self.datasourceRefreshNotifications = {};
self.calculatedSettingScripts = {};
if (_.isUndefined(self.type())) {
return;
}
// Check for any calculated settings
var settingsDefs = widgetPlugins[self.type()].settings;
var datasourceRegex = new RegExp("datasources.([\\w_-]+)|datasources\\[['\"]([^'\"]+)", "g");
var currentSettings = self.settings();
_.each(settingsDefs, function (settingDef) {
if (settingDef.type == "calculated") {
var script = currentSettings[settingDef.name];
if (!_.isUndefined(script)) {
// If there is no return, add one
if ((script.match(/;/g) || []).length <= 1 && script.indexOf("return") == -1) {
script = "return " + script;
}
var valueFunction;
try {
valueFunction = new Function("datasources", script);
}
catch (e) {
var literalText = currentSettings[settingDef.name].replace(/"/g, '\\"').replace(/[\r\n]/g, ' \\\n');
// If the value function cannot be created, then go ahead and treat it as literal text
valueFunction = new Function("datasources", "return \"" + literalText + "\";");
}
self.calculatedSettingScripts[settingDef.name] = valueFunction;
self.processCalculatedSetting(settingDef.name);
// Are there any datasources we need to be subscribed to?
var matches;
while (matches = datasourceRegex.exec(script)) {
var dsName = (matches[1] || matches[2]);
var refreshSettingNames = self.datasourceRefreshNotifications[dsName];
if (_.isUndefined(refreshSettingNames)) {
refreshSettingNames = [];
self.datasourceRefreshNotifications[dsName] = refreshSettingNames;
}
if(_.indexOf(refreshSettingNames, settingDef.name) == -1) // Only subscribe to this notification once.
{
refreshSettingNames.push(settingDef.name);
}
}
}
}
});
}
this._heightUpdate = ko.observable();
this.height = ko.computed({
read: function () {
self._heightUpdate();
if (!_.isUndefined(self.widgetInstance) && _.isFunction(self.widgetInstance.getHeight)) {
return self.widgetInstance.getHeight();
}
return 1;
}
});
this.shouldRender = ko.observable(false);
this.render = function (element) {
self.shouldRender(false);
if (!_.isUndefined(self.widgetInstance) && _.isFunction(self.widgetInstance.render)) {
self.widgetInstance.render(element);
self.updateCalculatedSettings();
}
}
this.dispose = function () {
}
this.serialize = function () {
return {
title: self.title(),
type: self.type(),
settings: self.settings()
};
}
this.deserialize = function (object) {
self.title(object.title);
self.settings(object.settings);
self.type(object.type);
}
}
// ┌────────────────────────────────────────────────────────────────────┐ \\
// │ F R E E B O A R D │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Copyright © 2013 Jim Heising (https://github.com/jheising) │ \\
// │ Copyright © 2013 Bug Labs, Inc. (http://buglabs.net) │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Licensed under the MIT license. │ \\
// └────────────────────────────────────────────────────────────────────┘ \\
// Jquery plugin to watch for attribute changes
(function($)
{
function isDOMAttrModifiedSupported()
{
var p = document.createElement('p');
var flag = false;
if(p.addEventListener)
{
p.addEventListener('DOMAttrModified', function()
{
flag = true
}, false);
}
else if(p.attachEvent)
{
p.attachEvent('onDOMAttrModified', function()
{
flag = true
});
}
else
{
return false;
}
p.setAttribute('id', 'target');
return flag;
}
function checkAttributes(chkAttr, e)
{
if(chkAttr)
{
var attributes = this.data('attr-old-value');
if(e.attributeName.indexOf('style') >= 0)
{
if(!attributes['style'])
{
attributes['style'] = {};
} //initialize
var keys = e.attributeName.split('.');
e.attributeName = keys[0];
e.oldValue = attributes['style'][keys[1]]; //old value
e.newValue = keys[1] + ':' + this.prop("style")[$.camelCase(keys[1])]; //new value
attributes['style'][keys[1]] = e.newValue;
}
else
{
e.oldValue = attributes[e.attributeName];
e.newValue = this.attr(e.attributeName);
attributes[e.attributeName] = e.newValue;
}
this.data('attr-old-value', attributes); //update the old value object
}
}
//initialize Mutation Observer
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
$.fn.attrchange = function(o)
{
var cfg = {
trackValues: false,
callback : $.noop
};
//for backward compatibility
if(typeof o === "function")
{
cfg.callback = o;
}
else
{
$.extend(cfg, o);
}
if(cfg.trackValues)
{ //get attributes old value
$(this).each(function(i, el)
{
var attributes = {};
for(var attr, i = 0, attrs = el.attributes, l = attrs.length; i < l; i++)
{
attr = attrs.item(i);
attributes[attr.nodeName] = attr.value;
}
$(this).data('attr-old-value', attributes);
});
}
if(MutationObserver)
{ //Modern Browsers supporting MutationObserver
/*
Mutation Observer is still new and not supported by all browsers.
http://lists.w3.org/Archives/Public/public-webapps/2011JulSep/1622.html
*/
var mOptions = {
subtree : false,
attributes : true,
attributeOldValue: cfg.trackValues
};
var observer = new MutationObserver(function(mutations)
{
mutations.forEach(function(e)
{
var _this = e.target;
//get new value if trackValues is true
if(cfg.trackValues)
{
/**
* @KNOWN_ISSUE: The new value is buggy for STYLE attribute as we don't have
* any additional information on which style is getting updated.
* */
e.newValue = $(_this).attr(e.attributeName);
}
cfg.callback.call(_this, e);
});
});
return this.each(function()
{
observer.observe(this, mOptions);
});
}
else if(isDOMAttrModifiedSupported())
{ //Opera
//Good old Mutation Events but the performance is no good
//http://hacks.mozilla.org/2012/05/dom-mutationobserver-reacting-to-dom-changes-without-killing-browser-performance/
return this.on('DOMAttrModified', function(event)
{
if(event.originalEvent)
{
event = event.originalEvent;
} //jQuery normalization is not required for us
event.attributeName = event.attrName; //property names to be consistent with MutationObserver
event.oldValue = event.prevValue; //property names to be consistent with MutationObserver
cfg.callback.call(this, event);
});
}
else if('onpropertychange' in document.body)
{ //works only in IE
return this.on('propertychange', function(e)
{
e.attributeName = window.event.propertyName;
//to set the attr old value
checkAttributes.call($(this), cfg.trackValues, e);
cfg.callback.call(this, e);
});
}
return this;
}
})(jQuery);
(function(jQuery) {
jQuery.eventEmitter = {
_JQInit: function() {
this._JQ = jQuery(this);
},
emit: function(evt, data) {
!this._JQ && this._JQInit();
this._JQ.trigger(evt, data);
},
once: function(evt, handler) {
!this._JQ && this._JQInit();
this._JQ.one(evt, handler);
},
on: function(evt, handler) {
!this._JQ && this._JQInit();
this._JQ.bind(evt, handler);
},
off: function(evt, handler) {
!this._JQ && this._JQInit();
this._JQ.unbind(evt, handler);
}
};
}(jQuery));
var freeboard = (function()
{
var datasourcePlugins = {};
var widgetPlugins = {};
var freeboardUI = new FreeboardUI();
var theFreeboardModel = new FreeboardModel(datasourcePlugins, widgetPlugins, freeboardUI);
var jsEditor = new JSEditor();
var valueEditor = new ValueEditor(theFreeboardModel);
var pluginEditor = new PluginEditor(jsEditor, valueEditor);
var developerConsole = new DeveloperConsole(theFreeboardModel);
var currentStyle = {
values: {
"font-family": '"HelveticaNeue-UltraLight", "Helvetica Neue Ultra Light", "Helvetica Neue", sans-serif',
"color" : "#d3d4d4",
"font-weight": 100
}
};
ko.bindingHandlers.pluginEditor = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
{
var options = ko.unwrap(valueAccessor());
var types = {};
var settings = undefined;
var title = "";
if(options.type == 'datasource')
{
types = datasourcePlugins;
title = "Datasource";
}
else if(options.type == 'widget')
{
types = widgetPlugins;
title = "Widget";
}
else if(options.type == 'pane')
{
title = "Pane";
}
$(element).click(function(event)
{
if(options.operation == 'delete')
{
var phraseElement = $('<p>Are you sure you want to delete this ' + title + '?</p>');
new DialogBox(phraseElement, "Confirm Delete", "Yes", "No", function()
{
if(options.type == 'datasource')
{
theFreeboardModel.deleteDatasource(viewModel);
}
else if(options.type == 'widget')
{
theFreeboardModel.deleteWidget(viewModel);
}
else if(options.type == 'pane')
{
theFreeboardModel.deletePane(viewModel);
}
});
}
else
{
var instanceType = undefined;
if(options.type == 'datasource')
{
if(options.operation == 'add')
{
settings = {};
}
else
{
instanceType = viewModel.type();
settings = viewModel.settings();
settings.name = viewModel.name();
}
}
else if(options.type == 'widget')
{
if(options.operation == 'add')
{
settings = {};
}
else
{
instanceType = viewModel.type();
settings = viewModel.settings();
}
}
else if(options.type == 'pane')
{
settings = {};
if(options.operation == 'edit')
{
settings.title = viewModel.title();
settings.col_width = viewModel.col_width();
}
types = {
settings: {
settings: [
{
name : "title",
display_name: "Title",
type : "text"
},
{
name : "col_width",
display_name : "Columns",
type : "number",
default_value : 1,
required : true
}
]
}
}
}
pluginEditor.createPluginEditor(title, types, instanceType, settings, function(newSettings)
{
if(options.operation == 'add')
{
if(options.type == 'datasource')
{
var newViewModel = new DatasourceModel(theFreeboardModel, datasourcePlugins);
theFreeboardModel.addDatasource(newViewModel);
newViewModel.name(newSettings.settings.name);
delete newSettings.settings.name;
newViewModel.settings(newSettings.settings);
newViewModel.type(newSettings.type);
}
else if(options.type == 'widget')
{
var newViewModel = new WidgetModel(theFreeboardModel, widgetPlugins);
newViewModel.settings(newSettings.settings);
newViewModel.type(newSettings.type);
viewModel.widgets.push(newViewModel);
freeboardUI.attachWidgetEditIcons(element);
}
}
else if(options.operation == 'edit')
{
if(options.type == 'pane')
{
viewModel.title(newSettings.settings.title);
viewModel.col_width(newSettings.settings.col_width);
freeboardUI.processResize(false);
}
else
{
if(options.type == 'datasource')
{
viewModel.name(newSettings.settings.name);
delete newSettings.settings.name;
}
viewModel.type(newSettings.type);
viewModel.settings(newSettings.settings);
}
}
});
}
});
}
}
ko.virtualElements.allowedBindings.datasourceTypeSettings = true;
ko.bindingHandlers.datasourceTypeSettings = {
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
{
processPluginSettings(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
}
}
ko.bindingHandlers.pane = {
init : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
{
if(theFreeboardModel.isEditing())
{
$(element).css({cursor: "pointer"});
}
freeboardUI.addPane(element, viewModel, bindingContext.$root.isEditing());
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
{
// If pane has been removed
if(theFreeboardModel.panes.indexOf(viewModel) == -1)
{
freeboardUI.removePane(element);
}
freeboardUI.updatePane(element, viewModel);
}
}
ko.bindingHandlers.widget = {
init : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
{
if(theFreeboardModel.isEditing())
{
freeboardUI.attachWidgetEditIcons($(element).parent());
}
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext)
{
if(viewModel.shouldRender())
{
$(element).empty();
viewModel.render(element);
}
}
}
function getParameterByName(name)
{
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
$(function()
{ //DOM Ready
// Show the loading indicator when we first load
freeboardUI.showLoadingIndicator(true);
var resizeTimer;
function resizeEnd()
{
freeboardUI.processResize(true);
}
$(window).resize(function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(resizeEnd, 500);
});
});
// PUBLIC FUNCTIONS
return {
initialize : function(allowEdit, finishedCallback)
{
ko.applyBindings(theFreeboardModel);
// Check to see if we have a query param called load. If so, we should load that dashboard initially
var freeboardLocation = getParameterByName("load");
if(freeboardLocation != "")
{
$.ajax({
url : freeboardLocation,
success: function(data)
{
theFreeboardModel.loadDashboard(data);
if(_.isFunction(finishedCallback))
{
finishedCallback();
}
}
});
}
else
{
theFreeboardModel.allow_edit(allowEdit);
theFreeboardModel.setEditing(allowEdit);
freeboardUI.showLoadingIndicator(false);
if(_.isFunction(finishedCallback))
{
finishedCallback();
}
freeboard.emit("initialized");
}
},
newDashboard : function()
{
theFreeboardModel.loadDashboard({allow_edit: true});
},
loadDashboard : function(configuration, callback)
{
theFreeboardModel.loadDashboard(configuration, callback);
},
serialize : function()
{
return theFreeboardModel.serialize();
},
setEditing : function(editing, animate)
{
theFreeboardModel.setEditing(editing, animate);
},
isEditing : function()
{
return theFreeboardModel.isEditing();
},
loadDatasourcePlugin: function(plugin)
{
if(_.isUndefined(plugin.display_name))
{
plugin.display_name = plugin.type_name;
}
// Add a required setting called name to the beginning
plugin.settings.unshift({
name : "name",
display_name : "Name",
type : "text",
required : true
});
theFreeboardModel.addPluginSource(plugin.source);
datasourcePlugins[plugin.type_name] = plugin;
theFreeboardModel._datasourceTypes.valueHasMutated();
},
resize : function()
{
freeboardUI.processResize(true);
},
loadWidgetPlugin : function(plugin)
{
if(_.isUndefined(plugin.display_name))
{
plugin.display_name = plugin.type_name;
}
theFreeboardModel.addPluginSource(plugin.source);
widgetPlugins[plugin.type_name] = plugin;
theFreeboardModel._widgetTypes.valueHasMutated();
},
// To be used if freeboard is going to load dynamic assets from a different root URL
setAssetRoot : function(assetRoot)
{
jsEditor.setAssetRoot(assetRoot);
},
addStyle : function(selector, rules)
{
var styleString = selector + "{" + rules + "}";
var styleElement = $("style#fb-styles");
if(styleElement.length == 0)
{
styleElement = $('<style id="fb-styles" type="text/css"></style>');
$("head").append(styleElement);
}
if(styleElement[0].styleSheet)
{
styleElement[0].styleSheet.cssText += styleString;
}
else
{
styleElement.text(styleElement.text() + styleString);
}
},
showLoadingIndicator: function(show)
{
freeboardUI.showLoadingIndicator(show);
},
showDialog : function(contentElement, title, okTitle, cancelTitle, okCallback)
{
new DialogBox(contentElement, title, okTitle, cancelTitle, okCallback);
},
getDatasourceSettings : function(datasourceName)
{
var datasources = theFreeboardModel.datasources();
// Find the datasource with the name specified
var datasource = _.find(datasources, function(datasourceModel){
return (datasourceModel.name() === datasourceName);
});
if(datasource)
{
return datasource.settings();
}
else
{
return null;
}
},
setDatasourceSettings : function(datasourceName, settings)
{
var datasources = theFreeboardModel.datasources();
// Find the datasource with the name specified
var datasource = _.find(datasources, function(datasourceModel){
return (datasourceModel.name() === datasourceName);
});
if(!datasource)
{
console.log("Datasource not found");
return;
}
var combinedSettings = _.defaults(settings, datasource.settings());
datasource.settings(combinedSettings);
},
getStyleString : function(name)
{
var returnString = "";
_.each(currentStyle[name], function(value, name)
{
returnString = returnString + name + ":" + value + ";";
});
return returnString;
},
getStyleObject : function(name)
{
return currentStyle[name];
},
showDeveloperConsole : function()
{
developerConsole.showDeveloperConsole();
}
};
}());
$.extend(freeboard, jQuery.eventEmitter);
// ┌────────────────────────────────────────────────────────────────────┐ \\
// │ F R E E B O A R D │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Copyright © 2013 Jim Heising (https://github.com/jheising) │ \\
// │ Copyright © 2013 Bug Labs, Inc. (http://buglabs.net) │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Licensed under the MIT license. │ \\
// └────────────────────────────────────────────────────────────────────┘ \\
(function () {
var jsonDatasource = function (settings, updateCallback) {
var self = this;
var updateTimer = null;
var currentSettings = settings;
var errorStage = 0; // 0 = try standard request
// 1 = try JSONP
// 2 = try thingproxy.freeboard.io
var lockErrorStage = false;
function updateRefresh(refreshTime) {
if (updateTimer) {
clearInterval(updateTimer);
}
updateTimer = setInterval(function () {
self.updateNow();
}, refreshTime);
}
updateRefresh(currentSettings.refresh * 1000);
this.updateNow = function () {
if ((errorStage > 1 && !currentSettings.use_thingproxy) || errorStage > 2) // We've tried everything, let's quit
{
return; // TODO: Report an error
}
var requestURL = currentSettings.url;
if (errorStage == 2 && currentSettings.use_thingproxy) {
requestURL = (location.protocol == "https:" ? "https:" : "http:") + "//thingproxy.freeboard.io/fetch/" + encodeURI(currentSettings.url);
}
var body = currentSettings.body;
// Can the body be converted to JSON?
if (body) {
try {
body = JSON.parse(body);
}
catch (e) {
}
}
$.ajax({
url: requestURL,
dataType: (errorStage == 1) ? "JSONP" : "JSON",
type: currentSettings.method || "GET",
data: body,
beforeSend: function (xhr) {
try {
_.each(currentSettings.headers, function (header) {
var name = header.name;
var value = header.value;
if (!_.isUndefined(name) && !_.isUndefined(value)) {
xhr.setRequestHeader(name, value);
}
});
}
catch (e) {
}
},
success: function (data) {
lockErrorStage = true;
updateCallback(data);
},
error: function (xhr, status, error) {
if (!lockErrorStage) {
// TODO: Figure out a way to intercept CORS errors only. The error message for CORS errors seems to be a standard 404.
errorStage++;
self.updateNow();
}
}
});
}
this.onDispose = function () {
clearInterval(updateTimer);
updateTimer = null;
}
this.onSettingsChanged = function (newSettings) {
lockErrorStage = false;
errorStage = 0;
currentSettings = newSettings;
updateRefresh(currentSettings.refresh * 1000);
self.updateNow();
}
};
freeboard.loadDatasourcePlugin({
type_name: "JSON",
settings: [
{
name: "url",
display_name: "URL",
type: "text"
},
{
name: "use_thingproxy",
display_name: "Try thingproxy",
description: 'A direct JSON connection will be tried first, if that fails, a JSONP connection will be tried. If that fails, you can use thingproxy, which can solve many connection problems to APIs. <a href="https://github.com/Freeboard/thingproxy" target="_blank">More information</a>.',
type: "boolean",
default_value: true
},
{
name: "refresh",
display_name: "Refresh Every",
type: "number",
suffix: "seconds",
default_value: 5
},
{
name: "method",
display_name: "Method",
type: "option",
options: [
{
name: "GET",
value: "GET"
},
{
name: "POST",
value: "POST"
},
{
name: "PUT",
value: "PUT"
},
{
name: "DELETE",
value: "DELETE"
}
]
},
{
name: "body",
display_name: "Body",
type: "text",
description: "The body of the request. Normally only used if method is POST"
},
{
name: "headers",
display_name: "Headers",
type: "array",
settings: [
{
name: "name",
display_name: "Name",
type: "text"
},
{
name: "value",
display_name: "Value",
type: "text"
}
]
}
],
newInstance: function (settings, newInstanceCallback, updateCallback) {
newInstanceCallback(new jsonDatasource(settings, updateCallback));
}
});
var openWeatherMapDatasource = function (settings, updateCallback) {
var self = this;
var updateTimer = null;
var currentSettings = settings;
function updateRefresh(refreshTime) {
if (updateTimer) {
clearInterval(updateTimer);
}
updateTimer = setInterval(function () {
self.updateNow();
}, refreshTime);
}
function toTitleCase(str) {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
}
updateRefresh(currentSettings.refresh * 1000);
this.updateNow = function () {
$.ajax({
url: "http://api.openweathermap.org/data/2.5/weather?q=" + encodeURIComponent(currentSettings.location) + "&units=" + currentSettings.units,
dataType: "JSONP",
success: function (data) {
// Rejigger our data into something easier to understand
var newData = {
place_name: data.name,
sunrise: (new Date(data.sys.sunrise * 1000)).toLocaleTimeString(),
sunset: (new Date(data.sys.sunset * 1000)).toLocaleTimeString(),
conditions: toTitleCase(data.weather[0].description),
current_temp: data.main.temp,
high_temp: data.main.temp_max,
low_temp: data.main.temp_min,
pressure: data.main.pressure,
humidity: data.main.humidity,
wind_speed: data.wind.speed,
wind_direction: data.wind.deg
};
updateCallback(newData);
},
error: function (xhr, status, error) {
}
});
}
this.onDispose = function () {
clearInterval(updateTimer);
updateTimer = null;
}
this.onSettingsChanged = function (newSettings) {
currentSettings = newSettings;
self.updateNow();
updateRefresh(currentSettings.refresh * 1000);
}
};
freeboard.loadDatasourcePlugin({
type_name: "openweathermap",
display_name: "Open Weather Map API",
settings: [
{
name: "location",
display_name: "Location",
type: "text",
description: "Example: London, UK"
},
{
name: "units",
display_name: "Units",
type: "option",
default: "imperial",
options: [
{
name: "Imperial",
value: "imperial"
},
{
name: "Metric",
value: "metric"
}
]
},
{
name: "refresh",
display_name: "Refresh Every",
type: "number",
suffix: "seconds",
default_value: 5
}
],
newInstance: function (settings, newInstanceCallback, updateCallback) {
newInstanceCallback(new openWeatherMapDatasource(settings, updateCallback));
}
});
var dweetioDatasource = function (settings, updateCallback) {
var self = this;
var currentSettings = settings;
function onNewDweet(dweet) {
updateCallback(dweet);
}
this.updateNow = function () {
dweetio.get_latest_dweet_for(currentSettings.thing_id, function (err, dweet) {
if (err) {
//onNewDweet({});
}
else {
onNewDweet(dweet[0].content);
}
});
}
this.onDispose = function () {
}
this.onSettingsChanged = function (newSettings) {
dweetio.stop_listening();
currentSettings = newSettings;
dweetio.listen_for(currentSettings.thing_id, function (dweet) {
onNewDweet(dweet.content);
});
}
self.onSettingsChanged(settings);
};
freeboard.loadDatasourcePlugin({
"type_name": "dweet_io",
"display_name": "Dweet.io",
"external_scripts": [
"http://dweet.io/client/dweet.io.min.js"
],
"settings": [
{
name: "thing_id",
display_name: "Thing Name",
"description": "Example: salty-dog-1",
type: "text"
}
],
newInstance: function (settings, newInstanceCallback, updateCallback) {
newInstanceCallback(new dweetioDatasource(settings, updateCallback));
}
});
var playbackDatasource = function (settings, updateCallback) {
var self = this;
var currentSettings = settings;
var currentDataset = [];
var currentIndex = 0;
var currentTimeout;
function moveNext() {
if (currentDataset.length > 0) {
if (currentIndex < currentDataset.length) {
updateCallback(currentDataset[currentIndex]);
currentIndex++;
}
if (currentIndex >= currentDataset.length && currentSettings.loop) {
currentIndex = 0;
}
if (currentIndex < currentDataset.length) {
currentTimeout = setTimeout(moveNext, currentSettings.refresh * 1000);
}
}
else {
updateCallback({});
}
}
function stopTimeout() {
currentDataset = [];
currentIndex = 0;
if (currentTimeout) {
clearTimeout(currentTimeout);
currentTimeout = null;
}
}
this.updateNow = function () {
stopTimeout();
$.ajax({
url: currentSettings.datafile,
dataType: (currentSettings.is_jsonp) ? "JSONP" : "JSON",
success: function (data) {
if (_.isArray(data)) {
currentDataset = data;
}
else {
currentDataset = [];
}
currentIndex = 0;
moveNext();
},
error: function (xhr, status, error) {
}
});
}
this.onDispose = function () {
stopTimeout();
}
this.onSettingsChanged = function (newSettings) {
currentSettings = newSettings;
self.updateNow();
}
};
freeboard.loadDatasourcePlugin({
"type_name": "playback",
"display_name": "Playback",
"settings": [
{
"name": "datafile",
"display_name": "Data File URL",
"type": "text",
"description": "A link to a JSON array of data."
},
{
name: "is_jsonp",
display_name: "Is JSONP",
type: "boolean"
},
{
"name": "loop",
"display_name": "Loop",
"type": "boolean",
"description": "Rewind and loop when finished"
},
{
"name": "refresh",
"display_name": "Refresh Every",
"type": "number",
"suffix": "seconds",
"default_value": 5
}
],
newInstance: function (settings, newInstanceCallback, updateCallback) {
newInstanceCallback(new playbackDatasource(settings, updateCallback));
}
});
var clockDatasource = function (settings, updateCallback) {
var self = this;
var currentSettings = settings;
var timer;
function stopTimer() {
if (timer) {
clearTimeout(timer);
timer = null;
}
}
function updateTimer() {
stopTimer();
timer = setInterval(self.updateNow, currentSettings.refresh * 1000);
}
this.updateNow = function () {
var date = new Date();
var data = {
numeric_value: date.getTime(),
full_string_value: date.toLocaleString(),
date_string_value: date.toLocaleDateString(),
time_string_value: date.toLocaleTimeString(),
date_object: date
};
updateCallback(data);
}
this.onDispose = function () {
stopTimer();
}
this.onSettingsChanged = function (newSettings) {
currentSettings = newSettings;
updateTimer();
}
updateTimer();
};
freeboard.loadDatasourcePlugin({
"type_name": "clock",
"display_name": "Clock",
"settings": [
{
"name": "refresh",
"display_name": "Refresh Every",
"type": "number",
"suffix": "seconds",
"default_value": 1
}
],
newInstance: function (settings, newInstanceCallback, updateCallback) {
newInstanceCallback(new clockDatasource(settings, updateCallback));
}
});
}());
// ┌────────────────────────────────────────────────────────────────────┐ \\
// │ F R E E B O A R D │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Copyright © 2013 Jim Heising (https://github.com/jheising) │ \\
// │ Copyright © 2013 Bug Labs, Inc. (http://buglabs.net) │ \\
// ├────────────────────────────────────────────────────────────────────┤ \\
// │ Licensed under the MIT license. │ \\
// └────────────────────────────────────────────────────────────────────┘ \\
(function () {
var SPARKLINE_HISTORY_LENGTH = 100;
function easeTransitionText(newValue, textElement, duration) {
var currentValue = $(textElement).text();
if (currentValue == newValue)
return;
if ($.isNumeric(newValue) && $.isNumeric(currentValue)) {
var numParts = newValue.toString().split('.');
var endingPrecision = 0;
if (numParts.length > 1) {
endingPrecision = numParts[1].length;
}
numParts = currentValue.toString().split('.');
var startingPrecision = 0;
if (numParts.length > 1) {
startingPrecision = numParts[1].length;
}
jQuery({transitionValue: Number(currentValue), precisionValue: startingPrecision}).animate({transitionValue: Number(newValue), precisionValue: endingPrecision}, {
duration: duration,
step: function () {
$(textElement).text(this.transitionValue.toFixed(this.precisionValue));
},
done: function () {
$(textElement).text(newValue);
}
});
}
else {
$(textElement).text(newValue);
}
}
function addValueToSparkline(element, value) {
var values = $(element).data().values;
if (!values) {
values = [];
}
if (values.length >= SPARKLINE_HISTORY_LENGTH) {
values.shift();
}
values.push(Number(value));
$(element).data().values = values;
$(element).sparkline(values, {
type: "line",
height: "100%",
width: "100%",
fillColor: false,
lineColor: "#FF9900",
lineWidth: 2,
spotRadius: 3,
spotColor: false,
minSpotColor: "#78AB49",
maxSpotColor: "#78AB49",
highlightSpotColor: "#9D3926",
highlightLineColor: "#9D3926"
});
}
var valueStyle = freeboard.getStyleString("values");
freeboard.addStyle('.widget-big-text', valueStyle + "font-size:75px;");
freeboard.addStyle('.tw-display', 'width: 100%; height:100%; display:table; table-layout:fixed;');
freeboard.addStyle('.tw-tr',
'display:table-row;');
freeboard.addStyle('.tw-tg',
'display:table-row-group;');
freeboard.addStyle('.tw-tc',
'display:table-caption;');
freeboard.addStyle('.tw-td',
'display:table-cell;');
freeboard.addStyle('.tw-value',
valueStyle +
'overflow: hidden;' +
'display: inline-block;' +
'text-overflow: ellipsis;');
freeboard.addStyle('.tw-unit',
'display: inline-block;' +
'padding-left: 10px;' +
'padding-bottom: 1.1em;' +
'vertical-align: bottom;');
freeboard.addStyle('.tw-value-wrapper',
'position: relative;' +
'vertical-align: middle;' +
'height:100%;');
freeboard.addStyle('.tw-sparkline',
'height:20px;');
var textWidget = function (settings) {
var self = this;
var currentSettings = settings;
var displayElement = $('<div class="tw-display"></div>');
var titleElement = $('<h2 class="section-title tw-title tw-td"></h2>');
var valueElement = $('<div class="tw-value"></div>');
var unitsElement = $('<div class="tw-unit"></div>');
var sparklineElement = $('<div class="tw-sparkline tw-td"></div>');
function updateValueSizing()
{
if(!_.isUndefined(currentSettings.units) && currentSettings.units != "") // If we're displaying our units
{
valueElement.css("max-width", (displayElement.innerWidth() - unitsElement.outerWidth(true)) + "px");
}
else
{
valueElement.css("max-width", "100%");
}
}
this.render = function (element) {
$(element).empty();
$(displayElement)
.append($('<div class="tw-tr"></div>').append(titleElement))
.append($('<div class="tw-tr"></div>').append($('<div class="tw-value-wrapper tw-td"></div>').append(valueElement).append(unitsElement)))
.append($('<div class="tw-tr"></div>').append(sparklineElement));
$(element).append(displayElement);
updateValueSizing();
}
this.onSettingsChanged = function (newSettings) {
currentSettings = newSettings;
var shouldDisplayTitle = (!_.isUndefined(newSettings.title) && newSettings.title != "");
var shouldDisplayUnits = (!_.isUndefined(newSettings.units) && newSettings.units != "");
if(newSettings.sparkline)
{
sparklineElement.attr("style", null);
}
else
{
delete sparklineElement.data().values;
sparklineElement.empty();
sparklineElement.hide();
}
if(shouldDisplayTitle)
{
titleElement.html((_.isUndefined(newSettings.title) ? "" : newSettings.title));
titleElement.attr("style", null);
}
else
{
titleElement.empty();
titleElement.hide();
}
if(shouldDisplayUnits)
{
unitsElement.html((_.isUndefined(newSettings.units) ? "" : newSettings.units));
unitsElement.attr("style", null);
}
else
{
unitsElement.empty();
unitsElement.hide();
}
var valueFontSize = 30;
if(newSettings.size == "big")
{
valueFontSize = 75;
if(newSettings.sparkline)
{
valueFontSize = 60;
}
}
valueElement.css({"font-size" : valueFontSize + "px"});
updateValueSizing();
}
this.onSizeChanged = function()
{
updateValueSizing();
}
this.onCalculatedValueChanged = function (settingName, newValue) {
if (settingName == "value") {
if (currentSettings.animate) {
easeTransitionText(newValue, valueElement, 500);
}
else {
valueElement.text(newValue);
}
if (currentSettings.sparkline) {
addValueToSparkline(sparklineElement, newValue);
}
}
}
this.onDispose = function () {
}
this.getHeight = function () {
if (currentSettings.size == "big" || currentSettings.sparkline) {
return 2;
}
else {
return 1;
}
}
this.onSettingsChanged(settings);
};
freeboard.loadWidgetPlugin({
type_name: "text_widget",
display_name: "Text",
"external_scripts" : [
"plugins/thirdparty/jquery.sparkline.min.js"
],
settings: [
{
name: "title",
display_name: "Title",
type: "text"
},
{
name: "size",
display_name: "Size",
type: "option",
options: [
{
name: "Regular",
value: "regular"
},
{
name: "Big",
value: "big"
}
]
},
{
name: "value",
display_name: "Value",
type: "calculated"
},
{
name: "sparkline",
display_name: "Include Sparkline",
type: "boolean"
},
{
name: "animate",
display_name: "Animate Value Changes",
type: "boolean",
default_value: true
},
{
name: "units",
display_name: "Units",
type: "text"
}
],
newInstance: function (settings, newInstanceCallback) {
newInstanceCallback(new textWidget(settings));
}
});
var gaugeID = 0;
freeboard.addStyle('.gauge-widget-wrapper', "width: 100%;text-align: center;");
freeboard.addStyle('.gauge-widget', "width:200px;height:160px;display:inline-block;");
var gaugeWidget = function (settings) {
var self = this;
var thisGaugeID = "gauge-" + gaugeID++;
var titleElement = $('<h2 class="section-title"></h2>');
var gaugeElement = $('<div class="gauge-widget" id="' + thisGaugeID + '"></div>');
var gaugeObject;
var rendered = false;
var currentSettings = settings;
function createGauge() {
if (!rendered) {
return;
}
gaugeElement.empty();
gaugeObject = new JustGage({
id: thisGaugeID,
value: (_.isUndefined(currentSettings.min_value) ? 0 : currentSettings.min_value),
min: (_.isUndefined(currentSettings.min_value) ? 0 : currentSettings.min_value),
max: (_.isUndefined(currentSettings.max_value) ? 0 : currentSettings.max_value),
label: currentSettings.units,
showInnerShadow: false,
valueFontColor: "#d3d4d4"
});
}
this.render = function (element) {
rendered = true;
$(element).append(titleElement).append($('<div class="gauge-widget-wrapper"></div>').append(gaugeElement));
createGauge();
}
this.onSettingsChanged = function (newSettings) {
if (newSettings.min_value != currentSettings.min_value || newSettings.max_value != currentSettings.max_value || newSettings.units != currentSettings.units) {
currentSettings = newSettings;
createGauge();
}
else {
currentSettings = newSettings;
}
titleElement.html(newSettings.title);
}
this.onCalculatedValueChanged = function (settingName, newValue) {
if (!_.isUndefined(gaugeObject)) {
gaugeObject.refresh(Number(newValue));
}
}
this.onDispose = function () {
}
this.getHeight = function () {
return 3;
}
this.onSettingsChanged(settings);
};
freeboard.loadWidgetPlugin({
type_name: "gauge",
display_name: "Gauge",
"external_scripts" : [
"plugins/thirdparty/raphael.2.1.0.min.js",
"plugins/thirdparty/justgage.1.0.1.js"
],
settings: [
{
name: "title",
display_name: "Title",
type: "text"
},
{
name: "value",
display_name: "Value",
type: "calculated"
},
{
name: "units",
display_name: "Units",
type: "text"
},
{
name: "min_value",
display_name: "Minimum",
type: "text",
default_value: 0
},
{
name: "max_value",
display_name: "Maximum",
type: "text",
default_value: 100
}
],
newInstance: function (settings, newInstanceCallback) {
newInstanceCallback(new gaugeWidget(settings));
}
});
freeboard.addStyle('.sparkline', "width:100%;height: 75px;");
var sparklineWidget = function (settings) {
var self = this;
var titleElement = $('<h2 class="section-title"></h2>');
var sparklineElement = $('<div class="sparkline"></div>');
this.render = function (element) {
$(element).append(titleElement).append(sparklineElement);
}
this.onSettingsChanged = function (newSettings) {
titleElement.html((_.isUndefined(newSettings.title) ? "" : newSettings.title));
}
this.onCalculatedValueChanged = function (settingName, newValue) {
addValueToSparkline(sparklineElement, newValue);
}
this.onDispose = function () {
}
this.getHeight = function () {
return 2;
}
this.onSettingsChanged(settings);
};
freeboard.loadWidgetPlugin({
type_name: "sparkline",
display_name: "Sparkline",
"external_scripts" : [
"plugins/thirdparty/jquery.sparkline.min.js"
],
settings: [
{
name: "title",
display_name: "Title",
type: "text"
},
{
name: "value",
display_name: "Value",
type: "calculated"
}
],
newInstance: function (settings, newInstanceCallback) {
newInstanceCallback(new sparklineWidget(settings));
}
});
freeboard.addStyle('div.pointer-value', "position:absolute;height:95px;margin: auto;top: 0px;bottom: 0px;width: 100%;text-align:center;");
var pointerWidget = function (settings) {
var self = this;
var paper;
var strokeWidth = 3;
var triangle;
var width, height;
var currentValue = 0;
var valueDiv = $('<div class="widget-big-text"></div>');
var unitsDiv = $('<div></div>');
function polygonPath(points) {
if (!points || points.length < 2)
return [];
var path = []; //will use path object type
path.push(['m', points[0], points[1]]);
for (var i = 2; i < points.length; i += 2) {
path.push(['l', points[i], points[i + 1]]);
}
path.push(['z']);
return path;
}
this.render = function (element) {
width = $(element).width();
height = $(element).height();
var radius = Math.min(width, height) / 2 - strokeWidth * 2;
paper = Raphael($(element).get()[0], width, height);
var circle = paper.circle(width / 2, height / 2, radius);
circle.attr("stroke", "#FF9900");
circle.attr("stroke-width", strokeWidth);
triangle = paper.path(polygonPath([width / 2, (height / 2) - radius + strokeWidth, 15, 20, -30, 0]));
triangle.attr("stroke-width", 0);
triangle.attr("fill", "#fff");
$(element).append($('<div class="pointer-value"></div>').append(valueDiv).append(unitsDiv));
}
this.onSettingsChanged = function (newSettings) {
unitsDiv.html(newSettings.units);
}
this.onCalculatedValueChanged = function (settingName, newValue) {
if (settingName == "direction") {
if (!_.isUndefined(triangle)) {
var direction = "r";
var oppositeCurrent = currentValue + 180;
if (oppositeCurrent < newValue) {
//direction = "l";
}
triangle.animate({transform: "r" + newValue + "," + (width / 2) + "," + (height / 2)}, 250, "bounce");
}
currentValue = newValue;
}
else if (settingName == "value_text") {
valueDiv.html(newValue);
}
}
this.onDispose = function () {
}
this.getHeight = function () {
return 4;
}
this.onSettingsChanged(settings);
};
freeboard.loadWidgetPlugin({
type_name: "pointer",
display_name: "Pointer",
"external_scripts" : [
"plugins/thirdparty/raphael.2.1.0.min.js"
],
settings: [
{
name: "direction",
display_name: "Direction",
type: "calculated",
description: "In degrees"
},
{
name: "value_text",
display_name: "Value Text",
type: "calculated"
},
{
name: "units",
display_name: "Units",
type: "text"
}
],
newInstance: function (settings, newInstanceCallback) {
newInstanceCallback(new pointerWidget(settings));
}
});
var pictureWidget = function(settings)
{
var self = this;
var widgetElement;
var timer;
var imageURL;
function stopTimer()
{
if(timer)
{
clearInterval(timer);
timer = null;
}
}
function updateImage()
{
if(widgetElement && imageURL)
{
var cacheBreakerURL = imageURL + (imageURL.indexOf("?") == -1 ? "?" : "&") + Date.now();
$(widgetElement).css({
"background-image" : "url(" + cacheBreakerURL + ")"
});
}
}
this.render = function(element)
{
$(element).css({
width : "100%",
height: "100%",
"background-size" : "cover",
"background-position" : "center"
});
widgetElement = element;
}
this.onSettingsChanged = function(newSettings)
{
stopTimer();
if(newSettings.refresh && newSettings.refresh > 0)
{
timer = setInterval(updateImage, Number(newSettings.refresh) * 1000);
}
}
this.onCalculatedValueChanged = function(settingName, newValue)
{
if(settingName == "src")
{
imageURL = newValue;
}
updateImage();
}
this.onDispose = function()
{
stopTimer();
}
this.getHeight = function()
{
return 4;
}
this.onSettingsChanged(settings);
};
freeboard.loadWidgetPlugin({
type_name: "picture",
display_name: "Picture",
fill_size: true,
settings: [
{
name: "src",
display_name: "Image URL",
type: "calculated"
},
{
"type": "number",
"display_name": "Refresh every",
"name": "refresh",
"suffix": "seconds",
"description":"Leave blank if the image doesn't need to be refreshed"
}
],
newInstance: function (settings, newInstanceCallback) {
newInstanceCallback(new pictureWidget(settings));
}
});
freeboard.addStyle('.indicator-light', "border-radius:50%;width:22px;height:22px;border:2px solid #3d3d3d;margin-top:5px;float:left;background-color:#222;margin-right:10px;");
freeboard.addStyle('.indicator-light.on', "background-color:#FFC773;box-shadow: 0px 0px 15px #FF9900;border-color:#FDF1DF;");
freeboard.addStyle('.indicator-text', "margin-top:10px;");
var indicatorWidget = function (settings) {
var self = this;
var titleElement = $('<h2 class="section-title"></h2>');
var stateElement = $('<div class="indicator-text"></div>');
var indicatorElement = $('<div class="indicator-light"></div>');
var currentSettings = settings;
var isOn = false;
function updateState() {
indicatorElement.toggleClass("on", isOn);
if (isOn) {
stateElement.text((_.isUndefined(currentSettings.on_text) ? "" : currentSettings.on_text));
}
else {
stateElement.text((_.isUndefined(currentSettings.off_text) ? "" : currentSettings.off_text));
}
}
this.render = function (element) {
$(element).append(titleElement).append(indicatorElement).append(stateElement);
}
this.onSettingsChanged = function (newSettings) {
currentSettings = newSettings;
titleElement.html((_.isUndefined(newSettings.title) ? "" : newSettings.title));
updateState();
}
this.onCalculatedValueChanged = function (settingName, newValue) {
if (settingName == "value") {
isOn = Boolean(newValue);
}
updateState();
}
this.onDispose = function () {
}
this.getHeight = function () {
return 1;
}
this.onSettingsChanged(settings);
};
freeboard.loadWidgetPlugin({
type_name: "indicator",
display_name: "Indicator Light",
settings: [
{
name: "title",
display_name: "Title",
type: "text"
},
{
name: "value",
display_name: "Value",
type: "calculated"
},
{
name: "on_text",
display_name: "On Text",
type: "calculated"
},
{
name: "off_text",
display_name: "Off Text",
type: "calculated"
}
],
newInstance: function (settings, newInstanceCallback) {
newInstanceCallback(new indicatorWidget(settings));
}
});
freeboard.addStyle('.gm-style-cc a', "text-shadow:none;");
var googleMapWidget = function (settings) {
var self = this;
var currentSettings = settings;
var map;
var marker;
var currentPosition = {};
function updatePosition() {
if (map && marker && currentPosition.lat && currentPosition.lon) {
var newLatLon = new google.maps.LatLng(currentPosition.lat, currentPosition.lon);
marker.setPosition(newLatLon);
map.panTo(newLatLon);
}
}
this.render = function (element) {
function initializeMap() {
var mapOptions = {
zoom: 13,
center: new google.maps.LatLng(37.235, -115.811111),
disableDefaultUI: true,
draggable: false,
styles: [
{"featureType": "water", "elementType": "geometry", "stylers": [
{"color": "#2a2a2a"}
]},
{"featureType": "landscape", "elementType": "geometry", "stylers": [
{"color": "#000000"},
{"lightness": 20}
]},
{"featureType": "road.highway", "elementType": "geometry.fill", "stylers": [
{"color": "#000000"},
{"lightness": 17}
]},
{"featureType": "road.highway", "elementType": "geometry.stroke", "stylers": [
{"color": "#000000"},
{"lightness": 29},
{"weight": 0.2}
]},
{"featureType": "road.arterial", "elementType": "geometry", "stylers": [
{"color": "#000000"},
{"lightness": 18}
]},
{"featureType": "road.local", "elementType": "geometry", "stylers": [
{"color": "#000000"},
{"lightness": 16}
]},
{"featureType": "poi", "elementType": "geometry", "stylers": [
{"color": "#000000"},
{"lightness": 21}
]},
{"elementType": "labels.text.stroke", "stylers": [
{"visibility": "on"},
{"color": "#000000"},
{"lightness": 16}
]},
{"elementType": "labels.text.fill", "stylers": [
{"saturation": 36},
{"color": "#000000"},
{"lightness": 40}
]},
{"elementType": "labels.icon", "stylers": [
{"visibility": "off"}
]},
{"featureType": "transit", "elementType": "geometry", "stylers": [
{"color": "#000000"},
{"lightness": 19}
]},
{"featureType": "administrative", "elementType": "geometry.fill", "stylers": [
{"color": "#000000"},
{"lightness": 20}
]},
{"featureType": "administrative", "elementType": "geometry.stroke", "stylers": [
{"color": "#000000"},
{"lightness": 17},
{"weight": 1.2}
]}
]
};
map = new google.maps.Map(element, mapOptions);
google.maps.event.addDomListener(element, 'mouseenter', function (e) {
e.cancelBubble = true;
if (!map.hover) {
map.hover = true;
map.setOptions({zoomControl: true});
}
});
google.maps.event.addDomListener(element, 'mouseleave', function (e) {
if (map.hover) {
map.setOptions({zoomControl: false});
map.hover = false;
}
});
marker = new google.maps.Marker({map: map});
updatePosition();
}
if (window.google && window.google.maps) {
initializeMap();
}
else {
window.gmap_initialize = initializeMap;
head.js("https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&callback=gmap_initialize");
}
}
this.onSettingsChanged = function (newSettings) {
currentSettings = newSettings;
}
this.onCalculatedValueChanged = function (settingName, newValue) {
if (settingName == "lat") {
currentPosition.lat = newValue;
}
else if (settingName == "lon") {
currentPosition.lon = newValue;
}
updatePosition();
}
this.onDispose = function () {
}
this.getHeight = function () {
return 4;
}
this.onSettingsChanged(settings);
};
freeboard.loadWidgetPlugin({
type_name: "google_map",
display_name: "Google Map",
fill_size: true,
settings: [
{
name: "lat",
display_name: "Latitude",
type: "calculated"
},
{
name: "lon",
display_name: "Longitude",
type: "calculated"
}
],
newInstance: function (settings, newInstanceCallback) {
newInstanceCallback(new googleMapWidget(settings));
}
});
freeboard.addStyle('.html-widget', "white-space:normal;width:100%;height:100%");
var htmlWidget = function (settings) {
var self = this;
var htmlElement = $('<div class="html-widget"></div>');
var currentSettings = settings;
this.render = function (element) {
$(element).append(htmlElement);
}
this.onSettingsChanged = function (newSettings) {
currentSettings = newSettings;
}
this.onCalculatedValueChanged = function (settingName, newValue) {
if (settingName == "html") {
htmlElement.html(newValue);
}
}
this.onDispose = function () {
}
this.getHeight = function () {
return Number(currentSettings.height);
}
this.onSettingsChanged(settings);
};
freeboard.loadWidgetPlugin({
"type_name": "html",
"display_name": "HTML",
"fill_size": true,
"settings": [
{
"name": "html",
"display_name": "HTML",
"type": "calculated",
"description": "Can be literal HTML, or javascript that outputs HTML."
},
{
"name": "height",
"display_name": "Height Blocks",
"type": "number",
"default_value": 4,
"description": "A height block is around 60 pixels"
}
],
newInstance: function (settings, newInstanceCallback) {
newInstanceCallback(new htmlWidget(settings));
}
});
}());