372 lines
14 KiB
JavaScript
372 lines
14 KiB
JavaScript
/*jshint multistr:true */
|
|
|
|
this.recline = this.recline || {};
|
|
this.recline.View = this.recline.View || {};
|
|
|
|
(function($, my) {
|
|
// ## (Data) Grid Dataset View
|
|
//
|
|
// Provides a tabular view on a Dataset.
|
|
//
|
|
// Initialize it with a `recline.Model.Dataset`.
|
|
my.Grid = Backbone.View.extend({
|
|
tagName: "div",
|
|
className: "recline-grid-container",
|
|
|
|
initialize: function(modelEtc) {
|
|
var self = this;
|
|
this.el = $(this.el);
|
|
_.bindAll(this, 'render', 'onHorizontalScroll');
|
|
this.model.currentRecords.bind('add', this.render);
|
|
this.model.currentRecords.bind('reset', this.render);
|
|
this.model.currentRecords.bind('remove', this.render);
|
|
this.tempState = {};
|
|
var state = _.extend({
|
|
hiddenFields: []
|
|
}, modelEtc.state
|
|
);
|
|
this.state = new recline.Model.ObjectState(state);
|
|
},
|
|
|
|
events: {
|
|
'click .column-header-menu .data-table-menu li a': 'onColumnHeaderClick',
|
|
'click .row-header-menu': 'onRowHeaderClick',
|
|
'click .root-header-menu': 'onRootHeaderClick',
|
|
'click .data-table-menu li a': 'onMenuClick',
|
|
// does not work here so done at end of render function
|
|
// 'scroll .recline-grid tbody': 'onHorizontalScroll'
|
|
},
|
|
|
|
// ======================================================
|
|
// Column and row menus
|
|
|
|
onColumnHeaderClick: function(e) {
|
|
this.tempState.currentColumn = $(e.target).closest('.column-header').attr('data-field');
|
|
},
|
|
|
|
onRowHeaderClick: function(e) {
|
|
this.tempState.currentRow = $(e.target).parents('tr:first').attr('data-id');
|
|
},
|
|
|
|
onRootHeaderClick: function(e) {
|
|
var tmpl = ' \
|
|
{{#columns}} \
|
|
<li><a data-action="showColumn" data-column="{{.}}" href="JavaScript:void(0);">Show column: {{.}}</a></li> \
|
|
{{/columns}}';
|
|
var tmp = Mustache.render(tmpl, {'columns': this.state.get('hiddenFields')});
|
|
this.el.find('.root-header-menu .dropdown-menu').html(tmp);
|
|
},
|
|
|
|
onMenuClick: function(e) {
|
|
var self = this;
|
|
e.preventDefault();
|
|
var actions = {
|
|
bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.tempState.currentColumn}); },
|
|
facet: function() {
|
|
self.model.queryState.addFacet(self.tempState.currentColumn);
|
|
},
|
|
facet_histogram: function() {
|
|
self.model.queryState.addHistogramFacet(self.tempState.currentColumn);
|
|
},
|
|
filter: function() {
|
|
self.model.queryState.addTermFilter(self.tempState.currentColumn, '');
|
|
},
|
|
sortAsc: function() { self.setColumnSort('asc'); },
|
|
sortDesc: function() { self.setColumnSort('desc'); },
|
|
hideColumn: function() { self.hideColumn(); },
|
|
showColumn: function() { self.showColumn(e); },
|
|
deleteRow: function() {
|
|
var self = this;
|
|
var doc = _.find(self.model.currentRecords.models, function(doc) {
|
|
// important this is == as the currentRow will be string (as comes
|
|
// from DOM) while id may be int
|
|
return doc.id == self.tempState.currentRow;
|
|
});
|
|
doc.destroy().then(function() {
|
|
self.model.currentRecords.remove(doc);
|
|
self.trigger('recline:flash', {message: "Row deleted successfully"});
|
|
}).fail(function(err) {
|
|
self.trigger('recline:flash', {message: "Errorz! " + err});
|
|
});
|
|
}
|
|
};
|
|
actions[$(e.target).attr('data-action')]();
|
|
},
|
|
|
|
showTransformColumnDialog: function() {
|
|
var self = this;
|
|
var view = new my.ColumnTransform({
|
|
model: this.model
|
|
});
|
|
// pass the flash message up the chain
|
|
view.bind('recline:flash', function(flash) {
|
|
self.trigger('recline:flash', flash);
|
|
});
|
|
view.state = this.tempState;
|
|
view.render();
|
|
this.el.append(view.el);
|
|
view.el.modal();
|
|
},
|
|
|
|
setColumnSort: function(order) {
|
|
var sort = [{}];
|
|
sort[0][this.tempState.currentColumn] = {order: order};
|
|
this.model.query({sort: sort});
|
|
},
|
|
|
|
hideColumn: function() {
|
|
var hiddenFields = this.state.get('hiddenFields');
|
|
hiddenFields.push(this.tempState.currentColumn);
|
|
this.state.set({hiddenFields: hiddenFields});
|
|
// change event not being triggered (because it is an array?) so trigger manually
|
|
this.state.trigger('change');
|
|
this.render();
|
|
},
|
|
|
|
showColumn: function(e) {
|
|
var hiddenFields = _.without(this.state.get('hiddenFields'), $(e.target).data('column'));
|
|
this.state.set({hiddenFields: hiddenFields});
|
|
this.render();
|
|
},
|
|
|
|
onHorizontalScroll: function(e) {
|
|
var currentScroll = $(e.target).scrollLeft();
|
|
this.el.find('.recline-grid thead tr').scrollLeft(currentScroll);
|
|
},
|
|
|
|
// ======================================================
|
|
// #### Templating
|
|
template: ' \
|
|
<div class="table-container"> \
|
|
<table class="recline-grid table-striped table-condensed" cellspacing="0"> \
|
|
<thead class="fixed-header"> \
|
|
<tr> \
|
|
{{#notEmpty}} \
|
|
<th class="column-header"> \
|
|
<div class="btn-group root-header-menu"> \
|
|
<a class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a> \
|
|
<ul class="dropdown-menu data-table-menu"> \
|
|
</ul> \
|
|
</div> \
|
|
<span class="column-header-name"></span> \
|
|
</th> \
|
|
{{/notEmpty}} \
|
|
{{#fields}} \
|
|
<th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \
|
|
<div class="btn-group column-header-menu"> \
|
|
<a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
|
|
<ul class="dropdown-menu data-table-menu pull-right"> \
|
|
<li><a data-action="facet" href="JavaScript:void(0);">Term Facet</a></li> \
|
|
<li><a data-action="facet_histogram" href="JavaScript:void(0);">Date Histogram Facet</a></li> \
|
|
<li><a data-action="filter" href="JavaScript:void(0);">Text Filter</a></li> \
|
|
<li class="divider"></li> \
|
|
<li><a data-action="sortAsc" href="JavaScript:void(0);">Sort ascending</a></li> \
|
|
<li><a data-action="sortDesc" href="JavaScript:void(0);">Sort descending</a></li> \
|
|
<li class="divider"></li> \
|
|
<li><a data-action="hideColumn" href="JavaScript:void(0);">Hide this column</a></li> \
|
|
<li class="divider"></li> \
|
|
<li class="write-op"><a data-action="bulkEdit" href="JavaScript:void(0);">Transform...</a></li> \
|
|
</ul> \
|
|
</div> \
|
|
<span class="column-header-name">{{label}}</span> \
|
|
</th> \
|
|
{{/fields}} \
|
|
<th class="last-header" style="width: {{lastHeaderWidth}}px; max-width: {{lastHeaderWidth}}px; min-width: {{lastHeaderWidth}}px; padding: 0; margin: 0;"></th> \
|
|
</tr> \
|
|
</thead> \
|
|
<tbody class="scroll-content"></tbody> \
|
|
</table> \
|
|
</div> \
|
|
',
|
|
|
|
toTemplateJSON: function() {
|
|
var self = this;
|
|
var modelData = this.model.toJSON();
|
|
modelData.notEmpty = ( this.fields.length > 0 );
|
|
// TODO: move this sort of thing into a toTemplateJSON method on Dataset?
|
|
modelData.fields = _.map(this.fields, function(field) {
|
|
return field.toJSON();
|
|
});
|
|
// last header width = scroll bar - border (2px) */
|
|
modelData.lastHeaderWidth = this.scrollbarDimensions.width - 2;
|
|
return modelData;
|
|
},
|
|
render: function() {
|
|
var self = this;
|
|
this.fields = this.model.fields.filter(function(field) {
|
|
return _.indexOf(self.state.get('hiddenFields'), field.id) == -1;
|
|
});
|
|
this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions
|
|
var numFields = this.fields.length;
|
|
// compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar)
|
|
var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width;
|
|
var width = parseInt(Math.max(50, fullWidth / numFields));
|
|
// if columns extend outside viewport then remainder is 0
|
|
var remainder = Math.max(fullWidth - numFields * width,0);
|
|
_.each(this.fields, function(field, idx) {
|
|
// add the remainder to the first field width so we make up full col
|
|
if (idx == 0) {
|
|
field.set({width: width+remainder});
|
|
} else {
|
|
field.set({width: width});
|
|
}
|
|
});
|
|
var htmls = Mustache.render(this.template, this.toTemplateJSON());
|
|
this.el.html(htmls);
|
|
this.model.currentRecords.forEach(function(doc) {
|
|
var tr = $('<tr />');
|
|
self.el.find('tbody').append(tr);
|
|
var newView = new my.GridRow({
|
|
model: doc,
|
|
el: tr,
|
|
fields: self.fields
|
|
});
|
|
newView.render();
|
|
});
|
|
// hide extra header col if no scrollbar to avoid unsightly overhang
|
|
var $tbody = this.el.find('tbody')[0];
|
|
if ($tbody.scrollHeight <= $tbody.offsetHeight) {
|
|
this.el.find('th.last-header').hide();
|
|
}
|
|
this.el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0));
|
|
this.el.find('.recline-grid tbody').scroll(this.onHorizontalScroll);
|
|
return this;
|
|
},
|
|
|
|
// ### _scrollbarSize
|
|
//
|
|
// Measure width of a vertical scrollbar and height of a horizontal scrollbar.
|
|
//
|
|
// @return: { width: pixelWidth, height: pixelHeight }
|
|
_scrollbarSize: function() {
|
|
var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
|
|
var dim = { width: $c.width() - $c[0].clientWidth + 1, height: $c.height() - $c[0].clientHeight };
|
|
$c.remove();
|
|
return dim;
|
|
}
|
|
});
|
|
|
|
// ## GridRow View for rendering an individual record.
|
|
//
|
|
// 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 FieldList in the constructor options. This should be list of fields for the Grid.
|
|
//
|
|
// Example:
|
|
//
|
|
// <pre>
|
|
// var row = new GridRow({
|
|
// model: dataset-record,
|
|
// el: dom-element,
|
|
// fields: mydatasets.fields // a FieldList object
|
|
// });
|
|
// </pre>
|
|
my.GridRow = Backbone.View.extend({
|
|
initialize: function(initData) {
|
|
_.bindAll(this, 'render');
|
|
this._fields = initData.fields;
|
|
this.el = $(this.el);
|
|
this.model.bind('change', this.render);
|
|
},
|
|
|
|
template: ' \
|
|
<td> \
|
|
<div class="btn-group row-header-menu"> \
|
|
<a class="btn dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a> \
|
|
<ul class="dropdown-menu data-table-menu"> \
|
|
<li class="write-op"><a data-action="deleteRow" href="JavaScript:void(0);">Delete this row</a></li> \
|
|
</ul> \
|
|
</div> \
|
|
</td> \
|
|
{{#cells}} \
|
|
<td data-field="{{field}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;"> \
|
|
<div class="data-table-cell-content"> \
|
|
<a href="javascript:{}" class="data-table-cell-edit" title="Edit this cell"> </a> \
|
|
<div class="data-table-cell-value">{{{value}}}</div> \
|
|
</div> \
|
|
</td> \
|
|
{{/cells}} \
|
|
',
|
|
events: {
|
|
'click .data-table-cell-edit': 'onEditClick',
|
|
'click .data-table-cell-editor .okButton': 'onEditorOK',
|
|
'click .data-table-cell-editor .cancelButton': 'onEditorCancel'
|
|
},
|
|
|
|
toTemplateJSON: function() {
|
|
var self = this;
|
|
var doc = this.model;
|
|
var cellData = this._fields.map(function(field) {
|
|
return {
|
|
field: field.id,
|
|
width: field.get('width'),
|
|
value: doc.getFieldValue(field)
|
|
};
|
|
});
|
|
return { id: this.id, cells: cellData };
|
|
},
|
|
|
|
render: function() {
|
|
this.el.attr('data-id', this.model.id);
|
|
var html = Mustache.render(this.template, this.toTemplateJSON());
|
|
$(this.el).html(html);
|
|
return this;
|
|
},
|
|
|
|
// ===================
|
|
// Cell Editor methods
|
|
|
|
cellEditorTemplate: ' \
|
|
<div class="menu-container data-table-cell-editor"> \
|
|
<textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
|
|
<div id="data-table-cell-editor-actions"> \
|
|
<div class="data-table-cell-editor-action"> \
|
|
<button class="okButton btn primary">Update</button> \
|
|
<button class="cancelButton btn danger">Cancel</button> \
|
|
</div> \
|
|
</div> \
|
|
</div> \
|
|
',
|
|
|
|
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());
|
|
var templated = Mustache.render(this.cellEditorTemplate, {value: cell.text()});
|
|
cell.html(templated);
|
|
},
|
|
|
|
onEditorOK: function(e) {
|
|
var self = this;
|
|
var cell = $(e.target);
|
|
var rowId = cell.parents('tr').attr('data-id');
|
|
var field = cell.parents('td').attr('data-field');
|
|
var newValue = cell.parents('.data-table-cell-editor').find('.data-table-cell-editor-editor').val();
|
|
var newData = {};
|
|
newData[field] = newValue;
|
|
this.model.set(newData);
|
|
this.trigger('recline:flash', {message: "Updating row...", loader: true});
|
|
this.model.save().then(function(response) {
|
|
this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'});
|
|
})
|
|
.fail(function() {
|
|
this.trigger('recline:flash', {
|
|
message: 'Error saving row',
|
|
category: 'error',
|
|
persist: true
|
|
});
|
|
});
|
|
},
|
|
|
|
onEditorCancel: function(e) {
|
|
var cell = $(e.target).parents('.data-table-cell-value');
|
|
cell.html(cell.data('previousContents')).siblings('.data-table-cell-edit').removeClass("hidden");
|
|
}
|
|
});
|
|
|
|
})(jQuery, recline.View);
|