UNPKG

freeboard-leaflet-realtime

Version:
1,927 lines (1,604 loc) 136 kB
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