freeboard-leaflet-realtime
Version:
Freeboard plugin for real time Leaflet web maps
1,927 lines (1,604 loc) • 136 kB
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.saveDashboardClicked = function(){
var target = $(event.currentTarget);
var siblingsShown = target.data('siblings-shown') || false;
if(!siblingsShown){
$(event.currentTarget).siblings('label').fadeIn('slow');
}else{
$(event.currentTarget).siblings('label').fadeOut('slow');
}
target.data('siblings-shown', !siblingsShown);
}
this.saveDashboard = function(_thisref, event)
{
var pretty = $(event.currentTarget).data('pretty');
var contentType = 'application/octet-stream';
var a = document.createElement('a');
if(pretty){
var blob = new Blob([JSON.stringify(self.serialize(), null, '\t')], {'type': contentType});
}else{
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: Number(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 _appendCalculatedSettingRow(valueCell, newSettings, settingDef, currentValue, includeRemove)
{
var input = $('<textarea></textarea>');
if(settingDef.multi_input) {
input.change(function() {
var arrayInput = [];
$(valueCell).find('textarea').each(function() {
var thisVal = $(this).val();
if(thisVal) {
arrayInput = arrayInput.concat(thisVal);
}
});
newSettings.settings[settingDef.name] = arrayInput;
});
} else {
input.change(function() {
newSettings.settings[settingDef.name] = $(this).val();
});
}
if(currentValue) {
input.val(currentValue);
}
valueEditor.createValueEditor(input);
var datasourceToolbox = $('<ul class="board-toolbar datasource-input-suffix"></ul>');
var wrapperDiv = $('<div class="calculated-setting-row"></div>');
wrapperDiv.append(input).append(datasourceToolbox);
var datasourceTool = $('<li><i class="icon-plus icon-white"></i><label>DATASOURCE</label></li>')
.mousedown(function(e) {
e.preventDefault();
$(input).val("").focus().insertAtCaret("datasources[\"").trigger("freeboard-eval");
});
datasourceToolbox.append(datasourceTool);
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();
});
});
datasourceToolbox.append(jsEditorTool);
if(includeRemove) {
var removeButton = $('<li class="remove-setting-row"><i class="icon-minus icon-white"></i><label></label></li>')
.mousedown(function(e) {
e.preventDefault();
wrapperDiv.remove();
$(valueCell).find('textarea:first').change();
});
datasourceToolbox.prepend(removeButton);
}
$(valueCell).append(wrapperDiv);
}
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, typeaheadSource, typeaheadDataSegment)
{
_.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")
{
if(settingDef.name in currentSettingsValues) {
var currentValue = currentSettingsValues[settingDef.name];
if(settingDef.multi_input && _.isArray(currentValue)) {
var includeRemove = false;
for(var i=0; i<currentValue.length; i++) {
_appendCalculatedSettingRow(valueCell, newSettings, settingDef, currentValue[i], includeRemove);
includeRemove = true;
}
} else {
_appendCalculatedSettingRow(valueCell, newSettings, settingDef, currentValue, false);
}
} else {
_appendCalculatedSettingRow(valueCell, newSettings, settingDef, null, false);
}
if(settingDef.multi_input) {
var inputAdder = $('<ul class="board-toolbar"><li class="add-setting-row"><i class="icon-plus icon-white"></i><label>ADD</label></li></ul>')
.mousedown(function(e) {
e.preventDefault();
_appendCalculatedSettingRow(valueCell, newSettings, settingDef, null, true);
});
$(valueCell).siblings('.form-label').append(inputAdder);
}
}
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]);
}
if(typeaheadSource && settingDef.typeahead_data_field){
input.addClass('typeahead_data_field-' + settingDef.typeahead_data_field);
}
if(typeaheadSource && settingDef.typeahead_field){
var typeaheadValues = [];
input.keyup(function(event){
if(event.which >= 65 && event.which <= 91) {
input.trigger('change');
}
});
$(input).autocomplete({
source: typeaheadValues,
select: function(event, ui){
input.val(ui.item.value);
input.trigger('change');
}
});
input.change(function(event){
var value = input.val();
var source = _.template(typeaheadSource)({input: value});
$.get(source, function(data){
if(typeaheadDataSegment){
data = data[typeaheadDataSegment];
}
data = _.select(data, function(elm){
return elm[settingDef.typeahead_field][0] == value[0];
});
typeaheadValues = _.map(data, function(elm){
return elm[settingDef.typeahead_field];
});
$(input).autocomplete("option", "source", typeaheadValues);
if(data.length == 1){
data = data[0];
//we found the one. let's use it to populate the other info
for(var field in data){
if(data.hasOwnProperty(field)){
var otherInput = $(_.template('input.typeahead_data_field-<%= field %>')({field: field}));
if(otherInput){
otherInput.val(data[field]);
if(otherInput.val() != input.val()) {
otherInput.trigger('change');
}
}
}
}
}
});
});
}
}
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 == "integer" && (newSettings.settings[settingDef.name] % 1 !== 0))
{
_displayValidationError(settingDef.name, "Must be a whole number.");
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, selectedType.typeahead_source, selectedType.typeahead_data_segment);
}
});
}
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 dropdown = null;
var selectedOptionIndex = 0;
var _autocompleteOptions = [];
var currentValue = null;
var EXPECTED_TYPE = {
ANY : "any",
ARRAY : "array",
OBJECT : "object",
STRING : "string",
NUMBER : "number",
BOOLEAN : "boolean"
};
function _isPotentialTypeMatch(value, expectsType)
{
if(_.isArray(value) || _.isObject(value))
{
return true;
}
return _isTypeMatch(value, expectsType);
}
function _isTypeMatch(value, expectsType) {
switch(expectsType)
{
case EXPECTED_TYPE.ANY: return true;
case EXPECTED_TYPE.ARRAY: return _.isArray(value);
case EXPECTED_TYPE.OBJECT: return _.isObject(value);
case EXPECTED_TYPE.STRING: return _.isString(value);
case EXPECTED_TYPE.NUMBER: return _.isNumber(value);
case EXPECTED_TYPE.BOOLEAN: return _.isBoolean(value);
}
}
function _checkCurrentValueType(element, expectsType) {
$(element).parent().find(".validation-error").remove();
if(!_isTypeMatch(currentValue, expectsType)) {
$(element).parent().append("<div class='validation-error'>" +
"This field expects an expression that evaluates to type " +
expectsType + ".</div>");
}
}
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, expectsType)
{
var match = _veDatasourceRegex.exec(inputString);
var options = [];
if(match)
{
// Editor value is: datasources["; List all datasources
if(match[1] == "")
{
_.each(datasources, function(datasource)
{
options.push({value: datasource.name(), entity: undefined,
precede_char: "", follow_char: "\"]"});
});
}
// Editor value is a partial match for a datasource; list matching datasources
else if(match[1] != "" && _.isUndefined(match[2]))
{
var replacementString = match[1];
_.each(datasources, function(datasource)
{
var dsName = datasource.name();
if(dsName != replacementString && dsName.indexOf(replacementString) == 0)
{
options.push({value: dsName, entity: undefined,
precede_char: "", follow_char: "\"]"});
}
});
}
// Editor value matches a datasources; parse JSON in order to populate list
else
{
// We already have a datasource selected; find it