4301 lines
112 KiB
JavaScript
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));
|
|
}
|
|
});
|
|
|
|
}());
|