UNPKG

slickgrid

Version:

A lightning fast JavaScript grid/spreadsheet

1,415 lines (1,242 loc) 133 kB
/** * @license * (c) 2009-2016 Michael Leibman * michael{dot}leibman{at}gmail{dot}com * http://github.com/mleibman/slickgrid * * Distributed under MIT license. * All rights reserved. * * SlickGrid v2.3 * * 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 new Error("SlickGrid requires jquery module to be loaded"); } if (!jQuery.fn.drag) { throw new Error("SlickGrid requires jquery.event.drag module to be loaded"); } if (typeof Slick === "undefined") { throw new Error("slick.core.js not loaded"); } (function ($) { // Slick.Grid $.extend(true, window, { Slick: { Grid: SlickGrid } }); // shared across all grids on the page var scrollbarDimensions; var maxSupportedCssHeight; // browser's breaking point ////////////////////////////////////////////////////////////////////////////////////////////// // SlickGrid class implementation (available as Slick.Grid) /** * Creates a new instance of the grid. * @class SlickGrid * @constructor * @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) { // settings var defaults = { alwaysShowVerticalScroll: false, explicitInitialization: false, rowHeight: 25, defaultColumnWidth: 80, enableAddRow: false, leaveSpaceForNewRows: false, editable: false, autoEdit: true, suppressActiveCellChangeOnEdit: false, enableCellNavigation: true, enableColumnReorder: true, asyncEditorLoading: false, asyncEditorLoadDelay: 100, forceFitColumns: false, enableAsyncPostRender: false, asyncPostRenderDelay: 50, enableAsyncPostRenderCleanup: false, asyncPostRenderCleanupDelay: 40, autoHeight: false, editorLock: Slick.GlobalEditorLock, showHeaderRow: false, headerRowHeight: 25, createFooterRow: false, showFooterRow: false, footerRowHeight: 25, createPreHeaderPanel: false, showPreHeaderPanel: false, preHeaderPanelHeight: 25, showTopPanel: false, topPanelHeight: 25, formatterFactory: null, editorFactory: null, cellFlashingCssClass: "flashing", selectedCellCssClass: "selected", multiSelect: true, enableTextSelectionOnCells: false, dataItemColumnValueExtractor: null, fullWidthRows: false, multiColumnSort: false, numberedMultiColumnSort: false, tristateMultiColumnSort: false, sortColNumberInSeparateSpan: false, defaultFormatter: defaultFormatter, forceSyncScrolling: false, addNewRowCssClass: "new-row", preserveCopiedSelectionOnPaste: false, showCellSelection: true, viewportClass: null, minRowBuffer: 3, emulatePagingWhenScrolling: true, // when scrolling off bottom of viewport, place new row at top of viewport editorCellNavOnLRKeys: false }; var columnDefaults = { name: "", resizable: true, sortable: false, minWidth: 30, rerenderOnResize: false, headerCssClass: null, defaultSortAsc: true, focusable: true, selectable: true }; // scroller 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 vScrollDir = 1; // private var initialized = false; var $container; var uid = "slickgrid_" + Math.round(1000000 * Math.random()); var self = this; var $focusSink, $focusSink2; var $headerScroller; var $headers; var $headerRow, $headerRowScroller, $headerRowSpacer; var $footerRow, $footerRowScroller, $footerRowSpacer; var $preHeaderPanel, $preHeaderPanelScroller, $preHeaderPanelSpacer; var $topPanelScroller; var $topPanel; var $viewport; var $canvas; var $style; var $boundAncestors; var stylesheet, columnCssRulesL, columnCssRulesR; var viewportH, viewportW; var canvasWidth; var viewportHasHScroll, viewportHasVScroll; var headerColumnWidthDiff = 0, headerColumnHeightDiff = 0, // border+padding cellWidthDiff = 0, cellHeightDiff = 0, jQueryNewWidthBehaviour = false; var absoluteColumnMinWidth; var tabbingDirection = 1; 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 lastRenderedScrollLeft = 0; var prevScrollLeft = 0; var scrollLeft = 0; var selectionModel; var selectedRows = []; var plugins = []; var cellCssClasses = {}; var columnsById = {}; var sortColumns = []; var columnPosLeft = []; var columnPosRight = []; var pagingActive = false; var pagingIsLastPage = false; var scrollThrottle = ActionThrottle(render, 50); // async call handles var h_editorLoader = null; var h_render = null; var h_postrender = null; var h_postrenderCleanup = null; var postProcessedRows = {}; var postProcessToRow = null; var postProcessFromRow = null; var postProcessedCleanupQueue = []; var postProcessgroupId = 0; // perf counters var counter_rows_rendered = 0; var counter_rows_removed = 0; // These two variables work around a bug with inertial scrolling in Webkit/Blink on Mac. // See http://crbug.com/312427. var rowNodeFromLastMouseWheelEvent; // this node must not be deleted while inertial scrolling var zombieRowNodeFromLastMouseWheelEvent; // node that was hidden instead of getting deleted var zombieRowCacheFromLastMouseWheelEvent; // row cache for above node var zombieRowPostProcessedFromLastMouseWheelEvent; // post processing references for above node // store css attributes if display:none is active in container or parent var cssShow = { position: 'absolute', visibility: 'hidden', display: 'block' }; var $hiddenParents; var oldProps = []; var columnResizeDragging = false; ////////////////////////////////////////////////////////////////////////////////////////////// // Initialization function init() { if (container instanceof jQuery) { $container = container; } else { $container = $(container); } if ($container.length < 1) { throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM."); } cacheCssForHiddenInit(); // calculate these only once and share between grid instances maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight(); options = $.extend({}, defaults, options); validateAndEnforceOptions(); columnDefaults.width = options.defaultColumnWidth; columnsById = {}; for (var i = 0; i < columns.length; i++) { var m = columns[i] = $.extend({}, columnDefaults, columns[i]); columnsById[m.id] = i; if (m.minWidth && m.width < m.minWidth) { m.width = m.minWidth; } if (m.maxWidth && m.width > m.maxWidth) { m.width = m.maxWidth; } } // 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() .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"); } $focusSink = $("<div tabIndex='0' hideFocus style='position:fixed;width:0;height:0;top:0;left:0;outline:0;'></div>").appendTo($container); if (options.createPreHeaderPanel) { $preHeaderPanelScroller = $("<div class='slick-preheader-panel ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container); $preHeaderPanel = $("<div />").appendTo($preHeaderPanelScroller); $preHeaderPanelSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>") .appendTo($preHeaderPanelScroller); if (!options.showPreHeaderPanel) { $preHeaderPanelScroller.hide(); } } $headerScroller = $("<div class='slick-header ui-state-default' />").appendTo($container); $headers = $("<div class='slick-header-columns' style='left:-1000px' />").appendTo($headerScroller); $headerRowScroller = $("<div class='slick-headerrow ui-state-default' />").appendTo($container); $headerRow = $("<div class='slick-headerrow-columns' />").appendTo($headerRowScroller); $headerRowSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>") .appendTo($headerRowScroller); $topPanelScroller = $("<div class='slick-top-panel-scroller ui-state-default' />").appendTo($container); $topPanel = $("<div class='slick-top-panel' style='width:10000px' />").appendTo($topPanelScroller); if (!options.showTopPanel) { $topPanelScroller.hide(); } if (!options.showHeaderRow) { $headerRowScroller.hide(); } $viewport = $("<div class='slick-viewport' style='width:100%;overflow:auto;outline:0;position:relative;;'>").appendTo($container); $viewport.css("overflow-y", options.alwaysShowVerticalScroll ? "scroll" : (options.autoHeight ? "hidden" : "auto")); $viewport.css("overflow-x", options.forceFitColumns ? "hidden" : "auto"); if (options.viewportClass) $viewport.toggleClass(options.viewportClass, true); $canvas = $("<div class='grid-canvas' />").appendTo($viewport); scrollbarDimensions = scrollbarDimensions || measureScrollbar(); if ($preHeaderPanelSpacer) $preHeaderPanelSpacer.css("width", getCanvasWidth() + scrollbarDimensions.width + "px"); $headers.width(getHeadersWidth()); $headerRowSpacer.css("width", getCanvasWidth() + scrollbarDimensions.width + "px"); if (options.createFooterRow) { $footerRowScroller = $("<div class='slick-footerrow ui-state-default' />").appendTo($container); $footerRow = $("<div class='slick-footerrow-columns' />").appendTo($footerRowScroller); $footerRowSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>") .css("width", getCanvasWidth() + scrollbarDimensions.width + "px") .appendTo($footerRowScroller); if (!options.showFooterRow) { $footerRowScroller.hide(); } } $focusSink2 = $focusSink.clone().appendTo($container); if (!options.explicitInitialization) { finishInitialization(); } } function finishInitialization() { if (!initialized) { initialized = true; viewportW = parseFloat($.css($container[0], "width", true)); // 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) if (!options.enableTextSelectionOnCells) { // disable text selection in grid cells except in input and textarea elements // (this is IE-specific, because selectstart event will only fire in IE) $viewport.on("selectstart.ui", function (event) { return $(event.target).is("input,textarea"); }); } updateColumnCaches(); createColumnHeaders(); setupColumnSort(); createCssRules(); resizeCanvas(); bindAncestorScrollEvents(); $container .on("resize.slickgrid", resizeCanvas); $viewport //.on("click", handleClick) .on("scroll", handleScroll); $headerScroller //.on("scroll", handleHeaderScroll) .on("contextmenu", handleHeaderContextMenu) .on("click", handleHeaderClick) .on("mouseenter", ".slick-header-column", handleHeaderMouseEnter) .on("mouseleave", ".slick-header-column", handleHeaderMouseLeave); $headerRowScroller .on("scroll", handleHeaderRowScroll); if (options.createFooterRow) { $footerRowScroller .on("scroll", handleFooterRowScroll); } if (options.createPreHeaderPanel) { $preHeaderPanelScroller .on("scroll", handlePreHeaderPanelScroll); } $focusSink.add($focusSink2) .on("keydown", handleKeyDown); $canvas .on("keydown", handleKeyDown) .on("click", handleClick) .on("dblclick", handleDblClick) .on("contextmenu", handleContextMenu) .on("draginit", handleDragInit) .on("dragstart", {distance: 3}, handleDragStart) .on("drag", handleDrag) .on("dragend", handleDragEnd) .on("mouseenter", ".slick-cell", handleMouseEnter) .on("mouseleave", ".slick-cell", handleMouseLeave); // Work around http://crbug.com/312427. if (navigator.userAgent.toLowerCase().match(/webkit/) && navigator.userAgent.toLowerCase().match(/macintosh/)) { $canvas.on("mousewheel", handleMouseWheel); } restoreCssFromHiddenInit(); } } function cacheCssForHiddenInit() { // handle display:none on container or container parents $hiddenParents = $container.parents().addBack().not(':visible'); $hiddenParents.each(function() { var old = {}; for ( var name in cssShow ) { old[ name ] = this.style[ name ]; this.style[ name ] = cssShow[ name ]; } oldProps.push(old); }); } function restoreCssFromHiddenInit() { // finish handle display:none on container or container parents // - put values back the way they were $hiddenParents.each(function(i) { var old = oldProps[i]; for ( var name in cssShow ) { this.style[ name ] = old[ name ]; } }); } 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; if (selectionModel) { selectionModel.init(self); selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged); } } function getSelectionModel() { return selectionModel; } function getCanvasNode() { return $canvas[0]; } function measureScrollbar() { var $outerdiv = $('<div class="' + $viewport.className + '" style="position:absolute; top:-10000px; left:-10000px; overflow:auto; width:100px; height:100px;"></div>').appendTo($viewport); var $innerdiv = $('<div style="width:200px; height:200px; overflow:auto;"></div>').appendTo($outerdiv); var dim = { width: $outerdiv[0].offsetWidth - $outerdiv[0].clientWidth, height: $outerdiv[0].offsetHeight - $outerdiv[0].clientHeight }; $innerdiv.remove(); $outerdiv.remove(); return dim; } function getColumnTotalWidth(includeScrollbar) { var totalWidth = 0; for (var i = 0, ii = columns.length; i < ii; i++) { var width = columns[i].width; totalWidth += width; } if (includeScrollbar) { totalWidth += scrollbarDimensions.width; } return totalWidth; } function getHeadersWidth() { var headersWidth = getColumnTotalWidth(!options.autoHeight); return Math.max(headersWidth, viewportW) + 1000; } function getCanvasWidth() { var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW; var rowWidth = 0; var i = columns.length; while (i--) { rowWidth += columns[i].width; } return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth; } function updateCanvasWidth(forceColumnWidthsUpdate) { var oldCanvasWidth = canvasWidth; canvasWidth = getCanvasWidth(); if (canvasWidth != oldCanvasWidth) { $canvas.width(canvasWidth); $headerRow.width(canvasWidth); if (options.createFooterRow) { $footerRow.width(canvasWidth); } if (options.createPreHeaderPanel) { $preHeaderPanel.width(canvasWidth); } $headers.width(getHeadersWidth()); viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width); } var w=canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0); $headerRowSpacer.width(w); if (options.createFooterRow) { $footerRowSpacer.width(w); } if (options.createPreHeaderPanel) { $preHeaderPanelSpacer.width(w); } if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) { applyColumnWidths(); } } function disableSelection($target) { if ($target && $target.jquery) { $target .attr("unselectable", "on") .css("MozUserSelect", "none") .on("selectstart.ui", function () { return false; }); // from jquery:ui.core.js 1.7.2 } } function getMaxSupportedCssHeight() { var supportedHeight = 1000000; // FF reports the height back but still renders blank after ~6M px var testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000; var div = $("<div style='display:none' />").appendTo(document.body); while (true) { var test = supportedHeight * 2; div.css("height", test); if (test > testUpTo || div.height() !== test) { break; } else { supportedHeight = test; } } div.remove(); return supportedHeight; } function getUID() { return uid; } function getHeaderColumnWidthDiff() { return headerColumnWidthDiff; } function getScrollbarDimensions() { return scrollbarDimensions; } // TODO: this is static. need to handle page mutation. function bindAncestorScrollEvents() { var elem = $canvas[0]; while ((elem = elem.parentNode) != document.body && elem != null) { // bind to scroll containers only if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight) { var $elem = $(elem); if (!$boundAncestors) { $boundAncestors = $elem; } else { $boundAncestors = $boundAncestors.add($elem); } $elem.on("scroll." + uid, handleActiveCellPositionChange); } } } function unbindAncestorScrollEvents() { if (!$boundAncestors) { return; } $boundAncestors.off("scroll." + uid); $boundAncestors = null; } function updateColumnHeader(columnId, title, toolTip) { if (!initialized) { return; } var idx = getColumnIndex(columnId); if (idx == null) { return; } var columnDef = columns[idx]; var $header = $headers.children().eq(idx); if ($header) { if (title !== undefined) { columns[idx].name = title; } if (toolTip !== undefined) { columns[idx].toolTip = toolTip; } trigger(self.onBeforeHeaderCellDestroy, { "node": $header[0], "column": columnDef, "grid": self }); $header .attr("title", toolTip || "") .children().eq(0).html(title); trigger(self.onHeaderCellRendered, { "node": $header[0], "column": columnDef, "grid": self }); } } function getHeader() { return $headers[0]; } function getHeaderColumn(columnIdOrIdx) { var idx = (typeof columnIdOrIdx === "number" ? columnIdOrIdx : getColumnIndex(columnIdOrIdx)); var $rtn = $headers.children().eq(idx); return $rtn && $rtn[0]; } function getHeaderRow() { return $headerRow[0]; } function getFooterRow() { return $footerRow[0]; } function getPreHeaderPanel() { return $preHeaderPanel[0]; } function getHeaderRowColumn(columnIdOrIdx) { var idx = (typeof columnIdOrIdx === "number" ? columnIdOrIdx : getColumnIndex(columnIdOrIdx)); var $rtn = $headerRow.children().eq(idx); return $rtn && $rtn[0]; } function getFooterRowColumn(columnIdOrIdx) { var idx = (typeof columnIdOrIdx === "number" ? columnIdOrIdx : getColumnIndex(columnIdOrIdx)); var $rtn = $footerRow.children().eq(idx); return $rtn && $rtn[0]; } function createColumnHeaders() { function onMouseEnter() { $(this).addClass("ui-state-hover"); } function onMouseLeave() { $(this).removeClass("ui-state-hover"); } $headers.find(".slick-header-column") .each(function() { var columnDef = $(this).data("column"); if (columnDef) { trigger(self.onBeforeHeaderCellDestroy, { "node": this, "column": columnDef, "grid": self }); } }); $headers.empty(); $headers.width(getHeadersWidth()); $headerRow.find(".slick-headerrow-column") .each(function() { var columnDef = $(this).data("column"); if (columnDef) { trigger(self.onBeforeHeaderRowCellDestroy, { "node": this, "column": columnDef, "grid": self }); } }); $headerRow.empty(); if (options.createFooterRow) { $footerRow.find(".slick-footerrow-column") .each(function() { var columnDef = $(this).data("column"); if (columnDef) { trigger(self.onBeforeFooterRowCellDestroy, { "node": this, "column": columnDef }); } }); $footerRow.empty(); } for (var i = 0; i < columns.length; i++) { var m = columns[i]; var header = $("<div class='ui-state-default slick-header-column' />") .html("<span class='slick-column-name'>" + m.name + "</span>") .width(m.width - headerColumnWidthDiff) .attr("id", "" + uid + m.id) .attr("title", m.toolTip || "") .data("column", m) .addClass(m.headerCssClass || "") .appendTo($headers); if (options.enableColumnReorder || m.sortable) { header .on('mouseenter', onMouseEnter) .on('mouseleave', onMouseLeave); } if (m.sortable) { header.addClass("slick-header-sortable"); header.append("<span class='slick-sort-indicator" + (options.numberedMultiColumnSort && !options.sortColNumberInSeparateSpan ? " slick-sort-indicator-numbered" : "" ) + "' />"); if (options.numberedMultiColumnSort && options.sortColNumberInSeparateSpan) { header.append("<span class='slick-sort-indicator-numbered' />"); } } trigger(self.onHeaderCellRendered, { "node": header[0], "column": m, "grid": self }); if (options.showHeaderRow) { var headerRowCell = $("<div class='ui-state-default slick-headerrow-column l" + i + " r" + i + "'></div>") .data("column", m) .appendTo($headerRow); trigger(self.onHeaderRowCellRendered, { "node": headerRowCell[0], "column": m, "grid": self }); } if (options.createFooterRow && options.showFooterRow) { var footerRowCell = $("<div class='ui-state-default slick-footerrow-column l" + i + " r" + i + "'></div>") .data("column", m) .appendTo($footerRow); trigger(self.onFooterRowCellRendered, { "node": footerRowCell[0], "column": m }); } } setSortColumns(sortColumns); setupColumnResize(); if (options.enableColumnReorder) { if (typeof options.enableColumnReorder == 'function') { options.enableColumnReorder(self, $headers, headerColumnWidthDiff, setColumns, setupColumnResize, columns, getColumnIndex, uid, trigger); } else { setupColumnReorder(); } } } function setupColumnSort() { $headers.click(function (e) { if (columnResizeDragging) return; // temporary workaround for a bug in jQuery 1.7.1 (http://bugs.jquery.com/ticket/11328) e.metaKey = e.metaKey || e.ctrlKey; if ($(e.target).hasClass("slick-resizable-handle")) { return; } var $col = $(e.target).closest(".slick-header-column"); if (!$col.length) { return; } var column = $col.data("column"); if (column.sortable) { if (!getEditorLock().commitCurrentEdit()) { return; } var sortColumn = null; var i = 0; for (; i < sortColumns.length; i++) { if (sortColumns[i].columnId == column.id) { sortColumn = sortColumns[i]; sortColumn.sortAsc = !sortColumn.sortAsc; break; } } var hadSortCol = !!sortColumn; if (options.tristateMultiColumnSort) { if (!sortColumn) { sortColumn = { columnId: column.id, sortAsc: column.defaultSortAsc }; } if (hadSortCol && sortColumn.sortAsc) { // three state: remove sort rather than go back to ASC sortColumns.splice(i, 1); sortColumn = null; } if (!options.multiColumnSort) { sortColumns = []; } if (sortColumn && (!hadSortCol || !options.multiColumnSort)) { sortColumns.push(sortColumn); } } else { // legacy behaviour if (e.metaKey && options.multiColumnSort) { if (sortColumn) { sortColumns.splice(i, 1); } } else { if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) { sortColumns = []; } if (!sortColumn) { sortColumn = { columnId: column.id, sortAsc: column.defaultSortAsc }; sortColumns.push(sortColumn); } else if (sortColumns.length == 0) { sortColumns.push(sortColumn); } } } setSortColumns(sortColumns); if (!options.multiColumnSort) { trigger(self.onSort, { multiColumnSort: false, sortCol: (sortColumns.length > 0 ? column : null), sortAsc: (sortColumns.length > 0 ? sortColumns[0].sortAsc : true) }, e); } else { trigger(self.onSort, { multiColumnSort: true, sortCols: $.map(sortColumns, function(col) { return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc }; }) }, e); } } }); } function setupColumnReorder() { $headers.filter(":ui-sortable").sortable("destroy"); $headers.sortable({ containment: "parent", distance: 3, axis: "x", cursor: "default", tolerance: "intersection", helper: "clone", placeholder: "slick-sortable-placeholder ui-state-default slick-header-column", start: function (e, ui) { ui.placeholder.width(ui.helper.outerWidth() - headerColumnWidthDiff); $(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 < reorderedIds.length; i++) { reorderedColumns.push(columns[getColumnIndex(reorderedIds[i].replace(uid, ""))]); } setColumns(reorderedColumns); trigger(self.onColumnsReordered, {}); e.stopPropagation(); setupColumnResize(); } }); } function setupColumnResize() { var $col, j, c, pageX, columnElements, minPageX, maxPageX, firstResizable, lastResizable; columnElements = $headers.children(); columnElements.find(".slick-resizable-handle").remove(); columnElements.each(function (i, e) { if (i >= columns.length) { return; } if (columns[i].resizable) { if (firstResizable === undefined) { firstResizable = i; } lastResizable = i; } }); if (firstResizable === undefined) { return; } columnElements.each(function (i, e) { if (i >= columns.length) { return; } if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) { return; } $col = $(e); $("<div class='slick-resizable-handle' />") .appendTo(e) .on("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) { if (i >= columns.length) { return; } columns[i].previousWidth = $(e).outerWidth(); }); if (options.forceFitColumns) { shrinkLeewayOnRight = 0; stretchLeewayOnRight = 0; // colums on right affect maxPageX/minPageX for (j = i + 1; j < columns.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); }) .on("drag", function (e, dd) { columnResizeDragging = true; var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX, x; 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 < columns.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 { // 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 < columns.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; } } } } } applyColumnHeaderWidths(); if (options.syncColumnCellResize) { applyColumnWidths(); } }) .on("dragend", function (e, dd) { var newWidth; $(this).parent().removeClass("slick-header-column-active"); for (j = 0; j < columns.length; j++) { c = columns[j]; newWidth = $(columnElements[j]).outerWidth(); if (c.previousWidth !== newWidth && c.rerenderOnResize) { invalidateAllRows(); } } updateCanvasWidth(true); render(); trigger(self.onColumnsResized, {}); setTimeout(function () { columnResizeDragging = false; }, 300); }); }); } 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"]; // jquery prior to version 1.8 handles .width setter/getter as a direct css write/read // jquery 1.8 changed .width to read the true inner element width if box-sizing is set to border-box, and introduced a setter for .outerWidth // so for equivalent functionality, prior to 1.8 use .width, and after use .outerWidth var verArray = $.fn.jquery.split('.'); jQueryNewWidthBehaviour = (verArray[0]==1 && verArray[1]>=8) || verArray[0] >=2; el = $("<div class='ui-state-default slick-header-column' style='visibility:hidden'>-</div>").appendTo($headers); headerColumnWidthDiff = headerColumnHeightDiff = 0; if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") { $.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 = $("<div class='slick-row' />").appendTo($canvas); el = $("<div class='slick-cell' id='' style='visibility:hidden'>-</div>").appendTo(r); cellWidthDiff = cellHeightDiff = 0; if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") { $.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 = $("<style type='text/css' rel='stylesheet' />").appendTo($("head")); var rowHeight = (options.rowHeight - cellHeightDiff); var rules = [ "." + uid + " .slick-header-column { left: 1000px; }", "." + uid + " .slick-top-panel { height:" + options.topPanelHeight + "px; }", "." + uid + " .slick-preheader-panel { height:" + options.preHeaderPanelHeight + "px; }", "." + uid + " .slick-headerrow-columns { height:" + options.headerRowHeight + "px; }", "." + uid + " .slick-footerrow-columns { height:" + options.footerRowHeight + "px; }", "." + uid + " .slick-cell { height:" + rowHeight + "px; }", "." + uid + " .slick-row { height:" + options.rowHeight + "px; }" ]; for (var i = 0; i < columns.length; i++) { rules.push("." + uid + " .l" + i + " { }"); rules.push("." + uid + " .r" + i + " { }"); } if ($style[0].styleSheet) { // IE $style[0].styleSheet.cssText = rules.join(" "); } else { $style[0].appendChild(document.createTextNode(rules.join(" "))); } } function getColumnCssRules(idx) { var i; if (!stylesheet) { var sheets = document.styleSheets; for (i = 0; i < sheets.length; i++) { if ((sheets[i].ownerNode || sheets[i].owningElement) == $style[0]) { stylesheet = sheets[i]; break; } } if (!stylesheet) { throw new Error("Cannot find stylesheet."); } // find and cache column CSS rules columnCssRulesL = []; columnCssRulesR = []; var cssRules = (stylesheet.cssRules || stylesheet.rules); var matches, columnIdx; for (i = 0; i < cssRules.length; i++) { var selector = cssRules[i].selectorText; if (matches = /\.l\d+/.exec(selector)) { columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10); columnCssRulesL[columnIdx] = cssRules[i]; } else if (matches = /\.r\d+/.exec(selector)) { columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10); columnCssRulesR[columnIdx] = cssRules[i]; } } } return { "left": columnCssRulesL[idx], "right": columnCssRulesR[idx] }; } function removeCssRules() { $style.remove(); stylesheet = null; } function destroy() { getEditorLock().cancelCurrentEdit(); trigger(self.onBeforeDestroy, {}); var i = plugins.length; while(i--) { unregisterPlugin(plugins[i]); } if (options.enableColumnReorder) { $headers.filter(":ui-sortable").sortable("destroy"); } unbindAncestorScrollEvents(); $container.off(".slickgrid"); removeCssRules(); $canvas.off("draginit dragstart dragend drag"); $container.empty().removeClass(uid); } ////////////////////////////////////////////////////////////////////////////////////////////// // General function trigger(evt, args, e) { e = e || new Slick.EventData(); args = args || {}; args.grid = self; return evt.notify(args, e, self); } function getEditorLock() { return options.editorLock; } function getEditController() { return editController; } function getColumnIndex(id) { return columnsById[id]; } function autosizeColumns() { var i, c, widths = [], shrinkLeeway = 0, total = 0, prevTotal, availWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW; for (i = 0; i < columns.length; i++) { c = columns[i]; widths.push(c.width); total += c.width; if (c.resizable) { shrinkLeeway += c.width - Math.max(c.minWidth, absoluteColumnMinWidth); } } // shrink prevTotal = total; while (total > availWidth && shrinkLeeway) { var shrinkProportion = (total - availWidth) / shrinkLeeway; for (i = 0; i < columns.length && total > availWidth; i++) { c = columns[i]; var width = widths[i]; if (!c.resizable || width <= c.minWidth || width <= absoluteColumnMinWidth) { continue; } var absMinWidth = Math.max(c.minWidth, absoluteColumnMinWidth); var shrinkSize = Math.floor(shrinkProportion * (width - absMinWidth)) || 1; shrinkSize = Math.min(shrinkSize, width - absMinWidth); total -= shrinkSize; shrinkLeeway -= shrinkSize; widths[i] -= shrinkSize; } if (prevTotal <= total) { // avoid infinite loop break; } prevTotal = total; } // grow prevTotal = total; while (total < availWidth) { var growProportion = availWidth / total; for (i = 0; i < columns.length && total < availWidth; i++) { c = columns[i]; var currentWidth = widths[i]; var growSize; if (!c.resizable || c.maxWidth <= currentWidth) { growSize = 0; } else { growSize = Math.min(Math.floor(growProportion * currentWidth) - currentWidth, (c.maxWidth - currentWidth) || 1000000) || 1; } total += growSize; widths[i] += (total <= availWidth ? growSize : 0); } if (prevTotal >= total) { // avoid infinite loop break; } prevTotal = total; } var reRender = false; for (i = 0; i < columns.length; i++) { if (columns[i].rerenderOnResize && columns[i].width != widths[i]) { reRender = true; } columns[i].width = widths[i]; } applyColumnHeaderWidths(); updateCanvasWidth(true); trigger(self.onAutosizeColumns, { "columns": columns}); if (reRender) { invalidateAllRows(); render(); } } function applyColumnHeaderWidths() { if (!initialized) { return; } var h; for (var i = 0, headers = $headers.children(), ii = columns.length; i < ii; i++) { h = $(headers[i]); if (jQueryNewWidthBehaviour) { if (h.outerWidth() !== columns[i].width) { h.outerWidth(columns[i].width); } } else { if (h.width() !== columns[i].width - headerColumnWidthDiff) { h.width(columns[i].width - headerColumnWidthDiff); } } } updateColumnCaches(); } function applyColumnWidths() { var x = 0, w, rule; for (var i = 0; i < columns.length; i++) { w = columns[i].width; rule = getColumnCssRules(i); rule.left.style.left = x + "px"; rule.right.style.right = (canvasWidth - x - w) + "px"; x += columns[i].width; } } function setSortColumn(columnId, ascending) { setSortColumns([{ columnId: columnId, sortAsc: ascending}]); } function setSortColumns(cols) { sortColumns = cols; var numberCols = options.numberedMultiColumnSort && sortColumns.length > 1; var headerColumnEls = $headers.children(); headerColumnEls .removeClass("slick-header-column-sorted") .find(".slick-sort-indicator") .removeClass("slick-sort-indicator-asc slick-sort-indicator-desc"); headerColumnEls .find(".slick-sort-indicator-numbered") .text(''); $.each(sortColumns, function(i, col) { if (col.sortAsc == null) { col.sortAsc = true; } var columnIndex = getColumnIndex(col.columnId); if (columnIndex != null) { headerColumnEls.eq(columnIndex) .addClass("slick-header-column-sorted") .find(".slick-sort-indicator") .addClass(col.sortAsc ? "slick-sort-indicator-asc" : "slick-sort-indicator-desc"); if (numberCols) { headerColumnEls.eq(columnIndex) .find(".slick-sort-indicator-numbered") .text(i+1); } } }); } function getSortColumns() { return sortColumns; } function handleSelectedRangesChanged(e, ranges) { selectedRows = []; var hash = {}; for (var i = 0; i < ranges.length; i++