/** * @license * (c) 2009-2010 Michael Leibman * michael{dot}leibman{at}gmail{dot}com * http://github.com/mleibman/slickgrid * Distributed under MIT license. * All rights reserved. * * SlickGrid v2.0 alpha * * NOTES: * Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods. * This increases the speed dramatically, but can only be done safely because there are no event handlers * or data associated with any cell/row DOM nodes. Cell editors must make sure they implement .destroy() * and do proper cleanup. */ // make sure required JavaScript modules are loaded if (typeof jQuery === "undefined") { throw "SlickGrid requires jquery module to be loaded"; } if (!jQuery.fn.drag) { throw "SlickGrid requires jquery.event.drag module to be loaded"; } if (typeof Slick === "undefined") { throw "slick.core.js not loaded"; } (function($) { // Slick.Grid $.extend(true, window, { Slick: { Grid: SlickGrid } }); var scrollbarDimensions; // shared across all grids on this page ////////////////////////////////////////////////////////////////////////////////////////////// // SlickGrid class implementation (available as Slick.Grid) /** * @param {Node} container Container node to create the grid in. * @param {Array,Object} data An array of objects for databinding. * @param {Array} columns An array of column definitions. * @param {Object} options Grid options. **/ function SlickGrid(container,data,columns,options) { /// /// Create and manage virtual grid in the specified $container, /// connecting it to the specified data source. Data is presented /// as a grid with the specified columns and data.length rows. /// Options alter behaviour of the grid. /// // settings var defaults = { headerHeight: 25, rowHeight: 25, defaultColumnWidth: 80, enableAddRow: false, leaveSpaceForNewRows: false, editable: false, autoEdit: true, enableCellNavigation: true, enableCellRangeSelection: false, enableColumnReorder: true, asyncEditorLoading: false, asyncEditorLoadDelay: 100, forceFitColumns: false, enableAsyncPostRender: false, asyncPostRenderDelay: 60, autoHeight: false, editorLock: Slick.GlobalEditorLock, showHeaderRow: false, headerRowHeight: 25, showTopPanel: false, topPanelHeight: 25, formatterFactory: null, editorFactory: null, cellFlashingCssClass: "flashing", selectedCellCssClass: "selected", multiSelect: true }; var columnDefaults = { name: "", resizable: true, sortable: false, minWidth: 30, rerenderOnResize: false, headerCssClass: null }; // scroller var maxSupportedCssHeight; // browser's breaking point var th; // virtual height var h; // real scrollable height var ph; // page height var n; // number of pages var cj; // "jumpiness" coefficient var page = 0; // current page var offset = 0; // current page offset var scrollDir = 1; // private var $container; var uid = "slickgrid_" + Math.round(1000000 * Math.random()); var self = this; var $headerScroller; var $headers; var $headerRow, $headerRowScroller; var $topPanelScroller; var $topPanel; var $viewport; var $canvas; var $style; var stylesheet; var viewportH, viewportW; var viewportHasHScroll; var headerColumnWidthDiff, headerColumnHeightDiff, cellWidthDiff, cellHeightDiff; // padding+border var absoluteColumnMinWidth; var activePosX; var activeRow, activeCell; var activeCellNode = null; var currentEditor = null; var serializedEditorValue; var editController; var rowsCache = {}; var renderedRows = 0; var numVisibleRows; var prevScrollTop = 0; var scrollTop = 0; var lastRenderedScrollTop = 0; var prevScrollLeft = 0; var avgRowRenderTime = 10; var selectionModel; var selectedRows = []; var plugins = []; var cellCssClasses = {}; var columnsById = {}; var sortColumnId; var sortAsc = true; // async call handles var h_editorLoader = null; var h_render = null; var h_postrender = null; var postProcessedRows = {}; var postProcessToRow = null; var postProcessFromRow = null; // perf counters var counter_rows_rendered = 0; var counter_rows_removed = 0; ////////////////////////////////////////////////////////////////////////////////////////////// // Initialization function init() { /// /// Initialize 'this' (self) instance of a SlickGrid. /// This function is called by the constructor. /// $container = $(container); if($container.length < 1) { throw new Error("SlickGrid requires a valid container, "+container+" does not exist in the DOM."); } maxSupportedCssHeight = getMaxSupportedCssHeight(); scrollbarDimensions = scrollbarDimensions || measureScrollbar(); // skip measurement if already have dimensions options = $.extend({},defaults,options); columnDefaults.width = options.defaultColumnWidth; // validate loaded JavaScript modules against requested options if (options.enableColumnReorder && !$.fn.sortable) { throw new Error("SlickGrid's \"enableColumnReorder = true\" option requires jquery-ui.sortable module to be loaded"); } editController = { "commitCurrentEdit": commitCurrentEdit, "cancelCurrentEdit": cancelCurrentEdit }; $container .empty() .attr("tabIndex",0) .attr("hideFocus",true) .css("overflow","hidden") .css("outline",0) .addClass(uid) .addClass("ui-widget"); // set up a positioning container if needed if (!/relative|absolute|fixed/.test($container.css("position"))) $container.css("position","relative"); $headerScroller = $("
").appendTo($container); $headers = $("
").appendTo($headerScroller); $headerRowScroller = $("
").appendTo($container); $headerRow = $("
").appendTo($headerRowScroller); $topPanelScroller = $("
").appendTo($container); $topPanel = $("
").appendTo($topPanelScroller); if (!options.showTopPanel) { $topPanelScroller.hide(); } if (!options.showHeaderRow) { $headerRowScroller.hide(); } $viewport = $("
").appendTo($container); $canvas = $("
").appendTo($viewport); // header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?) // calculate the diff so we can set consistent sizes measureCellPaddingAndBorder(); // for usability reasons, all text selection in SlickGrid is disabled // with the exception of input and textarea elements (selection must // be enabled there so that editors work as expected); note that // selection in grid cells (grid body) is already unavailable in // all browsers except IE disableSelection($headers); // disable all text selection in header (including input and textarea) $viewport.bind("selectstart.ui", function (event) { return $(event.target).is("input,textarea"); }); // disable text selection in grid cells except in input and textarea elements (this is IE-specific, because selectstart event will only fire in IE) viewportW = parseFloat($.css($container[0], "width", true)); createColumnHeaders(); setupColumnSort(); createCssRules(); resizeAndRender(); bindAncestorScrollEvents(); $viewport.bind("scroll.slickgrid", handleScroll); $container.bind("resize.slickgrid", resizeAndRender); $headerScroller .bind("contextmenu.slickgrid", handleHeaderContextMenu) .bind("click.slickgrid", handleHeaderClick); $canvas .bind("keydown.slickgrid", handleKeyDown) .bind("click.slickgrid", handleClick) .bind("dblclick.slickgrid", handleDblClick) .bind("contextmenu.slickgrid", handleContextMenu) .bind("draginit", handleDragInit) .bind("dragstart", handleDragStart) .bind("drag", handleDrag) .bind("dragend", handleDragEnd); $canvas.delegate(".slick-cell", "mouseenter", handleMouseEnter); $canvas.delegate(".slick-cell", "mouseleave", handleMouseLeave); } function registerPlugin(plugin) { plugins.unshift(plugin); plugin.init(self); } function unregisterPlugin(plugin) { for (var i = plugins.length; i >= 0; i--) { if (plugins[i] === plugin) { if (plugins[i].destroy) { plugins[i].destroy(); } plugins.splice(i, 1); break; } } } function setSelectionModel(model) { if (selectionModel) { selectionModel.onSelectedRangesChanged.unsubscribe(handleSelectedRangesChanged); if (selectionModel.destroy) { selectionModel.destroy(); } } selectionModel = model; selectionModel.init(self); selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged); } function getSelectionModel() { return selectionModel; } function getCanvasNode() { return $canvas[0]; } function measureScrollbar() { /// /// Measure width of a vertical scrollbar /// and height of a horizontal scrollbar. /// /// { width: pixelWidth, height: pixelHeight } /// var $c = $("
").appendTo("body"); var dim = { width: $c.width() - $c[0].clientWidth, height: $c.height() - $c[0].clientHeight }; $c.remove(); return dim; } function getRowWidth() { var rowWidth = 0; var i = columns.length; while (i--) { rowWidth += (columns[i].width || columnDefaults.width); } return rowWidth; } function setCanvasWidth(width) { $canvas.width(width); viewportHasHScroll = (width > viewportW - scrollbarDimensions.width); } function disableSelection($target) { /// /// Disable text selection (using mouse) in /// the specified target. /// ").appendTo(document.body); while (supportedHeight <= testUpTo) { div.css("height", supportedHeight + increment); if (div.height() !== supportedHeight + increment) break; else supportedHeight += increment; } div.remove(); return supportedHeight; } // TODO: this is static. need to handle page mutation. function bindAncestorScrollEvents() { var elem = $canvas[0]; while ((elem = elem.parentNode) != document.body) { // bind to scroll containers only if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight) $(elem).bind("scroll.slickgrid", handleActiveCellPositionChange); } } function unbindAncestorScrollEvents() { $canvas.parents().unbind("scroll.slickgrid"); } function updateColumnHeader(columnId, title, toolTip) { var idx = getColumnIndex(columnId); var $header = $headers.children().eq(idx); if ($header) { columns[idx].name = title; columns[idx].toolTip = toolTip; $header .attr("title", toolTip || title || "") .children().eq(0).html(title); } } function getHeaderRow() { return $headerRow[0]; } function getHeaderRowColumn(columnId) { var idx = getColumnIndex(columnId); var $header = $headerRow.children().eq(idx); return $header && $header[0]; } function createColumnHeaders() { var i; function hoverBegin() { $(this).addClass("ui-state-hover"); } function hoverEnd() { $(this).removeClass("ui-state-hover"); } $headers.empty(); $headerRow.empty(); columnsById = {}; for (i = 0; i < columns.length; i++) { var m = columns[i] = $.extend({},columnDefaults,columns[i]); columnsById[m.id] = i; var header = $("
") .html("" + m.name + "") .width(m.width - headerColumnWidthDiff) .attr("title", m.toolTip || m.name || "") .data("fieldId", m.id) .addClass(m.headerCssClass || "") .appendTo($headers); if (options.enableColumnReorder || m.sortable) { header.hover(hoverBegin, hoverEnd); } if (m.sortable) { header.append(""); } if (options.showHeaderRow) { $("
").appendTo($headerRow); } } setSortColumn(sortColumnId,sortAsc); setupColumnResize(); if (options.enableColumnReorder) { setupColumnReorder(); } } function setupColumnSort() { $headers.click(function(e) { if ($(e.target).hasClass("slick-resizable-handle")) { return; } var $col = $(e.target).closest(".slick-header-column"); if (!$col.length) return; var column = columns[getColumnIndex($col.data("fieldId"))]; if (column.sortable) { if (!getEditorLock().commitCurrentEdit()) return; if (column.id === sortColumnId) { sortAsc = !sortAsc; } else { sortColumnId = column.id; sortAsc = true; } setSortColumn(sortColumnId,sortAsc); trigger(self.onSort, {sortCol:column,sortAsc:sortAsc}); } }); } function setupColumnReorder() { $headers.sortable({ containment: "parent", axis: "x", cursor: "default", tolerance: "intersection", helper: "clone", placeholder: "slick-sortable-placeholder ui-state-default slick-header-column", forcePlaceholderSize: true, start: function(e, ui) { $(ui.helper).addClass("slick-header-column-active"); }, beforeStop: function(e, ui) { $(ui.helper).removeClass("slick-header-column-active"); }, stop: function(e) { if (!getEditorLock().commitCurrentEdit()) { $(this).sortable("cancel"); return; } var reorderedIds = $headers.sortable("toArray"); var reorderedColumns = []; for (var i=0; i= lastResizable)) { return; } $col = $(e); $("
") .appendTo(e) .bind("dragstart", function(e,dd) { if (!getEditorLock().commitCurrentEdit()) { return false; } pageX = e.pageX; $(this).parent().addClass("slick-header-column-active"); var shrinkLeewayOnRight = null, stretchLeewayOnRight = null; // lock each column's width option to current width columnElements.each(function(i,e) { columns[i].previousWidth = $(e).outerWidth(); }); if (options.forceFitColumns) { shrinkLeewayOnRight = 0; stretchLeewayOnRight = 0; // colums on right affect maxPageX/minPageX for (j = i + 1; j < columnElements.length; j++) { c = columns[j]; if (c.resizable) { if (stretchLeewayOnRight !== null) { if (c.maxWidth) { stretchLeewayOnRight += c.maxWidth - c.previousWidth; } else { stretchLeewayOnRight = null; } } shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth); } } } var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0; for (j = 0; j <= i; j++) { // columns on left only affect minPageX c = columns[j]; if (c.resizable) { if (stretchLeewayOnLeft !== null) { if (c.maxWidth) { stretchLeewayOnLeft += c.maxWidth - c.previousWidth; } else { stretchLeewayOnLeft = null; } } shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth); } } if (shrinkLeewayOnRight === null) { shrinkLeewayOnRight = 100000; } if (shrinkLeewayOnLeft === null) { shrinkLeewayOnLeft = 100000; } if (stretchLeewayOnRight === null) { stretchLeewayOnRight = 100000; } if (stretchLeewayOnLeft === null) { stretchLeewayOnLeft = 100000; } maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft); minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight); originalCanvasWidth = $canvas.width(); }) .bind("drag", function(e,dd) { var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX, x, ci; if (d < 0) { // shrink column x = d; for (j = i; j >= 0; j--) { c = columns[j]; if (c.resizable) { actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth); if (x && c.previousWidth + x < actualMinWidth) { x += c.previousWidth - actualMinWidth; c.width = actualMinWidth; } else { c.width = c.previousWidth + x; x = 0; } } } if (options.forceFitColumns) { x = -d; for (j = i + 1; j < columnElements.length; j++) { c = columns[j]; if (c.resizable) { if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) { x -= c.maxWidth - c.previousWidth; c.width = c.maxWidth; } else { c.width = c.previousWidth + x; x = 0; } } } } else if (options.syncColumnCellResize) { setCanvasWidth(originalCanvasWidth + d); } } else { // stretch column x = d; for (j = i; j >= 0; j--) { c = columns[j]; if (c.resizable) { if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) { x -= c.maxWidth - c.previousWidth; c.width = c.maxWidth; } else { c.width = c.previousWidth + x; x = 0; } } } if (options.forceFitColumns) { x = -d; for (j = i + 1; j < columnElements.length; j++) { c = columns[j]; if (c.resizable) { actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth); if (x && c.previousWidth + x < actualMinWidth) { x += c.previousWidth - actualMinWidth; c.width = actualMinWidth; } else { c.width = c.previousWidth + x; x = 0; } } } } else if (options.syncColumnCellResize) { setCanvasWidth(originalCanvasWidth + d); } } applyColumnHeaderWidths(); if (options.syncColumnCellResize) { applyColumnWidths(); } }) .bind("dragend", function(e,dd) { var newWidth; $(this).parent().removeClass("slick-header-column-active"); for (j = 0; j < columnElements.length; j++) { c = columns[j]; newWidth = $(columnElements[j]).outerWidth(); if (c.previousWidth !== newWidth && c.rerenderOnResize) { invalidateAllRows(); } } applyColumnWidths(); resizeCanvas(); trigger(self.onColumnsResized, {}); }); }); } function getVBoxDelta($el) { var p = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"]; var delta = 0; $.each(p, function(n,val) { delta += parseFloat($el.css(val)) || 0; }); return delta; } function measureCellPaddingAndBorder() { var el; var h = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"]; var v = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"]; el = $("").appendTo($headers); headerColumnWidthDiff = headerColumnHeightDiff = 0; $.each(h, function(n,val) { headerColumnWidthDiff += parseFloat(el.css(val)) || 0; }); $.each(v, function(n,val) { headerColumnHeightDiff += parseFloat(el.css(val)) || 0; }); el.remove(); var r = $("
").appendTo($canvas); el = $("").appendTo(r); cellWidthDiff = cellHeightDiff = 0; $.each(h, function(n,val) { cellWidthDiff += parseFloat(el.css(val)) || 0; }); $.each(v, function(n,val) { cellHeightDiff += parseFloat(el.css(val)) || 0; }); r.remove(); absoluteColumnMinWidth = Math.max(headerColumnWidthDiff,cellWidthDiff); } function createCssRules() { $style = $("