/*jshint multistr:true */
this.recline = this.recline || {};
this.recline.View = this.recline.View || {};
(function($, my) {
"use strict";
// ## (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;
_.bindAll(this, 'render', 'onHorizontalScroll');
this.model.records.bind('add', this.render);
this.model.records.bind('reset', this.render);
this.model.records.bind('remove', this.render);
this.tempState = {};
var state = _.extend({
hiddenFields: []
}, modelEtc.state
);
this.state = new recline.Model.ObjectState(state);
},
events: {
// does not work here so done at end of render function
// 'scroll .recline-grid tbody': 'onHorizontalScroll'
},
// ======================================================
// Column and row menus
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: ' \
\
',
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), 10);
// 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.records.forEach(function(doc) {
var 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 = $("").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:
//
//
// var row = new GridRow({
// model: dataset-record,
// el: dom-element,
// fields: mydatasets.fields // a FieldList object
// });
//
my.GridRow = Backbone.View.extend({
initialize: function(initData) {
_.bindAll(this, 'render');
this._fields = initData.fields;
this.model.bind('change', this.render);
},
template: ' \
{{#cells}} \
\
\
| \
{{/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: ' \
\
',
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);