502 lines
16 KiB
JavaScript
502 lines
16 KiB
JavaScript
/*
|
|
backgrid-sizeable-columns
|
|
https://github.com/FortesSolutions/backgrid-sizeable-columns
|
|
|
|
Copyright (c) 2016 Fortes Solutions
|
|
Licensed under the MIT @license.
|
|
*/
|
|
(function (root, factory) {
|
|
// CommonJS
|
|
if (typeof exports == "object") {
|
|
module.exports = factory(require("underscore"), require("backgrid"));
|
|
}
|
|
// AMD. Register as an anonymous module.
|
|
else if (typeof define === 'function' && define.amd) {
|
|
define(['underscore', 'backgrid'], factory);
|
|
}
|
|
// Browser
|
|
else {
|
|
factory(root._, root.Backgrid);
|
|
}
|
|
|
|
}(this, function (_, Backgrid) {
|
|
"use strict";
|
|
|
|
// Adds width support to columns
|
|
Backgrid.Extension.SizeAbleColumns = Backbone.View.extend({
|
|
/** @property */
|
|
tagName: "colgroup",
|
|
|
|
/**
|
|
* Initializer
|
|
* @param options
|
|
*/
|
|
initialize: function (options) {
|
|
this.grid = options.grid;
|
|
|
|
// Attach event listeners once on render
|
|
this.listenTo(this.grid.header, "backgrid:header:rendered", this.render);
|
|
this.listenTo(this.grid.columns, "width:auto", this.setWidthAuto);
|
|
this.listenTo(this.grid.columns, "width:fixed", this.setWidthFixed);
|
|
this.listenTo(this.grid, "backgrid:refresh", this.setColToActualWidth);
|
|
this.listenTo(this.grid.collection, "add remove reset", this.setColToActualWidth);
|
|
},
|
|
|
|
/**
|
|
* Adds sizeable columns using <col> elements in a <colgroup>
|
|
* @returns {Backgrid.Extension.SizeAbleColumns}
|
|
*/
|
|
render: function () {
|
|
var view = this;
|
|
view.$el.empty();
|
|
|
|
view.grid.columns.each(function (col) {
|
|
if (typeof col.get("renderable") == "undefined" || col.get("renderable")) {
|
|
var $colEl = $("<col>").appendTo(view.$el).attr("data-column-cid", col.cid);
|
|
var colWidth = col.get("width");
|
|
var colMinWidth = col.get("minWidth");
|
|
var colMaxWidth = col.get("maxWidth");
|
|
if (colWidth && colWidth != "*") {
|
|
if (colMinWidth && colWidth < colMinWidth) {
|
|
colWidth = colMinWidth;
|
|
}
|
|
if (colMaxWidth && colWidth > colMaxWidth) {
|
|
colWidth = colMaxWidth;
|
|
}
|
|
$colEl.width(colWidth);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Add data attribute to column cells
|
|
if (view.grid.header.headerRows) {
|
|
_.each(view.grid.header.headerRows, function(row) {
|
|
_.each(row.cells, function(cell) {
|
|
cell.$el.attr("data-column-cid", cell.column.cid);
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
_.each(view.grid.header.row.cells, function(cell) {
|
|
cell.$el.attr("data-column-cid", cell.column.cid);
|
|
});
|
|
}
|
|
|
|
// Trigger event
|
|
view.grid.collection.trigger("backgrid:colgroup:changed");
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Gets a <col> element belonging to given model
|
|
* @param colModel Backgrid.Column
|
|
* @returns {*|JQuery|any|jQuery}
|
|
* @private
|
|
*/
|
|
getColumnElement: function (colModel) {
|
|
return this.$el.find('col[data-column-cid="' + colModel.cid + '"]');
|
|
},
|
|
|
|
/**
|
|
* Get the column width of given model
|
|
* @param colModel Backgrid.Column
|
|
* @returns {Integer}
|
|
* @private
|
|
*/
|
|
getHeaderElementWidth: function(colModel) {
|
|
return this.grid.header.$el.find("th[data-column-cid='" + colModel.cid + "']").outerWidth();
|
|
},
|
|
|
|
/**
|
|
* Sets a width of the given column to "*" (auto)
|
|
* @param colModel Backgrid.Column
|
|
* @private
|
|
*/
|
|
setWidthAuto: function (colModel) {
|
|
// Get column element
|
|
var $colElement = this.getColumnElement(colModel);
|
|
|
|
// Save width
|
|
colModel.set("width", "*");
|
|
|
|
// Set column width to auto
|
|
$colElement.css("width", "");
|
|
|
|
this.grid.collection.trigger("backgrid:colgroup:updated");
|
|
},
|
|
|
|
/**
|
|
* Sets a width of the given column to a fixed width defined in the model.
|
|
* @param colModel Backgrid.Column
|
|
* @private
|
|
*/
|
|
setWidthFixed: function (colModel) {
|
|
// Get column element
|
|
var $colElement = this.getColumnElement(colModel);
|
|
|
|
// Get width of header element
|
|
var width = this.getHeaderElementWidth(colModel);
|
|
|
|
// Set column width to the original width
|
|
$colElement.css("width", width);
|
|
|
|
// Save width
|
|
colModel.set("width", width);
|
|
|
|
this.grid.collection.trigger("backgrid:colgroup:updated");
|
|
},
|
|
|
|
/**
|
|
* Updates the view's <col> elements to current width
|
|
* @private
|
|
*/
|
|
setColToActualWidth: function() {
|
|
var view = this;
|
|
var changed = false;
|
|
_.each(view.grid.header.row.cells, function(cell) {
|
|
var $colEl = view.getColumnElement(cell.column);
|
|
if (cell.column.get("width") !== "*") {
|
|
changed = changed || $colEl.width() == cell.$el.outerWidth();
|
|
$colEl.width(cell.$el.outerWidth());
|
|
}
|
|
});
|
|
|
|
if (changed) {
|
|
view.grid.collection.trigger("backgrid:colgroup:updated");
|
|
}
|
|
}
|
|
});
|
|
|
|
// Makes column resizable; requires Backgrid.Extension.sizeAbleColumns
|
|
Backgrid.Extension.SizeAbleColumnsHandlers = Backbone.View.extend({
|
|
minWidthDynamicColumns: 80,
|
|
|
|
/**
|
|
* Initializer
|
|
* @param options
|
|
*/
|
|
initialize: function (options) {
|
|
this.sizeAbleColumns = options.sizeAbleColumns;
|
|
this.grid = this.sizeAbleColumns.grid;
|
|
this.columns = this.grid.columns;
|
|
this.header = this.grid.header;
|
|
|
|
this.saveColumnWidth = options.saveColumnWidth;
|
|
if (options.minWidthDynamicColumns != null) {
|
|
this.minWidthDynamicColumns = options.minWidthDynamicColumns;
|
|
}
|
|
|
|
this.setHeaderElements();
|
|
this.attachEvents();
|
|
|
|
this.checkSpacerColumn();
|
|
},
|
|
|
|
/**
|
|
* Adds handlers to resize the columns
|
|
* @returns {Backgrid.Extension.SizeAbleColumnsHandlers}
|
|
*/
|
|
render: function () {
|
|
var view = this;
|
|
view.$el.empty();
|
|
|
|
// For now, loop tds in first row
|
|
_.each(view.headerElements, function (columnEl, index) {
|
|
// Get matching col element
|
|
var $column = $(columnEl);
|
|
var columnModelCid = $column.data("column-cid");
|
|
var $col = view.sizeAbleColumns.$el.find("col[data-column-cid=" + columnModelCid + "]");
|
|
var columnModel = view.columns.get({ cid: columnModelCid});
|
|
|
|
if (columnModel && columnModel.get("resizeable")) {
|
|
// Create helper elements
|
|
var $resizeHandler = $("<div></div>")
|
|
.addClass("resizeHandler")
|
|
.attr("data-column-index", index)
|
|
.appendTo(view.$el);
|
|
var $resizeHandlerHelper = $("<div></div>")
|
|
.hide()
|
|
.addClass("grid-draggable-cursor")
|
|
.appendTo($resizeHandler);
|
|
|
|
// Make draggable
|
|
$resizeHandler.on("mousedown", function (e) {
|
|
view._stopEvent(e);
|
|
var startX = Math.round($resizeHandler.offset().left);
|
|
var $doc = $(document);
|
|
var handlerNonDragSize = $resizeHandler.outerWidth();
|
|
|
|
// Set class
|
|
$resizeHandler.addClass("grid-draggable");
|
|
$resizeHandlerHelper.show();
|
|
|
|
// Follow the mouse
|
|
var mouseMoveHandler = function (evt) {
|
|
view._stopEvent(evt);
|
|
|
|
// Check for constraints
|
|
var minWidth = columnModel.get("minWidth");
|
|
if (!minWidth || minWidth < 20) {
|
|
minWidth = 20;
|
|
}
|
|
var maxWidth = columnModel.get("maxWidth");
|
|
var newLeftPos = evt.pageX;
|
|
var currentWidth = columnModel.get("width");
|
|
var newWidth = currentWidth + (newLeftPos - startX) - handlerNonDragSize / 2;
|
|
|
|
if (minWidth && newWidth <= minWidth) {
|
|
newLeftPos = startX - (currentWidth - minWidth) + handlerNonDragSize / 2;
|
|
}
|
|
if (maxWidth && newWidth >= maxWidth) {
|
|
newLeftPos = startX + maxWidth - currentWidth + handlerNonDragSize / 2;
|
|
}
|
|
|
|
// Apply mouse change to handler
|
|
$resizeHandler.offset({
|
|
left: newLeftPos
|
|
});
|
|
};
|
|
$doc.on("mousemove", mouseMoveHandler);
|
|
|
|
// Add handler to listen for mouseup
|
|
var mouseUpHandler = function (evt) {
|
|
// Cleanup
|
|
view._stopEvent(evt);
|
|
$resizeHandler.removeClass("grid-draggable");
|
|
$resizeHandlerHelper.hide();
|
|
$doc.off("mouseup", mouseUpHandler);
|
|
$doc.off("mousemove", mouseMoveHandler);
|
|
|
|
// Adjust column size
|
|
var stopX = Math.round($resizeHandler.offset().left);
|
|
var offset = (startX - stopX);
|
|
var oldWidth = $column.outerWidth();
|
|
var newWidth = oldWidth - offset;
|
|
|
|
// Save width and trigger events
|
|
if (newWidth != oldWidth) {
|
|
view._disableAutoWithColumns();
|
|
$col.width(newWidth);
|
|
|
|
if (view.saveColumnWidth) {
|
|
// Save updated width
|
|
columnModel.set("width", newWidth, {silent: true});
|
|
}
|
|
|
|
// Trigger event
|
|
columnModel.trigger("resize", columnModel, newWidth, oldWidth, offset);
|
|
}
|
|
view.updateHandlerPosition();
|
|
};
|
|
$doc.on("mouseup", mouseUpHandler);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Position drag handlers
|
|
view.updateHandlerPosition();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Disable automatic width for all columns and set the current width as the new width
|
|
* @returns {undefined}
|
|
*/
|
|
_disableAutoWithColumns: function () {
|
|
// Check if we have an autosize column, if so, trigger resize on it as well
|
|
var autoWidthColumns = this.columns.filter(function (column) {
|
|
return column.get('width') == '*' && column.get('name') != '__spacerColumn';
|
|
});
|
|
|
|
for (var i = 0; i < autoWidthColumns.length; i++) {
|
|
var autoWidthColumn = autoWidthColumns[i];
|
|
|
|
// find the corresponding header element to determine width and cid from
|
|
var $headerElement;
|
|
for (var j = 0; j < this.headerElements.length; j++) {
|
|
$headerElement = $(this.headerElements[j]);
|
|
if ($headerElement.hasClass(autoWidthColumn.get('name'))) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
var outerWidth = $headerElement.outerWidth();
|
|
var columnModelCid = $headerElement.data("column-cid");
|
|
this.sizeAbleColumns.$el.find("col[data-column-cid=" + columnModelCid + "]").width(outerWidth);
|
|
|
|
var column = this.columns.get({ cid: columnModelCid});
|
|
if (this.saveColumnWidth) {
|
|
column.set('width', outerWidth, {silent: true});
|
|
}
|
|
column.trigger('resize', column, outerWidth);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Helper function to prevent event propagation
|
|
* @param e {Event}
|
|
* @private
|
|
*/
|
|
_stopEvent: function (e) {
|
|
if (e.stopPropagation) {
|
|
e.stopPropagation();
|
|
}
|
|
if (e.preventDefault) {
|
|
e.preventDefault();
|
|
}
|
|
e.cancelBubble = true;
|
|
e.returnValue = false;
|
|
},
|
|
|
|
/**
|
|
* Add listeners
|
|
* @private
|
|
*/
|
|
attachEvents: function () {
|
|
var view = this;
|
|
view.listenTo(view.columns, "change:resizeable", view.render);
|
|
view.listenTo(view.columns, "resize width:auto width:fixed add remove", view.checkSpacerColumn);
|
|
view.listenTo(view.grid.collection, "backgrid:colgroup:updated", view.updateHandlerPosition);
|
|
view.listenTo(view.grid.collection, "backgrid:colgroup:changed", function () {
|
|
// Wait for callstack to be cleared
|
|
_.defer(function () {
|
|
view.setHeaderElements();
|
|
view.render();
|
|
});
|
|
});
|
|
|
|
this.resizeEvtHandler = _.debounce(_.bind(view.updateHandlerPosition, view), 250);
|
|
$(window).on("resize", this.resizeEvtHandler);
|
|
},
|
|
|
|
/**
|
|
* Checks whether a spacer column is nessecary. This is the case when widths are set on all columns and it's smaller
|
|
* that the grid element width.
|
|
* @private
|
|
*/
|
|
checkSpacerColumn: function () {
|
|
var view = this;
|
|
var spacerColumn = _.first(view.columns.where({name: "__spacerColumn"}));
|
|
var autoColumns = view.columns.filter(function (col) {
|
|
return col.get("width") == "*" && col.get("name") != "__spacerColumn";
|
|
});
|
|
|
|
// Check if there is a column with auto width, if so, no need to do anything
|
|
if (_.isEmpty(autoColumns)) {
|
|
var totalWidth = view.columns.reduce(function (memo, num) {
|
|
// count 0 pixels for the spacer column
|
|
var colWidth = (num.get("width") == "*") ? 0 : num.get("width");
|
|
return memo + colWidth;
|
|
}, 0);
|
|
var gridWidth = view.grid.$el.width();
|
|
|
|
if (gridWidth > totalWidth) {
|
|
// The grid is larger than the cumulative column width, we need a spacer column
|
|
if (!spacerColumn) {
|
|
// Create new column model
|
|
view.columns.add(view.getSpacerColumn(), {silent: true});
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (spacerColumn) {
|
|
view.columns.remove(spacerColumn, {silent: true});
|
|
}
|
|
|
|
var columnsWidth = this.columns.reduce(function (memo, column) {
|
|
return memo + (column.get('width') == '*' ? this.minWidthDynamicColumns : column.get('width'));
|
|
}, 0, this);
|
|
// min width on columns does not work, therefore place the min width on the table itself
|
|
this.grid.$el.css('min-width', columnsWidth + 'px');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a spacer column definition
|
|
* @returns Object
|
|
* @private
|
|
*/
|
|
getSpacerColumn: function() {
|
|
return Backgrid.Extension.SizeAbleColumns.spacerColumnDefinition;
|
|
},
|
|
|
|
/**
|
|
* Updates the position of the handlers
|
|
* @private
|
|
*/
|
|
updateHandlerPosition: function () {
|
|
var view = this;
|
|
_.each(view.headerElements, function (columnEl, index) {
|
|
var $column = $(columnEl);
|
|
|
|
// Get handler for current column and update position
|
|
view.$el.children().filter("[data-column-index='" + index + "']")
|
|
.css("left", $column.position().left + $column.outerWidth());
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Find the current header elements and stores them
|
|
*/
|
|
setHeaderElements: function () {
|
|
var self = this;
|
|
var rows = self.grid.header.headerRows || [self.grid.header.row];
|
|
self.headerCells = [];
|
|
|
|
// Loop all rows
|
|
_.each(rows, function (row) {
|
|
// Loop cells of row
|
|
_.each(row.cells, function (cell) {
|
|
var columnModel = self.columns.get({cid: cell.column.cid});
|
|
if (!_.isEmpty(columnModel)) {
|
|
self.headerCells.push({
|
|
$el: cell.$el,
|
|
el: cell.el,
|
|
column: columnModel
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Sort cells
|
|
var headerCells = _.sortBy(self.headerCells, function (cell) {
|
|
return self.columns.indexOf(cell.column);
|
|
});
|
|
|
|
// Filter cells
|
|
self.headerCells = _.filter(headerCells, function(cell) {
|
|
return cell.column.get("renderable") === true ||
|
|
typeof cell.column.get("renderable") === "undefined";
|
|
});
|
|
|
|
self.headerElements = _.map(self.headerCells, function (cell) {
|
|
return cell.el;
|
|
});
|
|
},
|
|
|
|
remove: function() {
|
|
$(window).off("resize", this.resizeEvtHandler);
|
|
Backbone.View.prototype.remove.call(this);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Sample definition for the spacer column
|
|
*/
|
|
Backgrid.Extension.SizeAbleColumns.spacerColumnDefinition = {
|
|
name: "__spacerColumn",
|
|
label: "",
|
|
editable: false,
|
|
cell: Backgrid.StringCell,
|
|
width: "*",
|
|
nesting: [],
|
|
resizeable: false,
|
|
sortable: false,
|
|
orderable: false,
|
|
displayOrder: 9999
|
|
};
|
|
return Backgrid;
|
|
}));
|
|
|