freeboard/js/freeboard.js

2951 lines
74 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 _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)
{
_.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]);
}
}
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 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
var datasource = _.find(datasources, function(datasource)
{
return (datasource.name() === match[1]);
});
if(!_.isUndefined(datasource))
{
var dataPath = "data";
var remainder = "";
// Parse the partial JSON selectors
if(!_.isUndefined(match[2]))
{
// Strip any incomplete field values, and store the remainder
var remainderIndex = match[3].lastIndexOf("]") + 1;
dataPath = dataPath + match[3].substring(0, remainderIndex);
remainder = match[3].substring(remainderIndex, match[3].length);
remainder = remainder.replace(/^[\[\"]*/, "");
remainder = remainder.replace(/[\"\]]*$/, "");
}
// Get the data for the last complete JSON field
var dataValue = datasource.getDataRepresentation(dataPath);
currentValue = dataValue;
// For arrays, list out the indices
if(_.isArray(dataValue))
{
for(var index = 0; index < dataValue.length; index++)
{
if(index.toString().indexOf(remainder) == 0)
{
var value = dataValue[index];
if(_isPotentialTypeMatch(value, expectsType))
{
options.push({value: index, entity: value,
precede_char: "[", follow_char: "]",
preview: value.toString()});
}
}
}
}
// For objects, list out the keys
else if(_.isObject(dataValue))
{
_.each(dataValue, function(value, name)
{
if(name.indexOf(remainder) == 0)
{
if(_isPotentialTypeMatch(value, expectsType))
{
options.push({value: name, entity: value,
precede_char: "[\"", follow_char: "\"]"});
}
}
});
}
// For everything else, do nothing (no further selection possible)
else
{
// no-op
}
}
}
}
_autocompleteOptions = options;
}
function _renderAutocompleteDropdown(element, expectsType)
{
var inputString = $(element).val().substring(0, $(element).getCaretPosition());
// Weird issue where the textarea box was putting in ASCII (nbsp) for spaces.
inputString = inputString.replace(String.fromCharCode(160), " ");
_autocompleteFromDatasource(inputString, theFreeboardModel.datasources(), expectsType);
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;
_.each(_autocompleteOptions, function(option, index)
{
var li = _renderAutocompleteDropdownOption(element, inputString, option, index);
if(selected)
{
$(li).addClass("selected");
selected = false;
}
});
}
else
{
_checkCurrentValueType(element, expectsType);
$(element).next("ul#value-selector").remove();
dropdown = null;
selectedOptionIndex = -1;
}
}
function _renderAutocompleteDropdownOption(element, inputString, option, currentIndex)
{
var optionLabel = option.value;
if(option.preview)
{
optionLabel = optionLabel + "<span class='preview'>" + option.preview + "</span>";
}
var li = $('<li>' + optionLabel + '</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;
optionValue = option.precede_char + optionValue + option.follow_char;
var replacementIndex = inputString.lastIndexOf("]");
if(replacementIndex != -1)
{
$(element).replaceTextAt(replacementIndex+1, $(element).val().length,
optionValue);
}
else
{
$(element).insertAtCaret(optionValue);
}
currentValue = option.entity;
$(element).triggerHandler("mouseup");
})
.bind("freeboard-select", function()
{
$(this).parent().find("li.selected").removeClass("selected");
$(this).addClass("selected");
selectedOptionIndex = $(this).data("freeboard-optionIndex");
});
return li;
}
function createValueEditor(element, expectsType)
{
$(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;
}
_renderAutocompleteDropdown(element, expectsType);
})
.focus(function()
{
$(element).css({"z-index" : 3001});
_resizeValueEditor(element);
})
.focusout(function()
{
_checkCurrentValueType(element, expectsType);
$(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, expectsType)
{
if(expectsType)
{
createValueEditor(element, expectsType);
}
else {
createValueEditor(element, EXPECTED_TYPE.ANY);
}
},
EXPECTED_TYPE : EXPECTED_TYPE
}
}
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(_.isArray(script)) {
script = "[" + script.join(",") + "]";
}
// 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);