this.recline = this.recline || {}; // Views module following classic module pattern recline.View = function($) { var my = {}; my.DataExplorer = Backbone.View.extend({ tagName: 'div', className: 'data-explorer', template: ' \ \
\ ', events: { 'change input[name="nav-toggle"]': 'navChange', 'submit form.display-count': 'displayCountUpdate' }, initialize: function(options) { this.el = $(this.el); this.config = options.config || {}; _.extend(this.config, { displayCount: 10 }); this.draw(); }, displayCountUpdate: function(e) { e.preventDefault(); this.config.displayCount = parseInt(this.el.find('input[name="displayCount"]').val()); this.draw(); }, draw: function() { var self = this; this.el.empty(); this.render(); this.$dataViewContainer = this.el.find('.data-view-container'); // retrieve basic data like headers etc // note this.model and dataset returned are the same this.model.fetch().then(function(dataset) { // initialize of dataTable calls render self.dataTable = new my.DataTable({ model: dataset }); self.flotGraph = new my.FlotGraph({ model: dataset }); self.flotGraph.el.hide(); self.$dataViewContainer.append(self.dataTable.el) self.$dataViewContainer.append(self.flotGraph.el); self.model.getRows(self.config.displayCount); }); }, render: function() { var template = $.mustache(this.template, this.config); $(this.el).html(template); }, navChange: function(e) { // TODO: really ugly and will not scale to more widgets ... var widgetToShow = $(e.target).val(); if (widgetToShow == 'datatable') { this.flotGraph.el.hide(); this.dataTable.el.show(); } else if (widgetToShow == 'graph') { this.flotGraph.el.show(); this.dataTable.el.hide(); // Have to call this here // If you attempt to render with flot when graph is hidden / invisible flot will complain with // Invalid dimensions for plot, width = 0, height = 0 // (Could hack this by moving plot left -1000 or similar ...) this.flotGraph.createPlot(); } } }); // DataTable provides a tabular view on a Dataset. // // Initialize it with a recline.Dataset object. my.DataTable = Backbone.View.extend({ tagName: "div", className: "data-table-container", initialize: function() { var self = this; this.el = $(this.el); _.bindAll(this, 'render'); this.model.currentDocuments.bind('add', this.render); this.model.currentDocuments.bind('reset', this.render); this.state = {}; // this is nasty. Due to fact that .menu element is not inside this view but is elsewhere in DOM $('.menu li a').live('click', function(e) { // self.onMenuClick(e).apply(self); self.onMenuClick(e); }); }, events: { // see initialize // 'click .menu li': 'onMenuClick', 'click .column-header-menu': 'onColumnHeaderClick', 'click .row-header-menu': 'onRowHeaderClick', 'click .data-table-cell-edit': 'onEditClick', // cell editor 'click .data-table-cell-editor .okButton': 'onEditorOK', 'click .data-table-cell-editor .cancelButton': 'onEditorCancel' }, showDialog: function(template, data) { if (!data) data = {}; util.show('dialog'); util.render(template, 'dialog-content', data); util.observeExit($('.dialog-content'), function() { util.hide('dialog'); }) $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); }, // ====================================================== // Column and row menus onColumnHeaderClick: function(e) { this.state.currentColumn = $(e.target).siblings().text(); util.position('menu', e); util.render('columnActions', 'menu'); }, onRowHeaderClick: function(e) { this.state.currentRow = $(e.target).parents('tr:first').attr('data-id'); util.position('menu', e); util.render('rowActions', 'menu'); }, onMenuClick: function(e) { var self = this; e.preventDefault(); var actions = { bulkEdit: function() { self.showDialog('bulkEdit', {name: self.state.currentColumn}) }, transform: function() { showDialog('transform') }, csv: function() { window.location.href = app.csvUrl }, json: function() { window.location.href = "_rewrite/api/json" }, urlImport: function() { showDialog('urlImport') }, pasteImport: function() { showDialog('pasteImport') }, uploadImport: function() { showDialog('uploadImport') }, deleteColumn: function() { var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents."; // TODO: alert('This function needs to be re-implemented'); return; if (confirm(msg)) costco.deleteColumn(self.state.currentColumn); }, deleteRow: function() { // TODO: alert('This function needs to be re-implemented'); return; var doc = _.find(app.cache, function(doc) { return doc._id === app.currentRow }); doc._deleted = true; costco.uploadDocs([doc]).then( function(updatedDocs) { util.notify("Row deleted successfully"); recline.initializeTable(app.offset); }, function(err) { util.notify("Errorz! " + err) } ) } } util.hide('menu'); actions[$(e.target).attr('data-action')](); }, // ====================================================== // Cell Editor onEditClick: function(e) { var editing = this.el.find('.data-table-cell-editor-editor'); if (editing.length > 0) { editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); } $(e.target).addClass("hidden"); var cell = $(e.target).siblings('.data-table-cell-value'); cell.data("previousContents", cell.text()); util.render('cellEditor', cell, {value: cell.text()}); }, onEditorOK: function(e) { var cell = $(e.target); var rowId = cell.parents('tr').attr('data-id'); var header = cell.parents('td').attr('data-header'); var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor)').val(); // TODO: alert('Update: ' + header + ' with value ' + newValue + '(But no save as not yet operational'); return; var doc = _.find(app.cache, function(cacheDoc) { return cacheDoc._id === rowId; }); doc[header] = newValue; util.notify("Updating row...", {persist: true, loader: true}); costco.updateDoc(doc).then(function(response) { util.notify("Row updated successfully"); recline.initializeTable(); }) }, onEditorCancel: function(e) { var cell = $(e.target).parents('.data-table-cell-value'); cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden"); }, // ====================================================== // Core Templating template: ' \ \ \ \ {{#notEmpty}}{{/notEmpty}} \ {{#headers}} \ \ {{/headers}} \ \ \ \
\
\ \ {{.}} \
\ \
\ ', toTemplateJSON: function() { var modelData = this.model.toJSON() modelData.notEmpty = ( modelData.headers.length > 0 ) return modelData; }, render: function() { var self = this; var template = $( ".dataTableTemplate:first" ).html() , htmls = $.mustache(template, this.toTemplateJSON()) ; this.el.html(htmls); this.model.currentDocuments.forEach(function(doc) { var tr = $(''); self.el.find('tbody').append(tr); var newView = new my.DataTableRow({ model: doc, el: tr, headers: self.model.get('headers') }); newView.render(); }); return this; } }); // DataTableRow View for rendering an individual document. // // Since we want this to update in place it is up to creator to provider the element to attach to. // In addition you must pass in a headers in the constructor options. This should be list of headers for the DataTable. my.DataTableRow = Backbone.View.extend({ initialize: function(options) { this._headers = options.headers; this.el = $(this.el); this.model.bind('change', this.render); }, template: ' \ \ {{#cells}} \ \
\   \
{{value}}
\
\ \ {{/cells}} \ ', toTemplateJSON: function() { var doc = this.model; var cellData = _.map(this._headers, function(header) { return {header: header, value: doc.get(header)} }) return { id: this.id, cells: cellData } }, render: function() { this.el.attr('data-id', this.model.id); var html = $.mustache(this.template, this.toTemplateJSON()); $(this.el).html(html); return this; } }); my.FlotGraph = Backbone.View.extend({ tagName: "div", className: "data-graph-container", // TODO: normalize css template: ' \
\
\
\

Help

\

To create a chart select a column (group) to use as the x-axis \ then another column (Series A) to plot against it.

\

You can add add \ additional series by clicking the "Add series" button

\

Please note you must be logged in to save charts.

\
\
\ \
\ \
\
\ \ \
\
\
\ \ ', initialize: function(options, chart) { var self = this; this.el = $(this.el); _.bindAll(this, 'render'); this.model.currentDocuments.bind('add', this.render); this.model.currentDocuments.bind('reset', this.render); this.chart = chart; this.chartConfig = { group: null, series: [], graphType: 'line' }; }, events: { // 'change select': 'onEditorSubmit' }, onEditorSubmit: function(e) { var select = this.el.find('.editor-group select'); this._getEditorData(); this.plot.setData(this.createSeries()); this.plot.resize(); this.plot.setupGrid(); this.plot.draw(); }, _getEditorData: function() { $editor = this var series = this.$series.map(function () { return $(this).val(); }); this.chartConfig.series = $.makeArray(series) this.chartConfig.group = this.el.find('.editor-group select').val(); }, toTemplateJSON: function() { return this.model.toJSON(); }, render: function() { htmls = $.mustache(this.template, this.toTemplateJSON()); $(this.el).html(htmls); // now set a load of stuff up this.$graph = this.el.find('.panel.graph'); // event approach did not seem to work this.$series = this.el.find('.editor-series select'); this.$seriesClone = this.$series.parent().clone(); var self = this; this.el.find('form select').change(function() { self.onEditorSubmit.apply(self, arguments) }); return this; }, createPlot: function () { // only lines for the present options = { id: 'line', name: 'Line Chart' }; this.plot = $.plot(this.$graph, this.createSeries(), options); return this; }, createSeries: function () { var self = this; var series = []; if (this.chartConfig) { $.each(this.chartConfig.series, function (seriesIndex, field) { var points = []; $.each(self.model.currentDocuments.models, function (index, doc) { var x = doc.get(self.chartConfig.group); var y = doc.get(field); if (typeof x === 'string') { x = index; } points.push([x, y]); }); series.push({data: points, label: field}); }); } return series; }, // TODO: finish porting this function addSeries: function () { var element = this.seriesClone.clone(), label = element.find('label'), index = this.series.length; this.el.$series.parent().find('ul').append(element); this.updateSeries(); label.append('Remove'); label.find('span').text(String.fromCharCode(this.series.length + 64)); return this; } }); return my; }(jQuery);