1284 lines
39 KiB
JavaScript
1284 lines
39 KiB
JavaScript
(function($, window, undefined){
|
|
|
|
/*
|
|
events:
|
|
- beforeRowDeleted
|
|
- rowDeleted
|
|
- beforeRowAdded
|
|
- rowAdded
|
|
- api.rowAdded
|
|
- api.rowDeleted
|
|
- beforeRowRendered
|
|
- rowRendered
|
|
- beforeRowsSorted
|
|
- rowsSorted
|
|
- beforeRender
|
|
- render
|
|
- recordCountUpdated
|
|
- pagination.pageChanged
|
|
- search
|
|
- beforeRowSelected
|
|
- rowSelected
|
|
- beforeDataLoaded
|
|
*/
|
|
|
|
if (typeof($) === "undefined" || typeof($.fn) === "undefined" || typeof($.fn.jquery) === "undefined") {
|
|
throw "Missing jQuery (1.4.2 or greater) dependency";
|
|
}
|
|
|
|
var defaultSettings = {
|
|
columns: [], //array of objects representing each column; see defaultColumn below for the allowed options in each column object
|
|
height: "auto", //height of the grid; "auto" for no scrolling or an integer
|
|
cssScope: "gridiron-", //the scope of the GridIron CSS classes; string
|
|
showHeader: true, //whether to show the header columns; boolean
|
|
showFooter: true, //whether to show the footer; boolean
|
|
data: [], //array of objects
|
|
showRecordCount: true, //shows or hides the "viewing x of y" message; boolean
|
|
|
|
//pagination
|
|
enablePagination: true,
|
|
rowsPerPage: 20,
|
|
showPaginationControls: true,
|
|
|
|
//ajax
|
|
ajax: {
|
|
enabled: false,
|
|
url: "",
|
|
httpMethod: "GET", //HTTP method to use to retrieve data
|
|
success: null,
|
|
error: function(xhr, status, error) {
|
|
window.alert("Failed to load data, got a " + xhr.status + " response.");
|
|
},
|
|
complete: null,
|
|
useCache: true,
|
|
parameters: {
|
|
sortColumn: "sort_column", //name of the column that is being sorted
|
|
sortDirection: "sort_dir", //asc or desc
|
|
rowsPerPage: "limit",
|
|
startingRow: "offset"
|
|
}
|
|
},
|
|
|
|
//overlay
|
|
enableOverlay: false,
|
|
overlayMessage: "Loading" + String.fromCharCode(0x2026), //<-- ellipsis ftw
|
|
|
|
title: "",
|
|
showSearchBar: false,
|
|
virtualScrolling: false,
|
|
virtualScrollingContext: 20,
|
|
rowHeight: "auto", //height of a row; "auto" or an integer
|
|
highlight: "row", //enable row or cell highlighting; "row", "cell" or "none"
|
|
select: "none", //enable row or cell selecting; "row", "cell" or "none"
|
|
enableSingleRowSelect: false
|
|
};
|
|
|
|
var defaultColumn = {
|
|
title: "",
|
|
formatter: null,
|
|
width: 200,
|
|
sortable: false
|
|
};
|
|
|
|
var scrollbarDimensions = null;
|
|
var ASCENDING = "asc", DESCENDING = "desc";
|
|
|
|
/**
|
|
* Stolen from SlickGrid
|
|
*/
|
|
var getScrollbarDimensions = function() {
|
|
var $temp = $("<div/>")
|
|
.css({ position: "absolute", top: "-10000px", left: "-10000px", width: "100px", height: "100px", overflow: "scroll" })
|
|
.appendTo(document.body);
|
|
|
|
var dimensions = {
|
|
width: $temp.width() - $temp[0].clientWidth,
|
|
height: $temp.height() - $temp[0].clientHeight
|
|
};
|
|
|
|
$temp.remove();
|
|
return dimensions;
|
|
};
|
|
|
|
//@todo these values should be determined dynamically by examining stylesheets or something
|
|
//4 pixels for padding, 1 pixel for (left) border
|
|
var cellWidthOffset = 5;
|
|
|
|
//two pixels for top and bottom borders
|
|
var rowHeightOffset = 2;
|
|
|
|
var gridIron = function(settings) {
|
|
scrollbarDimensions = scrollbarDimensions || getScrollbarDimensions();
|
|
settings = $.extend(true, {}, defaultSettings, settings || {});
|
|
|
|
var sortInfo = {
|
|
primary: "",
|
|
secondary: [],
|
|
direction: ASCENDING
|
|
};
|
|
var searchInfo = {
|
|
globalQuery: null,
|
|
columns: {} //"column name": "search term"
|
|
};
|
|
|
|
var totalGridWidth = function() {
|
|
for (var i = 0, sum = 0; i < settings.columns.length; i++) {
|
|
sum += settings.columns[i].width;
|
|
};
|
|
|
|
return sum;
|
|
}();
|
|
|
|
//default AJAX options
|
|
var defaultAjaxOptions = {
|
|
dataType: "json",
|
|
url: settings.ajax.url,
|
|
type: settings.ajax.httpMethod,
|
|
success: function(data, status, xhr) {
|
|
if ($.isArray(data)) {
|
|
loadData(data, 0);
|
|
} else {
|
|
//assumed to be a paged object
|
|
if (typeof(data.totalRecordCount) === "undefined" || typeof(data.records) === "undefined" || typeof(data.offset) === "undefined") {
|
|
throw "Invalid JSON response from an AJAX request: expected either an array or an object with properties \"totalRecordCount\", \"offset\" and \"records\"";
|
|
}
|
|
|
|
if (settings.enablePagination) {
|
|
loadData(data.records, data.offset);
|
|
recordTracker.setCount(data.totalRecordCount);
|
|
} else {
|
|
loadData(data.records, 0);
|
|
//if it's non-paged, then the total amount of data is the total number of records
|
|
recordTracker.setCount(data.records.length);
|
|
}
|
|
}
|
|
|
|
renderRows();
|
|
|
|
if (settings.ajax.success) {
|
|
settings.ajax.success.call(this, data, status, xhr);
|
|
}
|
|
},
|
|
|
|
error: settings.ajax.error,
|
|
complete: settings.ajax.complete
|
|
};
|
|
|
|
//to aid in minifying, plus I'm sick of typing it
|
|
var cssScope = settings.cssScope;
|
|
|
|
//the container for the grid
|
|
var $gridContainer = this;
|
|
|
|
//the container for the grid data, this has height = settings.height
|
|
var $dataContainer = null;
|
|
|
|
//the container for the rows
|
|
var $rowContainer = null;
|
|
|
|
var $header = null;
|
|
var $footer = null;
|
|
|
|
//array of all rows, whether visible or not
|
|
var rows = [];
|
|
|
|
//need to define this here because the default row height calculation needs it
|
|
var createCellContainer = function() {
|
|
var $cellContainer = $("<div/>").addClass(cssScope + "cell");
|
|
|
|
return function(columnIndex) {
|
|
var $cell = $cellContainer.clone();
|
|
if (columnIndex === 0) {
|
|
$cell.addClass(cssScope + "first");
|
|
}
|
|
|
|
return $cell;
|
|
}
|
|
}();
|
|
|
|
//set the default row height if necessary
|
|
var defaultRowHeight = function() {
|
|
var $temp = $("<div/>")
|
|
.css({ left: "-10000px", top: "-10000px", position: "absolute" })
|
|
.addClass(cssScope + "row")
|
|
.append(createCellContainer())
|
|
.appendTo(document.body);
|
|
|
|
var height = $temp.height();
|
|
$temp.remove();
|
|
return height;
|
|
}();
|
|
|
|
//the actual height of each row
|
|
var actualRowHeight = ((settings.rowHeight === "auto") ? defaultRowHeight : settings.rowHeight) + rowHeightOffset;
|
|
|
|
//dom stuff
|
|
//-------------------------------------------------//
|
|
var createIndividualRowContainer = function() {
|
|
var $container = $("<div/>").addClass(cssScope + "row ui-widget-content");
|
|
if (settings.rowHeight !== "auto" && settings.rowHeight !== defaultRowHeight) {
|
|
//don't need to muck up the markup if the specified row height matches what's in the CSS
|
|
$container.height(settings.rowHeight);
|
|
}
|
|
|
|
return function() {
|
|
return $container.clone();
|
|
}
|
|
}();
|
|
|
|
var createCell = function(columnIndex, column, value, data) {
|
|
var $cell = createCellContainer(columnIndex).width(parseInt(column.width) - cellWidthOffset);
|
|
|
|
value = column.formatter ? column.formatter.call(null, columnIndex, column, value, data) : value;
|
|
if (value === null || value === "") {
|
|
//required to make the cell heights match
|
|
value = String.fromCharCode(0x00A0); //<-- nbspftw
|
|
}
|
|
|
|
$cell.append(value);
|
|
|
|
return $cell;
|
|
};
|
|
|
|
var createHeader = function() {
|
|
$header = createIndividualRowContainer().addClass(cssScope + "header ui-widget-header");
|
|
if (!settings.title && !settings.showSearchBar) {
|
|
$header.addClass("ui-corner-top");
|
|
}
|
|
|
|
for (var i = 0, len = settings.columns.length, column, $cell; i < len; i++) {
|
|
column = $.extend({}, defaultColumn, settings.columns[i]);
|
|
$cell = createCellContainer(i).width(column.width - cellWidthOffset).text(column.displayText);
|
|
|
|
if (column.sortable) {
|
|
enableSortingForColumn($cell, column.name);
|
|
}
|
|
if (i === 0) {
|
|
$cell.addClass("ui-corner-tl");
|
|
} else if (i === settings.columns.length - 1) {
|
|
$cell.addClass("ui-corner-tr");
|
|
}
|
|
|
|
$header.append($cell);
|
|
}
|
|
|
|
return $header;
|
|
};
|
|
|
|
var createFooter = function() {
|
|
$footer = $("<div/>").addClass(cssScope + "footer ui-widget-header ui-corner-bottom ui-helper-clearfix");
|
|
|
|
if (settings.showRecordCount) {
|
|
$("<span/>")
|
|
.addClass(cssScope + "record-display")
|
|
.text("No records")
|
|
.appendTo($footer);
|
|
}
|
|
|
|
return $footer;
|
|
};
|
|
|
|
var createCaption = function() {
|
|
var $caption = $("<div/>")
|
|
.addClass(cssScope + "caption ui-state-default ui-corner-top ui-helper-clearfix")
|
|
.append($("<span/>").addClass(cssScope + "title").text(settings.title));
|
|
|
|
if (settings.showSearchBar) {
|
|
var $textbox = $("<input type='text'/>").keypress(function(e) {
|
|
if (e.which === 13) {
|
|
submitSearch.call(this);
|
|
}
|
|
});
|
|
|
|
var submitSearch = function() {
|
|
$gridContainer.triggerHandler("search", [$.trim($textbox.val())]);
|
|
};
|
|
|
|
var $icon = $("<span/>")
|
|
.attr("title", "Search")
|
|
.addClass(cssScope + "search-submit ui-icon ui-icon-search")
|
|
.click(submitSearch);
|
|
|
|
$("<span/>")
|
|
.addClass(cssScope + "search")
|
|
.append($textbox, $icon)
|
|
.appendTo($caption);
|
|
}
|
|
|
|
return $caption;
|
|
};
|
|
|
|
var createRow = function(data, rowId) {
|
|
if (rowId === undefined) {
|
|
rowId = generateRowId();
|
|
}
|
|
|
|
var $row = createIndividualRowContainer().data("rowId", rowId), cells = [];
|
|
for (var i = 0; i < settings.columns.length; i++) {
|
|
cells[i] = {
|
|
element: createCell(i, settings.columns[i], data[settings.columns[i].name], data),
|
|
column: settings.columns[i],
|
|
position: i
|
|
};
|
|
|
|
$row.append(cells[i].element);
|
|
}
|
|
|
|
return {
|
|
rowId: rowId,
|
|
cells: cells,
|
|
data: data,
|
|
element: $row,
|
|
selected: false,
|
|
enabled: true,
|
|
select: function() {
|
|
if (!this.enabled) {
|
|
return this;
|
|
}
|
|
|
|
$row.addClass("ui-state-focus " + cssScope + "selected");
|
|
this.selected = true;
|
|
return this;
|
|
},
|
|
deselect: function() {
|
|
if (!this.enabled) {
|
|
return this;
|
|
}
|
|
|
|
$row.removeClass("ui-state-focus " + cssScope + "selected");
|
|
this.selected = false;
|
|
return this;
|
|
},
|
|
hide: function() {
|
|
$row.hide();
|
|
return this;
|
|
},
|
|
show: function() {
|
|
$row.show();
|
|
return this;
|
|
},
|
|
disable: function() {
|
|
$row.addClass("ui-state-disabled");
|
|
this.enabled = false;
|
|
return this;
|
|
},
|
|
enable: function() {
|
|
$row.removeClass("ui-state-disabled");
|
|
this.enabled = true;
|
|
return this;
|
|
}
|
|
};
|
|
};
|
|
|
|
var enableSortingForColumn = function($headerCell, columnName) {
|
|
$headerCell.addClass(cssScope + "sortable").click(function() {
|
|
var $icon = $("." + cssScope + "sort-indicator", this);
|
|
var direction = $icon.hasClass("ui-icon-triangle-1-n") ? DESCENDING : ASCENDING;
|
|
|
|
$(this)
|
|
.closest("." + cssScope + "header")
|
|
.find("." + cssScope + "sort-indicator")
|
|
.removeClass("ui-icon-triangle-1-s ui-icon-triangle-1-n")
|
|
.addClass("ui-icon-triangle-2-n-s");
|
|
|
|
sortRows(columnName, direction);
|
|
$icon.addClass("ui-icon-triangle-1-" + (direction === DESCENDING ? "s" : "n")).removeClass("ui-icon-triangle-2-n-s");
|
|
});
|
|
|
|
var $indicator = $("<span/>").addClass(cssScope + "sort-indicator ui-icon ui-icon-triangle-2-n-s");
|
|
$indicator.appendTo($headerCell);
|
|
};
|
|
//-------------------------------------------------//
|
|
|
|
//rendering
|
|
//-------------------------------------------------//
|
|
var loadData = function(data, startingRowIndex) {
|
|
if (!$gridContainer.triggerHandler("beforeDataLoaded", [data])) {
|
|
return;
|
|
}
|
|
if (startingRowIndex === undefined) {
|
|
resetRows();
|
|
startingRowIndex = 0;
|
|
}
|
|
|
|
startingRowIndex = parseInt(startingRowIndex);
|
|
|
|
//note that this doesn't actually render anything, just sets up the rows array
|
|
for (var i = 0, row; i < data.length; i++) {
|
|
row = createRow(data[i]);
|
|
addRowToArray(row, startingRowIndex + i);
|
|
}
|
|
};
|
|
|
|
var renderRows = function(startRow, endRow) {
|
|
if (!$gridContainer.triggerHandler("beforeRender")) {
|
|
return;
|
|
}
|
|
|
|
$rowContainer.empty();
|
|
|
|
if (startRow === undefined) {
|
|
startRow = settings.enablePagination ? (pager.getCurrentPage() - 1) * settings.rowsPerPage : 0;
|
|
}
|
|
if (endRow === undefined) {
|
|
endRow = settings.enablePagination ? Math.min(startRow + settings.rowsPerPage, rows.length - 1) : rows.length - 1;
|
|
}
|
|
|
|
for (var i = startRow; i <= endRow; i++) {
|
|
rows[i].element.data("rowId", rows[i].rowId); //we empty the row container, and apparently the data leaves when the element is removed
|
|
|
|
if (settings.virtualScrolling) {
|
|
rows[i].element.css({ position: "absolute", top: (actualRowHeight * i) + "px" });
|
|
} else {
|
|
rows[i].element.css({ position: "relative", top: 0 });
|
|
}
|
|
|
|
insertRow(rows[i], null, true);
|
|
}
|
|
|
|
$gridContainer.triggerHandler("render");
|
|
};
|
|
|
|
var addRowToArray = function(row, insertionIndex) {
|
|
if ($gridContainer.triggerHandler("beforeRowAdded", [row])) {
|
|
if (typeof(rows[insertionIndex]) === "object") {
|
|
//row already exists, so splice it in
|
|
var left = rows.slice(0, insertionIndex), right = rows.slice(insertionIndex);
|
|
rows = left.concat(row, right);
|
|
} else {
|
|
rows[insertionIndex] = row;
|
|
}
|
|
|
|
$gridContainer.triggerHandler("rowAdded", [row]);
|
|
}
|
|
}
|
|
|
|
var insertRow = function(row, insertionIndex, justAppendIt) {
|
|
if (!$gridContainer.triggerHandler("beforeRowRendered", [row])) {
|
|
return;
|
|
}
|
|
|
|
if (justAppendIt) {
|
|
$rowContainer.append(row.element);
|
|
} else if (insertionIndex > 0) {
|
|
//@todo problems be here
|
|
row.element.insertAfter(rows[insertionIndex - 1].element);
|
|
} else {
|
|
$rowContainer.prepend(row.element);
|
|
}
|
|
|
|
$gridContainer.triggerHandler("rowRendered", [row]);
|
|
};
|
|
//-------------------------------------------------//
|
|
|
|
//shared api
|
|
//-------------------------------------------------//
|
|
var deleteRowById = function(rowId) {
|
|
for (var i = 0; i < rows.length; i++) {
|
|
if (rows[i].rowId == rowId) {
|
|
var row = rows[i];
|
|
if ($gridContainer.triggerHandler("beforeRowDeleted", [row])) {
|
|
rows[i].element.remove();
|
|
rows.splice(i, 1);
|
|
|
|
$gridContainer.triggerHandler("rowDeleted", [row]);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $gridContainer;
|
|
};
|
|
|
|
var findRowById = function(rowId) {
|
|
for (var rowIndex in rows) {
|
|
if (rows[rowIndex].rowId == rowId) {
|
|
return rows[rowIndex];
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
var sortRows = function(primaryColumn, direction, secondaryColumns) {
|
|
if (!$gridContainer.triggerHandler("beforeRowsSorted", [primaryColumn, direction, secondaryColumns || []])) {
|
|
return;
|
|
}
|
|
|
|
//client-side sort: this will sort strings by first converting them to lower case (so a == A, instead of a > A)
|
|
//if the column value is a number, it will sort numerically
|
|
direction = direction === DESCENDING ? -1 : 1;
|
|
rows.sort(function(x, y) {
|
|
var xValue = x.data[primaryColumn];
|
|
var yValue = y.data[primaryColumn];
|
|
|
|
if (xValue === yValue) {
|
|
return 0;
|
|
}
|
|
|
|
//handle numbers
|
|
var xFloat = parseFloat(xValue);
|
|
var yFloat = parseFloat(yValue);
|
|
if (!isNaN(xFloat) || !isNaN(yFloat)) {
|
|
if (isNaN(xFloat)) {
|
|
return -direction;
|
|
}
|
|
if (isNaN(yFloat)) {
|
|
return direction;
|
|
}
|
|
|
|
return xFloat < yFloat ? -direction : direction;
|
|
}
|
|
|
|
//assume they're both strings
|
|
xValue = $.trim((xValue + "")).toLowerCase();
|
|
yValue = $.trim((yValue + "")).toLowerCase();
|
|
|
|
return xValue < yValue ? -direction : direction;
|
|
});
|
|
|
|
$gridContainer.triggerHandler("rowsSorted");
|
|
|
|
renderRows();
|
|
};
|
|
//-------------------------------------------------//
|
|
|
|
//helpers
|
|
//-------------------------------------------------//
|
|
var resetRows = function() {
|
|
rows = [];
|
|
};
|
|
|
|
var getVisibleRowRange = function() {
|
|
var scrollTop = $dataContainer.scrollTop();
|
|
var dataHeight = $dataContainer.height();
|
|
|
|
var first = Math.max(0, scrollTop / actualRowHeight);
|
|
|
|
return {
|
|
first: Math.floor(first),
|
|
last: Math.min(recordTracker.getCount(), Math.ceil(getMaxVisibleRowCount() + first))
|
|
};
|
|
};
|
|
|
|
var getMaxVisibleRowCount = function() {
|
|
return Math.floor($dataContainer.height() / actualRowHeight);
|
|
};
|
|
|
|
var rowsRequiresVerticalScrollbar = function() {
|
|
return settings.height !== "auto" && (settings.virtualScrolling || getVisibleRowCount() * actualRowHeight >= $dataContainer.height());
|
|
};
|
|
|
|
var horizontalScrollbarIsVisible = function() {
|
|
return $dataContainer.width() !== $dataContainer[0].clientWidth;
|
|
};
|
|
|
|
var getVisibleRowCount = function() {
|
|
return $rowContainer.find("." + cssScope + "row:visible").length;
|
|
};
|
|
|
|
var generateRowId = function() {
|
|
var rowIds = [];
|
|
return function() {
|
|
var rowId = rowIds.length;
|
|
rowIds[rowId] = 1;
|
|
return rowId;
|
|
};
|
|
}();
|
|
|
|
var rowDataAlreadyLoaded = function(startingRow, endingRow) {
|
|
for (var i = startingRow; i <= endingRow; i++) {
|
|
if (rows[i] === undefined) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return i === endingRow + 1;
|
|
};
|
|
|
|
var rowsAlreadyRendered = function(startingRow, endingRow) {
|
|
//if the first and last rows and one exactly between are already rendered, everything in between them should be as well
|
|
var firstAndLast =
|
|
rows[startingRow] !== undefined && rows[startingRow].element.is(":visible") &&
|
|
rows[endingRow] !== undefined && rows[endingRow].element.is(":visible");
|
|
|
|
var middle = Math.floor(startingRow + (endingRow - startingRow) / 2);
|
|
return firstAndLast && rows[middle] !== undefined && rows[middle].element.is(":visible");
|
|
};
|
|
|
|
var arrayFilter = function() {
|
|
var filter = null;
|
|
if (Array.prototype.filter) {
|
|
filter = Array.prototype.filter;
|
|
} else {
|
|
filter = function(func) {
|
|
var filteredArray = [];
|
|
for (var i = 0, count = 0; i < this.length; i++) {
|
|
if (func(this[i], i, this)) {
|
|
filteredArray[count++] = this[i];
|
|
}
|
|
}
|
|
|
|
return filteredArray;
|
|
};
|
|
}
|
|
|
|
return function(array, func) {
|
|
return filter.call(array, func);
|
|
};
|
|
}();
|
|
//-------------------------------------------------//
|
|
|
|
//controllers
|
|
//-------------------------------------------------//
|
|
var pager = function() {
|
|
var currentPage = 1;
|
|
var $prevPage, $nextPage, $pageDisplay;
|
|
|
|
return {
|
|
init: function() {
|
|
var $tempFooter = $footer || $gridContainer.find("." + cssScope + "footer");
|
|
$prevPage = $tempFooter.find("." + cssScope + "pagination-previous");
|
|
$nextPage = $tempFooter.find("." + cssScope + "pagination-next");
|
|
$pageDisplay = $tempFooter.find("." + cssScope + "pagination-page-display");
|
|
},
|
|
|
|
getCurrentPage: function() {
|
|
return currentPage;
|
|
},
|
|
|
|
getTotalPages: function() {
|
|
return Math.max(1, Math.ceil(recordTracker.getCount() / settings.rowsPerPage));
|
|
},
|
|
|
|
previous: function() {
|
|
if (currentPage === 1) {
|
|
return false;
|
|
}
|
|
|
|
$gridContainer.triggerHandler("pagination.pageChanged", [currentPage - 1]);
|
|
return true;
|
|
},
|
|
|
|
next: function() {
|
|
if (currentPage === pager.getTotalPages()) {
|
|
return false;
|
|
}
|
|
|
|
$gridContainer.triggerHandler("pagination.pageChanged", [currentPage + 1]);
|
|
return true;
|
|
},
|
|
|
|
jumpToPage: function(pageNumber) {
|
|
if (pageNumber < 1 || pageNumber > pager.getTotalPages()) {
|
|
return false;
|
|
}
|
|
|
|
$gridContainer.triggerHandler("pagination.pageChanged", [pageNumber]);
|
|
},
|
|
|
|
onBeforeRowRendered: function(e, row) {
|
|
return getVisibleRowCount() < settings.rowsPerPage;
|
|
},
|
|
|
|
updatePaginationControls: function() {
|
|
var totalPages = pager.getTotalPages();
|
|
$pageDisplay.find("span").text(" of " + totalPages);
|
|
|
|
$prevPage.removeClass("ui-state-disabled");
|
|
$nextPage.removeClass("ui-state-disabled");
|
|
|
|
if (currentPage === 1) {
|
|
$prevPage.addClass("ui-state-disabled");
|
|
}
|
|
if (currentPage === totalPages) {
|
|
$nextPage.addClass("ui-state-disabled");
|
|
}
|
|
|
|
$pageDisplay.find("input").val(currentPage).removeClass("ui-state-error");
|
|
},
|
|
|
|
reload: function() {
|
|
pager.updatePaginationControls();
|
|
recordTracker.updateDisplay();
|
|
renderRows();
|
|
},
|
|
|
|
updatePageNumber: function(e, newPageNumber) {
|
|
currentPage = newPageNumber;
|
|
recordTracker.updateDisplay();
|
|
}
|
|
}
|
|
}();
|
|
|
|
var recordTracker = function() {
|
|
var $recordDisplay = null;
|
|
var totalRecordCount = 0;
|
|
|
|
return {
|
|
updateDisplay: function() {
|
|
if (!settings.showRecordCount) {
|
|
return;
|
|
}
|
|
|
|
$recordDisplay = $recordDisplay || $gridContainer.find("." + cssScope + "footer ." + cssScope + "record-display");
|
|
|
|
if (totalRecordCount > 0) {
|
|
var text = "";
|
|
if (settings.virtualScrolling) {
|
|
//display the currently visible records
|
|
var rowRange = getVisibleRowRange();
|
|
//+1 because rowRange is zero-based
|
|
text = "Viewing " + (rowRange.first + 1) + "-" + rowRange.last + " of " + totalRecordCount;
|
|
} else if (settings.enablePagination) {
|
|
text =
|
|
"Viewing " + ((pager.getCurrentPage() - 1) * settings.rowsPerPage + 1) +
|
|
"-" + Math.min(rows.length, pager.getCurrentPage() * settings.rowsPerPage) +
|
|
" of " + totalRecordCount;
|
|
} else {
|
|
text = totalRecordCount + " record" + (totalRecordCount !== 1 ? "s" : "");
|
|
}
|
|
|
|
$recordDisplay.text(text);
|
|
} else {
|
|
$recordDisplay.text("No records");
|
|
}
|
|
},
|
|
|
|
setCount: function(recordCount) {
|
|
totalRecordCount = recordCount;
|
|
$gridContainer.triggerHandler("recordCountUpdated", [totalRecordCount]);
|
|
},
|
|
|
|
getCount: function() {
|
|
return totalRecordCount;
|
|
}
|
|
};
|
|
}();
|
|
|
|
var ajax = function() {
|
|
var defaultOptions = defaultAjaxOptions;
|
|
|
|
return {
|
|
init: function() {
|
|
if (settings.enableOverlay) {
|
|
var oldComplete = defaultOptions.complete;
|
|
$.extend(defaultOptions, {
|
|
complete: function() {
|
|
oldComplete && oldComplete();
|
|
overlay.remove();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
send: function(extraOptions, rowsPerPage, startingRow) {
|
|
var queryStringValues = {};
|
|
if (settings.enablePagination && rowsPerPage !== undefined && startingRow !== undefined) {
|
|
queryStringValues[settings.ajax.parameters.sortColumn] = sortInfo.primary;
|
|
queryStringValues[settings.ajax.parameters.sortDirection] = sortInfo.direction;
|
|
queryStringValues[settings.ajax.parameters.rowsPerPage] = rowsPerPage;
|
|
queryStringValues[settings.ajax.parameters.startingRow] = startingRow;
|
|
}
|
|
|
|
if (searchInfo.globalQuery !== null) {
|
|
queryStringValues["query"] = searchInfo.globalQuery;
|
|
}
|
|
|
|
$.each(searchInfo.columns, function(columnName, value) {
|
|
if (searchInfo.columns[columnName] !== null) {
|
|
queryStringValues["filter[" + columnName + "]"] = searchInfo.columns[columnName];
|
|
}
|
|
});
|
|
|
|
$.ajax($.extend(true, {}, defaultOptions, extraOptions || {}, { data: queryStringValues }));
|
|
},
|
|
|
|
sendAndResetFromCurrentOffset: function(callback) {
|
|
//this assumes either virtual scrolling or pagination, which is not always true...
|
|
var rowsPerPage = settings.virtualScrolling ? getMaxVisibleRowCount() + settings.virtualScrollingContext * 2 : settings.rowsPerPage;
|
|
var startingRow = settings.virtualScrolling ? Math.max(0, getVisibleRowRange().first - settings.virtualScrollingContext) : (pager.getCurrentPage() - 1) * settings.rowsPerPage;
|
|
|
|
ajax.send({
|
|
success: function(data) {
|
|
callback && callback.call(this);
|
|
resetRows();
|
|
|
|
loadData(data.records, data.offset);
|
|
recordTracker.setCount(data.totalRecordCount);
|
|
if (settings.virtualScrolling) {
|
|
renderRows(data.offset, data.offset + data.records.length - 1); //-1 because renderRows needs to be zero-based
|
|
} else {
|
|
renderRows();
|
|
}
|
|
}
|
|
}, rowsPerPage, startingRow);
|
|
},
|
|
|
|
onSort: function(e, primaryColumn, direction, secondaryColumns) {
|
|
ajax.sendAndResetFromCurrentOffset(function() { $gridContainer.trigger("rowsSorted"); });
|
|
|
|
//return false, because we don't want the client side sort to override anything
|
|
return false;
|
|
},
|
|
|
|
sendPaged: function(e, pageNumber) {
|
|
if (settings.ajax.useCache) {
|
|
var start = (pageNumber - 1) * settings.rowsPerPage, end = start + settings.rowsPerPage;
|
|
if (rowDataAlreadyLoaded(start, end)) {
|
|
renderRows(start, end);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ajax.send(null, settings.rowsPerPage, (pager.getCurrentPage() - 1) * settings.rowsPerPage);
|
|
}
|
|
};
|
|
}();
|
|
|
|
var overlay = function() {
|
|
var $element = null, $indicator = null;
|
|
return {
|
|
add: function() {
|
|
if ($element || $indicator) {
|
|
return;
|
|
}
|
|
|
|
$indicator = $("<div/>")
|
|
.addClass(cssScope + "loading-indicator ui-widget-content ui-corner-all")
|
|
.append($("<div/>").addClass(cssScope + "spinner"))
|
|
.append($("<div/>").addClass(cssScope + "message").text(settings.overlayMessage))
|
|
.css("visibility", "hidden");
|
|
|
|
var position = $dataContainer.position();
|
|
$element = $("<div/>")
|
|
.addClass("ui-widget-overlay")
|
|
.width($dataContainer.outerWidth())
|
|
.height($dataContainer.outerHeight())
|
|
.css({ top: position.top, left: position.left })
|
|
.appendTo($gridContainer);
|
|
|
|
//center the loading indicator vertically and horizontally.
|
|
//we need its own width and height to do that, hence the visibility weirdness
|
|
$indicator.appendTo($gridContainer).css({
|
|
left: Math.floor(position.left + $dataContainer.width() / 2 - $indicator.width() / 2) + "px",
|
|
top: Math.floor(position.top + $dataContainer.height() / 2 - $indicator.height() / 2) + "px",
|
|
visibility: "visible"
|
|
});
|
|
},
|
|
|
|
remove: function() {
|
|
$element && $element.remove() && ($element = null);
|
|
$indicator && $indicator.remove() && ($indicator = null);
|
|
}
|
|
};
|
|
}();
|
|
//-------------------------------------------------//
|
|
|
|
//initialization stuff
|
|
//-------------------------------------------------//
|
|
var prepareGrid = function() {
|
|
//add css class and set width and height
|
|
$gridContainer.addClass(cssScope + "grid ui-widget ui-widget-content").width(totalGridWidth);
|
|
|
|
if (settings.title || settings.showSearchBar || settings.showHeader) {
|
|
$gridContainer.addClass("ui-corner-top");
|
|
|
|
if (settings.title || settings.showSearchBar) {
|
|
$gridContainer.append(createCaption());
|
|
}
|
|
|
|
if (settings.showHeader) {
|
|
$gridContainer.append(createHeader());
|
|
}
|
|
}
|
|
|
|
var overflowY = "auto";
|
|
if (settings.height === "auto") {
|
|
//defaulting to overflow: auto for auto-height royally jacks things up (somewhat ironically)
|
|
overflowY = "hidden";
|
|
} else if (settings.virtualScrolling) {
|
|
//to fix rendering bugs that i don't want to solve more elegantly at the moment
|
|
overflowY = "scroll";
|
|
}
|
|
|
|
//create data container
|
|
$dataContainer = $("<div/>")
|
|
.addClass(cssScope + "data-container")
|
|
.css({
|
|
height: (settings.height === "auto" ? "auto" : settings.height + "px"),
|
|
"overflow-y": overflowY
|
|
}).appendTo($gridContainer);
|
|
|
|
$rowContainer = $("<div/>").addClass(cssScope + "row-container");
|
|
$rowContainer.appendTo($dataContainer);
|
|
|
|
if (settings.showFooter) {
|
|
$gridContainer.addClass("ui-corner-bottom").append(createFooter());
|
|
}
|
|
|
|
settings.enablePagination && setupPagination();
|
|
settings.virtualScrolling && setupVirtualScrolling();
|
|
};
|
|
|
|
var setupVirtualScrolling = function() {
|
|
//this should probably happen after initial rendering (if possible) because it screws up the rendering of the last cell
|
|
$rowContainer.height(actualRowHeight * settings.data.length);
|
|
|
|
//when the data container is scrolled, we need to load the data for the visible rows
|
|
var waitingForScrollingToStopInterval = null;
|
|
var handleScroll = function() {
|
|
var rowRange = getVisibleRowRange();
|
|
var startingRow = Math.max(0, rowRange.first - settings.virtualScrollingContext);
|
|
var endingRow = Math.min(recordTracker.getCount() - 1, rowRange.last + settings.virtualScrollingContext);
|
|
|
|
if (rowsAlreadyRendered(startingRow, endingRow)) {
|
|
//if the rows are already visible, there's no need to rerender; this prevents hideous flashes when you scroll to the very bottom
|
|
//it's also a hefty performance increase since it's not doing rowsPerPage + 2 * virtualScrollingContext DOM deletions and inserts every time you scroll
|
|
return;
|
|
}
|
|
|
|
if (settings.ajax.enabled) {
|
|
if (settings.ajax.useCache && rowDataAlreadyLoaded(startingRow, endingRow)) {
|
|
renderRows(startingRow, endingRow);
|
|
return true;
|
|
}
|
|
|
|
ajax.send({
|
|
success: function(data) {
|
|
loadData(data.records, data.offset);
|
|
recordTracker.setCount(data.totalRecordCount);
|
|
renderRows(data.offset, data.offset + data.records.length - 1); //-1 because row indices are zero-based, and length is not
|
|
}
|
|
}, endingRow - startingRow + 1, startingRow); //+1 because it includes endingRow and startingRow
|
|
} else {
|
|
renderRows(startingRow, endingRow);
|
|
}
|
|
|
|
waitingForScrollingToStopInterval = null;
|
|
return true;
|
|
};
|
|
|
|
$dataContainer.scroll(function() {
|
|
recordTracker.updateDisplay(); //update the record display with the currently visible row numbers
|
|
if (settings.enablePagination) {
|
|
var pageNumber = Math.floor(getVisibleRowRange().first / settings.rowsPerPage) + 1; //+1 because getVisibleRowRange() is zero-based, and page numbers are one-based
|
|
if (pager.getCurrentPage() !== pageNumber) {
|
|
pager.updatePageNumber(null, pageNumber);
|
|
pager.updatePaginationControls();
|
|
}
|
|
}
|
|
|
|
if (waitingForScrollingToStopInterval !== null) {
|
|
window.clearTimeout(waitingForScrollingToStopInterval);
|
|
}
|
|
|
|
waitingForScrollingToStopInterval = window.setTimeout(handleScroll, 100);
|
|
});
|
|
};
|
|
|
|
var setupPagination = function() {
|
|
if (settings.showPaginationControls) {
|
|
var $prevPage = $("<span/>")
|
|
.addClass(cssScope + "pagination-previous ui-icon ui-icon-triangle-1-w ui-state-disabled")
|
|
.click(pager.previous);
|
|
|
|
var $nextPage = $("<span/>")
|
|
.addClass(cssScope + "pagination-next ui-icon ui-icon-triangle-1-e ui-state-disabled")
|
|
.click(pager.next);
|
|
|
|
var $pageDisplay = $("<span/>").addClass(cssScope + "pagination-page-display");
|
|
var $input = $("<input type='text'/>").attr("maxlength", 6).val(1).keypress(function(e) {
|
|
if (e.which === 13) {
|
|
var pageNumber = parseInt($(this).val());
|
|
if (isNaN(pageNumber) || pageNumber < 1 || pageNumber > pager.getTotalPages()) {
|
|
$(this).addClass("ui-state-error");
|
|
return false;
|
|
}
|
|
|
|
pager.jumpToPage(pageNumber);
|
|
}
|
|
});
|
|
|
|
$pageDisplay.append($input, $("<span/>").text(" of " + pager.getTotalPages()));
|
|
$gridContainer.find("." + cssScope + "footer").append($prevPage, $pageDisplay, $nextPage);
|
|
}
|
|
|
|
pager.init();
|
|
};
|
|
|
|
var setupDelegates = function() {
|
|
var selectable = settings.select === "row";
|
|
|
|
//set up highlighting
|
|
if (settings.highlight === "row") {
|
|
var highlightClass = selectable ? "ui-state-hover" : "ui-state-highlight";
|
|
//hover causes problems with a lot of data, so use mouseout and mouseover instead
|
|
$rowContainer.delegate("." + cssScope + settings.highlight, "mouseover", function() {
|
|
if (!$(this).hasClass("ui-state-disabled")) {
|
|
$(this).removeClass("ui-state-active");
|
|
$(this).addClass(highlightClass);
|
|
}
|
|
}).delegate("." + cssScope + settings.highlight, "mouseout", function() {
|
|
if (!$(this).hasClass("ui-state-disabled")) {
|
|
$(this).removeClass("ui-state-active " + highlightClass);
|
|
}
|
|
});
|
|
}
|
|
|
|
//set up selecting
|
|
if (selectable) {
|
|
$rowContainer.delegate("." + cssScope + settings.select, "click", function() {
|
|
if (!$(this).hasClass("ui-state-disabled")) {
|
|
var row = findRowById($(this).data("rowId"));
|
|
if ($gridContainer.triggerHandler("beforeRowSelected", [row])) {
|
|
row.element.removeClass("ui-state-active");
|
|
row[row.selected ? "deselect" : "select"]();
|
|
$gridContainer.triggerHandler("rowSelected", [row]);
|
|
}
|
|
}
|
|
}).delegate("." + cssScope + settings.select, "mousedown", function() {
|
|
if (!$(this).hasClass("ui-state-disabled")) {
|
|
var row = findRowById($(this).data("rowId"));
|
|
row.element.addClass("ui-state-active");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
var setupDefaultEvents = function() {
|
|
var scrollbarIsCurrentlyVisible = false;
|
|
|
|
var setScrollbarVisibilityVariable = function(e, row) {
|
|
scrollbarIsCurrentlyVisible = horizontalScrollbarIsVisible();
|
|
return true;
|
|
};
|
|
|
|
//the order that these events get bound is important: some need to happen before others or the world explodes!
|
|
$gridContainer
|
|
.bind("beforeRowDeleted beforeRowRendered", setScrollbarVisibilityVariable)
|
|
.bind("api.rowAdded", function(e, row) {
|
|
//can't do this on rowAdded because otherwise it gets called a billion times and slows everything down
|
|
//and we really only care about when a row gets added outside of the normal rendering phase
|
|
if (settings.showRecordCount) {
|
|
recordTracker.setCount(recordTracker.getCount() + 1);
|
|
}
|
|
}).bind("rowDeleted", function(e, row) {
|
|
if (scrollbarIsCurrentlyVisible && !rowsRequiresVerticalScrollbar()) {
|
|
//need to fix the width of the last cell so that they are no longer accounting for the vertical scrollbar
|
|
for (var i = 0, lastCell; i < rows.length; i++) {
|
|
if (rows[i] !== undefined) {
|
|
lastCell = rows[i].cells[rows[i].cells.length - 1];
|
|
lastCell.element.width(lastCell.column.width - cellWidthOffset);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (settings.showRecordCount) {
|
|
recordTracker.setCount(recordTracker.getCount() - 1);
|
|
}
|
|
}).bind("rowRendered", function(e, row) {
|
|
if (rowsRequiresVerticalScrollbar()) {
|
|
if (!scrollbarIsCurrentlyVisible) {
|
|
//need to fix the widths on the last cell so that they are accounting for the vertical scrollbar
|
|
for (var i = 0, lastCell; i < rows.length; i++) {
|
|
if (rows[i] !== undefined) {
|
|
lastCell = rows[i].cells[rows[i].cells.length - 1];
|
|
lastCell.element.width(lastCell.column.width - cellWidthOffset - scrollbarDimensions.width);
|
|
}
|
|
}
|
|
} else {
|
|
//scrollbar is already visible, but we still need to adjust the width of the newly inserted row
|
|
var lastCell = row.cells[row.cells.length - 1];
|
|
lastCell.element.width(lastCell.column.width - cellWidthOffset - scrollbarDimensions.width);
|
|
}
|
|
}
|
|
}).bind("beforeRowsSorted", function(e, primaryColumn, direction, secondaryColumns) {
|
|
$.extend(sortInfo, { primary: primaryColumn, direction: direction, secondary: secondaryColumns });
|
|
return true;
|
|
}).bind("search", function(e, searchQuery) {
|
|
searchInfo.globalQuery = searchQuery;
|
|
}).bind("recordCountUpdated", recordTracker.updateDisplay)
|
|
|
|
//default events return true to allow continued execution
|
|
$gridContainer.bind("beforeRender beforeRowRendered beforeRowAdded beforeRowSelected beforeDataLoaded", function() { return true; });
|
|
|
|
if (settings.enableOverlay) {
|
|
$gridContainer
|
|
.bind("beforeRowsSorted", overlay.add)
|
|
.bind("render rowsSorted", overlay.remove);
|
|
}
|
|
|
|
if (settings.ajax.enabled) {
|
|
ajax.init(); //there is a probably better place to call this...
|
|
if (!settings.enablePagination) {
|
|
//if no pagination, we just load the data immediately
|
|
ajax.send();
|
|
}
|
|
|
|
//sorting is special with ajax, because it needs to send a request to the server rather than
|
|
//doing some kind of client-side sort
|
|
$gridContainer.bind("beforeRowsSorted", ajax.onSort);
|
|
}
|
|
|
|
if (settings.enablePagination) {
|
|
if (settings.enableOverlay) {
|
|
$gridContainer.bind("pagination.pageChanged", overlay.add);
|
|
}
|
|
|
|
$gridContainer
|
|
.bind("pagination.pageChanged", pager.updatePageNumber)
|
|
.bind("rowDeleted", function() { renderRows(); }) //this is so that when a row is deleted, rows on other pages get pushed up to the visible page
|
|
.bind("beforeRowRendered", pager.onBeforeRowRendered);
|
|
|
|
if (settings.ajax.enabled) {
|
|
//set up the ajax call so that each time the page changes, the ajax request is sent
|
|
$gridContainer.bind("pagination.pageChanged", ajax.sendPaged);
|
|
} else {
|
|
//with an ajax call rendering can't happen until rows are loaded, but at all other times
|
|
//rendering should happen whenever a page is changed
|
|
$gridContainer.bind("pagination.pageChanged", function() { renderRows(); });
|
|
}
|
|
|
|
$gridContainer.bind("pagination.pageChanged recordCountUpdated", pager.updatePaginationControls);
|
|
}
|
|
|
|
if (settings.virtualScrolling) {
|
|
$gridContainer.bind("recordCountUpdated", function() {
|
|
$rowContainer.height(actualRowHeight * recordTracker.getCount());
|
|
});
|
|
|
|
if (settings.enablePagination) {
|
|
$gridContainer.bind("pagination.pageChanged", function(e, pageNumber) {
|
|
//scroll to the new page
|
|
$dataContainer.scrollTop(actualRowHeight * ((pageNumber - 1) * settings.rowsPerPage));
|
|
recordTracker.updateDisplay();
|
|
});
|
|
}
|
|
}
|
|
|
|
if (settings.enableSingleRowSelect) {
|
|
$gridContainer.bind("rowSelected", function(e, row) {
|
|
//deselect all other rows
|
|
for (var i = 0; i < rows.length; i++) {
|
|
if (rows[i].rowId !== row.rowId) {
|
|
rows[i].deselect();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
//-------------------------------------------------//
|
|
|
|
//do stuff
|
|
prepareGrid();
|
|
setupDefaultEvents();
|
|
setupDelegates();
|
|
loadData(settings.data, 0);
|
|
settings.data = [];
|
|
if (settings.ajax.enabled) {
|
|
if (settings.enablePagination) {
|
|
pager.jumpToPage(1);
|
|
}
|
|
} else {
|
|
renderRows();
|
|
}
|
|
//we're done doing stuff
|
|
|
|
//expose api
|
|
$.extend($gridContainer, {
|
|
gridIronApi: {
|
|
version: "1.0",
|
|
|
|
insertRow: function(data, rowId, insertionIndex) {
|
|
//@todo this insertionIndex is sketchy...
|
|
insertionIndex = insertionIndex === undefined ? rows.length : insertionIndex;
|
|
var row = createRow(data, rowId);
|
|
addRowToArray(row, insertionIndex);
|
|
renderRows();
|
|
$gridContainer.triggerHandler("api.rowAdded", [row]);
|
|
return row;
|
|
},
|
|
|
|
deleteRowById: function(rowId) {
|
|
var row = findRowById(rowId);
|
|
deleteRowById(rowId);
|
|
$gridContainer.triggerHandler("api.rowDeleted", [row]);
|
|
return $gridContainer.gridIronApid;
|
|
},
|
|
|
|
destroy: function() {
|
|
resetRows();
|
|
$gridContainer
|
|
.removeClass(cssScope + "grid ui-widget ui-widget-content ui-corner-top ui-corner-bottom")
|
|
.css("width", "")
|
|
.unbind() //this is vitally important to performance
|
|
.empty(); //this should not be an empty() call, but more specific calls to remove each added element
|
|
},
|
|
|
|
getRowAt: function(rowIndex) {
|
|
return rows[rowIndex];
|
|
},
|
|
|
|
findRowById: findRowById,
|
|
|
|
hasRow: function(rowId) {
|
|
return findRowById(rowId) !== undefined;
|
|
},
|
|
|
|
findRows: function(matcher) {
|
|
return arrayFilter(rows, matcher);
|
|
},
|
|
|
|
findRowByElement: function(element) {
|
|
return arrayFilter(rows, function(row) {
|
|
return row.element.get(0) === element;
|
|
});
|
|
},
|
|
|
|
getRows: function() {
|
|
//should this be a copy?
|
|
return rows;
|
|
},
|
|
|
|
getSelectedRows: function() {
|
|
var selectedRows = [];
|
|
$.each(rows, function(i, row) {
|
|
if (row !== undefined && row.selected && row.element.is(":visible")) {
|
|
selectedRows.push(row);
|
|
}
|
|
});
|
|
|
|
return selectedRows;
|
|
},
|
|
|
|
importData: function(data) {
|
|
loadData(data);
|
|
recordTracker.setCount(data.length);
|
|
return $gridContainer.gridIronApi;
|
|
},
|
|
|
|
//elements
|
|
getDataContainer: function() {
|
|
return $dataContainer;
|
|
},
|
|
|
|
getRowHeight: function() {
|
|
return actualRowHeight;
|
|
},
|
|
|
|
//controllers
|
|
ajax: ajax,
|
|
recordTracker: recordTracker,
|
|
pager: pager,
|
|
overlay: overlay
|
|
}
|
|
});
|
|
|
|
return $gridContainer;
|
|
};
|
|
|
|
$.extend($.fn, { gridIron: gridIron });
|
|
|
|
}(jQuery, window)); |