265 lines
9.1 KiB
JavaScript
265 lines
9.1 KiB
JavaScript
/*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.listenTo(this.model.records, 'add reset 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: ' \
|
|
<div class="table-container"> \
|
|
<table class="recline-grid table-striped table-condensed" cellspacing="0"> \
|
|
<thead class="fixed-header"> \
|
|
<tr> \
|
|
{{#fields}} \
|
|
<th class="column-header {{#hidden}}hidden{{/hidden}}" data-field="{{id}}" style="width: {{width}}px; max-width: {{width}}px; min-width: {{width}}px;" title="{{label}}"> \
|
|
<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 = this.fields.map(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 = new recline.Model.FieldList(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);
|
|
this.fields.each(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 = $('<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.listenTo(this.model, 'change', this.render);
|
|
},
|
|
|
|
template: ' \
|
|
{{#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);
|