gridiron/jquery.gridiron.js
2021-01-02 22:16:37 -08:00

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));