(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 = $("
") .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 = $("").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 = $("") .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 = $("").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 = $("").addClass(cssScope + "footer ui-widget-header ui-corner-bottom ui-helper-clearfix"); if (settings.showRecordCount) { $("") .addClass(cssScope + "record-display") .text("No records") .appendTo($footer); } return $footer; }; var createCaption = function() { var $caption = $("") .addClass(cssScope + "caption ui-state-default ui-corner-top ui-helper-clearfix") .append($("").addClass(cssScope + "title").text(settings.title)); if (settings.showSearchBar) { var $textbox = $("").keypress(function(e) { if (e.which === 13) { submitSearch.call(this); } }); var submitSearch = function() { $gridContainer.triggerHandler("search", [$.trim($textbox.val())]); }; var $icon = $("") .attr("title", "Search") .addClass(cssScope + "search-submit ui-icon ui-icon-search") .click(submitSearch); $("") .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 = $("").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 = $("") .addClass(cssScope + "loading-indicator ui-widget-content ui-corner-all") .append($("").addClass(cssScope + "spinner")) .append($("").addClass(cssScope + "message").text(settings.overlayMessage)) .css("visibility", "hidden"); var position = $dataContainer.position(); $element = $("") .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 = $("") .addClass(cssScope + "data-container") .css({ height: (settings.height === "auto" ? "auto" : settings.height + "px"), "overflow-y": overflowY }).appendTo($gridContainer); $rowContainer = $("").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 = $("") .addClass(cssScope + "pagination-previous ui-icon ui-icon-triangle-1-w ui-state-disabled") .click(pager.previous); var $nextPage = $("") .addClass(cssScope + "pagination-next ui-icon ui-icon-triangle-1-e ui-state-disabled") .click(pager.next); var $pageDisplay = $("").addClass(cssScope + "pagination-page-display"); var $input = $("").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, $("").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));