3423 lines
		
	
	
		
			106 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			3423 lines
		
	
	
		
			106 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @license
 | |
|  * (c) 2009-2013 Michael Leibman
 | |
|  * michael{dot}leibman{at}gmail{dot}com
 | |
|  * http://github.com/mleibman/slickgrid
 | |
|  *
 | |
|  * Distributed under MIT license.
 | |
|  * All rights reserved.
 | |
|  *
 | |
|  * SlickGrid v2.2
 | |
|  *
 | |
|  * 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
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   // 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 = {
 | |
|       explicitInitialization: false,
 | |
|       rowHeight: 25,
 | |
|       defaultColumnWidth: 80,
 | |
|       enableAddRow: false,
 | |
|       leaveSpaceForNewRows: false,
 | |
|       editable: false,
 | |
|       autoEdit: true,
 | |
|       enableCellNavigation: true,
 | |
|       enableColumnReorder: true,
 | |
|       asyncEditorLoading: false,
 | |
|       asyncEditorLoadDelay: 100,
 | |
|       forceFitColumns: false,
 | |
|       enableAsyncPostRender: false,
 | |
|       asyncPostRenderDelay: 50,
 | |
|       autoHeight: false,
 | |
|       editorLock: Slick.GlobalEditorLock,
 | |
|       showHeaderRow: false,
 | |
|       headerRowHeight: 25,
 | |
|       showTopPanel: false,
 | |
|       topPanelHeight: 25,
 | |
|       formatterFactory: null,
 | |
|       editorFactory: null,
 | |
|       cellFlashingCssClass: "flashing",
 | |
|       selectedCellCssClass: "selected",
 | |
|       multiSelect: true,
 | |
|       enableTextSelectionOnCells: false,
 | |
|       dataItemColumnValueExtractor: null,
 | |
|       fullWidthRows: false,
 | |
|       multiColumnSort: false,
 | |
|       defaultFormatter: defaultFormatter,
 | |
|       forceSyncScrolling: false,
 | |
|       addNewRowCssClass: "new-row"
 | |
|     };
 | |
| 
 | |
|     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 $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;
 | |
|     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 = [];
 | |
| 
 | |
| 
 | |
|     // 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;
 | |
| 
 | |
|     // 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
 | |
| 
 | |
| 
 | |
|     //////////////////////////////////////////////////////////////////////////////////////////////
 | |
|     // Initialization
 | |
| 
 | |
|     function init() {
 | |
|       $container = $(container);
 | |
|       if ($container.length < 1) {
 | |
|         throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM.");
 | |
|       }
 | |
| 
 | |
|       // calculate these only once and share between grid instances
 | |
|       maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight();
 | |
|       scrollbarDimensions = scrollbarDimensions || measureScrollbar();
 | |
| 
 | |
|       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);
 | |
| 
 | |
|       $headerScroller = $("<div class='slick-header ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
 | |
|       $headers = $("<div class='slick-header-columns' style='left:-1000px' />").appendTo($headerScroller);
 | |
|       $headers.width(getHeadersWidth());
 | |
| 
 | |
|       $headerRowScroller = $("<div class='slick-headerrow ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
 | |
|       $headerRow = $("<div class='slick-headerrow-columns' />").appendTo($headerRowScroller);
 | |
|       $headerRowSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>")
 | |
|           .css("width", getCanvasWidth() + scrollbarDimensions.width + "px")
 | |
|           .appendTo($headerRowScroller);
 | |
| 
 | |
|       $topPanelScroller = $("<div class='slick-top-panel-scroller ui-state-default' style='overflow:hidden;position:relative;' />").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.autoHeight ? "hidden" : "auto");
 | |
| 
 | |
|       $canvas = $("<div class='grid-canvas' />").appendTo($viewport);
 | |
| 
 | |
|       $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.bind("selectstart.ui", function (event) {
 | |
|             return $(event.target).is("input,textarea");
 | |
|           });
 | |
|         }
 | |
| 
 | |
|         updateColumnCaches();
 | |
|         createColumnHeaders();
 | |
|         setupColumnSort();
 | |
|         createCssRules();
 | |
|         resizeCanvas();
 | |
|         bindAncestorScrollEvents();
 | |
| 
 | |
|         $container
 | |
|             .bind("resize.slickgrid", resizeCanvas);
 | |
|         $viewport
 | |
|             //.bind("click", handleClick)
 | |
|             .bind("scroll", handleScroll);
 | |
|         $headerScroller
 | |
|             .bind("contextmenu", handleHeaderContextMenu)
 | |
|             .bind("click", handleHeaderClick)
 | |
|             .delegate(".slick-header-column", "mouseenter", handleHeaderMouseEnter)
 | |
|             .delegate(".slick-header-column", "mouseleave", handleHeaderMouseLeave);
 | |
|         $headerRowScroller
 | |
|             .bind("scroll", handleHeaderRowScroll);
 | |
|         $focusSink.add($focusSink2)
 | |
|             .bind("keydown", handleKeyDown);
 | |
|         $canvas
 | |
|             .bind("keydown", handleKeyDown)
 | |
|             .bind("click", handleClick)
 | |
|             .bind("dblclick", handleDblClick)
 | |
|             .bind("contextmenu", handleContextMenu)
 | |
|             .bind("draginit", handleDragInit)
 | |
|             .bind("dragstart", {distance: 3}, handleDragStart)
 | |
|             .bind("drag", handleDrag)
 | |
|             .bind("dragend", handleDragEnd)
 | |
|             .delegate(".slick-cell", "mouseenter", handleMouseEnter)
 | |
|             .delegate(".slick-cell", "mouseleave", handleMouseLeave);
 | |
| 
 | |
|         // Work around http://crbug.com/312427.
 | |
|         if (navigator.userAgent.toLowerCase().match(/webkit/) &&
 | |
|             navigator.userAgent.toLowerCase().match(/macintosh/)) {
 | |
|           $canvas.bind("mousewheel", handleMouseWheel);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     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 $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
 | |
|       var dim = {
 | |
|         width: $c.width() - $c[0].clientWidth,
 | |
|         height: $c.height() - $c[0].clientHeight
 | |
|       };
 | |
|       $c.remove();
 | |
|       return dim;
 | |
|     }
 | |
| 
 | |
|     function getHeadersWidth() {
 | |
|       var headersWidth = 0;
 | |
|       for (var i = 0, ii = columns.length; i < ii; i++) {
 | |
|         var width = columns[i].width;
 | |
|         headersWidth += width;
 | |
|       }
 | |
|       headersWidth += scrollbarDimensions.width;
 | |
|       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);
 | |
|         $headers.width(getHeadersWidth());
 | |
|         viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width);
 | |
|       }
 | |
| 
 | |
|       $headerRowSpacer.width(canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0));
 | |
| 
 | |
|       if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) {
 | |
|         applyColumnWidths();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function disableSelection($target) {
 | |
|       if ($target && $target.jquery) {
 | |
|         $target
 | |
|             .attr("unselectable", "on")
 | |
|             .css("MozUserSelect", "none")
 | |
|             .bind("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;
 | |
|     }
 | |
| 
 | |
|     // 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.bind("scroll." + uid, handleActiveCellPositionChange);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function unbindAncestorScrollEvents() {
 | |
|       if (!$boundAncestors) {
 | |
|         return;
 | |
|       }
 | |
|       $boundAncestors.unbind("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
 | |
|         });
 | |
| 
 | |
|         $header
 | |
|             .attr("title", toolTip || "")
 | |
|             .children().eq(0).html(title);
 | |
| 
 | |
|         trigger(self.onHeaderCellRendered, {
 | |
|           "node": $header[0],
 | |
|           "column": columnDef
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getHeaderRow() {
 | |
|       return $headerRow[0];
 | |
|     }
 | |
| 
 | |
|     function getHeaderRowColumn(columnId) {
 | |
|       var idx = getColumnIndex(columnId);
 | |
|       var $header = $headerRow.children().eq(idx);
 | |
|       return $header && $header[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
 | |
|             });
 | |
|           }
 | |
|         });
 | |
|       $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
 | |
|             });
 | |
|           }
 | |
|         });
 | |
|       $headerRow.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' />");
 | |
|         }
 | |
| 
 | |
|         trigger(self.onHeaderCellRendered, {
 | |
|           "node": header[0],
 | |
|           "column": m
 | |
|         });
 | |
| 
 | |
|         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
 | |
|           });
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       setSortColumns(sortColumns);
 | |
|       setupColumnResize();
 | |
|       if (options.enableColumnReorder) {
 | |
|         setupColumnReorder();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function setupColumnSort() {
 | |
|       $headers.click(function (e) {
 | |
|         // 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 sortOpts = null;
 | |
|           var i = 0;
 | |
|           for (; i < sortColumns.length; i++) {
 | |
|             if (sortColumns[i].columnId == column.id) {
 | |
|               sortOpts = sortColumns[i];
 | |
|               sortOpts.sortAsc = !sortOpts.sortAsc;
 | |
|               break;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           if (e.metaKey && options.multiColumnSort) {
 | |
|             if (sortOpts) {
 | |
|               sortColumns.splice(i, 1);
 | |
|             }
 | |
|           }
 | |
|           else {
 | |
|             if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) {
 | |
|               sortColumns = [];
 | |
|             }
 | |
| 
 | |
|             if (!sortOpts) {
 | |
|               sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc };
 | |
|               sortColumns.push(sortOpts);
 | |
|             } else if (sortColumns.length == 0) {
 | |
|               sortColumns.push(sortOpts);
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           setSortColumns(sortColumns);
 | |
| 
 | |
|           if (!options.multiColumnSort) {
 | |
|             trigger(self.onSort, {
 | |
|               multiColumnSort: false,
 | |
|               sortCol: column,
 | |
|               sortAsc: sortOpts.sortAsc}, 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 (columns[i].resizable) {
 | |
|           if (firstResizable === undefined) {
 | |
|             firstResizable = i;
 | |
|           }
 | |
|           lastResizable = i;
 | |
|         }
 | |
|       });
 | |
|       if (firstResizable === undefined) {
 | |
|         return;
 | |
|       }
 | |
|       columnElements.each(function (i, e) {
 | |
|         if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) {
 | |
|           return;
 | |
|         }
 | |
|         $col = $(e);
 | |
|         $("<div class='slick-resizable-handle' />")
 | |
|             .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);
 | |
|             })
 | |
|             .bind("drag", function (e, dd) {
 | |
|               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 < 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 { // 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;
 | |
|                       }
 | |
|                     }
 | |
|                   }
 | |
|                 }
 | |
|               }
 | |
|               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();
 | |
|                 }
 | |
|               }
 | |
|               updateCanvasWidth(true);
 | |
|               render();
 | |
|               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 = $("<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-headerrow-columns { height:" + options.headerRowHeight + "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) {
 | |
|       if (!stylesheet) {
 | |
|         var sheets = document.styleSheets;
 | |
|         for (var 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 (var 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.unbind(".slickgrid");
 | |
|       removeCssRules();
 | |
| 
 | |
|       $canvas.unbind("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] += growSize;
 | |
|         }
 | |
|         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);
 | |
|       if (reRender) {
 | |
|         invalidateAllRows();
 | |
|         render();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function applyColumnHeaderWidths() {
 | |
|       if (!initialized) { return; }
 | |
|       var h;
 | |
|       for (var i = 0, headers = $headers.children(), ii = headers.length; i < ii; i++) {
 | |
|         h = $(headers[i]);
 | |
|         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 headerColumnEls = $headers.children();
 | |
|       headerColumnEls
 | |
|           .removeClass("slick-header-column-sorted")
 | |
|           .find(".slick-sort-indicator")
 | |
|               .removeClass("slick-sort-indicator-asc slick-sort-indicator-desc");
 | |
| 
 | |
|       $.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");
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     function getSortColumns() {
 | |
|       return sortColumns;
 | |
|     }
 | |
| 
 | |
|     function handleSelectedRangesChanged(e, ranges) {
 | |
|       selectedRows = [];
 | |
|       var hash = {};
 | |
|       for (var i = 0; i < ranges.length; i++) {
 | |
|         for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
 | |
|           if (!hash[j]) {  // prevent duplicates
 | |
|             selectedRows.push(j);
 | |
|             hash[j] = {};
 | |
|           }
 | |
|           for (var k = ranges[i].fromCell; k <= ranges[i].toCell; k++) {
 | |
|             if (canCellBeSelected(j, k)) {
 | |
|               hash[j][columns[k].id] = options.selectedCellCssClass;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       setCellCssStyles(options.selectedCellCssClass, hash);
 | |
| 
 | |
|       trigger(self.onSelectedRowsChanged, {rows: getSelectedRows()}, e);
 | |
|     }
 | |
| 
 | |
|     function getColumns() {
 | |
|       return columns;
 | |
|     }
 | |
| 
 | |
|     function updateColumnCaches() {
 | |
|       // Pre-calculate cell boundaries.
 | |
|       columnPosLeft = [];
 | |
|       columnPosRight = [];
 | |
|       var x = 0;
 | |
|       for (var i = 0, ii = columns.length; i < ii; i++) {
 | |
|         columnPosLeft[i] = x;
 | |
|         columnPosRight[i] = x + columns[i].width;
 | |
|         x += columns[i].width;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function setColumns(columnDefinitions) {
 | |
|       columns = columnDefinitions;
 | |
| 
 | |
|       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;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       updateColumnCaches();
 | |
| 
 | |
|       if (initialized) {
 | |
|         invalidateAllRows();
 | |
|         createColumnHeaders();
 | |
|         removeCssRules();
 | |
|         createCssRules();
 | |
|         resizeCanvas();
 | |
|         applyColumnWidths();
 | |
|         handleScroll();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getOptions() {
 | |
|       return options;
 | |
|     }
 | |
| 
 | |
|     function setOptions(args) {
 | |
|       if (!getEditorLock().commitCurrentEdit()) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       makeActiveCellNormal();
 | |
| 
 | |
|       if (options.enableAddRow !== args.enableAddRow) {
 | |
|         invalidateRow(getDataLength());
 | |
|       }
 | |
| 
 | |
|       options = $.extend(options, args);
 | |
|       validateAndEnforceOptions();
 | |
| 
 | |
|       $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
 | |
|       render();
 | |
|     }
 | |
| 
 | |
|     function validateAndEnforceOptions() {
 | |
|       if (options.autoHeight) {
 | |
|         options.leaveSpaceForNewRows = false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function setData(newData, scrollToTop) {
 | |
|       data = newData;
 | |
|       invalidateAllRows();
 | |
|       updateRowCount();
 | |
|       if (scrollToTop) {
 | |
|         scrollTo(0);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getData() {
 | |
|       return data;
 | |
|     }
 | |
| 
 | |
|     function getDataLength() {
 | |
|       if (data.getLength) {
 | |
|         return data.getLength();
 | |
|       } else {
 | |
|         return data.length;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getDataLengthIncludingAddNew() {
 | |
|       return getDataLength() + (options.enableAddRow ? 1 : 0);
 | |
|     }
 | |
| 
 | |
|     function getDataItem(i) {
 | |
|       if (data.getItem) {
 | |
|         return data.getItem(i);
 | |
|       } else {
 | |
|         return data[i];
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getTopPanel() {
 | |
|       return $topPanel[0];
 | |
|     }
 | |
| 
 | |
|     function setTopPanelVisibility(visible) {
 | |
|       if (options.showTopPanel != visible) {
 | |
|         options.showTopPanel = visible;
 | |
|         if (visible) {
 | |
|           $topPanelScroller.slideDown("fast", resizeCanvas);
 | |
|         } else {
 | |
|           $topPanelScroller.slideUp("fast", resizeCanvas);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function setHeaderRowVisibility(visible) {
 | |
|       if (options.showHeaderRow != visible) {
 | |
|         options.showHeaderRow = visible;
 | |
|         if (visible) {
 | |
|           $headerRowScroller.slideDown("fast", resizeCanvas);
 | |
|         } else {
 | |
|           $headerRowScroller.slideUp("fast", resizeCanvas);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getContainerNode() {
 | |
|       return $container.get(0);
 | |
|     }
 | |
| 
 | |
|     //////////////////////////////////////////////////////////////////////////////////////////////
 | |
|     // Rendering / Scrolling
 | |
| 
 | |
|     function getRowTop(row) {
 | |
|       return options.rowHeight * row - offset;
 | |
|     }
 | |
| 
 | |
|     function getRowFromPosition(y) {
 | |
|       return Math.floor((y + offset) / options.rowHeight);
 | |
|     }
 | |
| 
 | |
|     function scrollTo(y) {
 | |
|       y = Math.max(y, 0);
 | |
|       y = Math.min(y, th - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0));
 | |
| 
 | |
|       var oldOffset = offset;
 | |
| 
 | |
|       page = Math.min(n - 1, Math.floor(y / ph));
 | |
|       offset = Math.round(page * cj);
 | |
|       var newScrollTop = y - offset;
 | |
| 
 | |
|       if (offset != oldOffset) {
 | |
|         var range = getVisibleRange(newScrollTop);
 | |
|         cleanupRows(range);
 | |
|         updateRowPositions();
 | |
|       }
 | |
| 
 | |
|       if (prevScrollTop != newScrollTop) {
 | |
|         vScrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
 | |
|         $viewport[0].scrollTop = (lastRenderedScrollTop = scrollTop = prevScrollTop = newScrollTop);
 | |
| 
 | |
|         trigger(self.onViewportChanged, {});
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function defaultFormatter(row, cell, value, columnDef, dataContext) {
 | |
|       if (value == null) {
 | |
|         return "";
 | |
|       } else {
 | |
|         return (value + "").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getFormatter(row, column) {
 | |
|       var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
 | |
| 
 | |
|       // look up by id, then index
 | |
|       var columnOverrides = rowMetadata &&
 | |
|           rowMetadata.columns &&
 | |
|           (rowMetadata.columns[column.id] || rowMetadata.columns[getColumnIndex(column.id)]);
 | |
| 
 | |
|       return (columnOverrides && columnOverrides.formatter) ||
 | |
|           (rowMetadata && rowMetadata.formatter) ||
 | |
|           column.formatter ||
 | |
|           (options.formatterFactory && options.formatterFactory.getFormatter(column)) ||
 | |
|           options.defaultFormatter;
 | |
|     }
 | |
| 
 | |
|     function getEditor(row, cell) {
 | |
|       var column = columns[cell];
 | |
|       var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
 | |
|       var columnMetadata = rowMetadata && rowMetadata.columns;
 | |
| 
 | |
|       if (columnMetadata && columnMetadata[column.id] && columnMetadata[column.id].editor !== undefined) {
 | |
|         return columnMetadata[column.id].editor;
 | |
|       }
 | |
|       if (columnMetadata && columnMetadata[cell] && columnMetadata[cell].editor !== undefined) {
 | |
|         return columnMetadata[cell].editor;
 | |
|       }
 | |
| 
 | |
|       return column.editor || (options.editorFactory && options.editorFactory.getEditor(column));
 | |
|     }
 | |
| 
 | |
|     function getDataItemValueForColumn(item, columnDef) {
 | |
|       if (options.dataItemColumnValueExtractor) {
 | |
|         return options.dataItemColumnValueExtractor(item, columnDef);
 | |
|       }
 | |
|       return item[columnDef.field];
 | |
|     }
 | |
| 
 | |
|     function appendRowHtml(stringArray, row, range, dataLength) {
 | |
|       var d = getDataItem(row);
 | |
|       var dataLoading = row < dataLength && !d;
 | |
|       var rowCss = "slick-row" +
 | |
|           (dataLoading ? " loading" : "") +
 | |
|           (row === activeRow ? " active" : "") +
 | |
|           (row % 2 == 1 ? " odd" : " even");
 | |
| 
 | |
|       if (!d) {
 | |
|         rowCss += " " + options.addNewRowCssClass;
 | |
|       }
 | |
| 
 | |
|       var metadata = data.getItemMetadata && data.getItemMetadata(row);
 | |
| 
 | |
|       if (metadata && metadata.cssClasses) {
 | |
|         rowCss += " " + metadata.cssClasses;
 | |
|       }
 | |
| 
 | |
|       stringArray.push("<div class='ui-widget-content " + rowCss + "' style='top:" + getRowTop(row) + "px'>");
 | |
| 
 | |
|       var colspan, m;
 | |
|       for (var i = 0, ii = columns.length; i < ii; i++) {
 | |
|         m = columns[i];
 | |
|         colspan = 1;
 | |
|         if (metadata && metadata.columns) {
 | |
|           var columnData = metadata.columns[m.id] || metadata.columns[i];
 | |
|           colspan = (columnData && columnData.colspan) || 1;
 | |
|           if (colspan === "*") {
 | |
|             colspan = ii - i;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // Do not render cells outside of the viewport.
 | |
|         if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
 | |
|           if (columnPosLeft[i] > range.rightPx) {
 | |
|             // All columns to the right are outside the range.
 | |
|             break;
 | |
|           }
 | |
| 
 | |
|           appendCellHtml(stringArray, row, i, colspan, d);
 | |
|         }
 | |
| 
 | |
|         if (colspan > 1) {
 | |
|           i += (colspan - 1);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       stringArray.push("</div>");
 | |
|     }
 | |
| 
 | |
|     function appendCellHtml(stringArray, row, cell, colspan, item) {
 | |
|       var m = columns[cell];
 | |
|       var cellCss = "slick-cell l" + cell + " r" + Math.min(columns.length - 1, cell + colspan - 1) +
 | |
|           (m.cssClass ? " " + m.cssClass : "");
 | |
|       if (row === activeRow && cell === activeCell) {
 | |
|         cellCss += (" active");
 | |
|       }
 | |
| 
 | |
|       // TODO:  merge them together in the setter
 | |
|       for (var key in cellCssClasses) {
 | |
|         if (cellCssClasses[key][row] && cellCssClasses[key][row][m.id]) {
 | |
|           cellCss += (" " + cellCssClasses[key][row][m.id]);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       stringArray.push("<div class='" + cellCss + "'>");
 | |
| 
 | |
|       // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
 | |
|       if (item) {
 | |
|         var value = getDataItemValueForColumn(item, m);
 | |
|         stringArray.push(getFormatter(row, m)(row, cell, value, m, item));
 | |
|       }
 | |
| 
 | |
|       stringArray.push("</div>");
 | |
| 
 | |
|       rowsCache[row].cellRenderQueue.push(cell);
 | |
|       rowsCache[row].cellColSpans[cell] = colspan;
 | |
|     }
 | |
| 
 | |
| 
 | |
|     function cleanupRows(rangeToKeep) {
 | |
|       for (var i in rowsCache) {
 | |
|         if (((i = parseInt(i, 10)) !== activeRow) && (i < rangeToKeep.top || i > rangeToKeep.bottom)) {
 | |
|           removeRowFromCache(i);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function invalidate() {
 | |
|       updateRowCount();
 | |
|       invalidateAllRows();
 | |
|       render();
 | |
|     }
 | |
| 
 | |
|     function invalidateAllRows() {
 | |
|       if (currentEditor) {
 | |
|         makeActiveCellNormal();
 | |
|       }
 | |
|       for (var row in rowsCache) {
 | |
|         removeRowFromCache(row);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function removeRowFromCache(row) {
 | |
|       var cacheEntry = rowsCache[row];
 | |
|       if (!cacheEntry) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (rowNodeFromLastMouseWheelEvent == cacheEntry.rowNode) {
 | |
|         cacheEntry.rowNode.style.display = 'none';
 | |
|         zombieRowNodeFromLastMouseWheelEvent = rowNodeFromLastMouseWheelEvent;
 | |
|       } else {
 | |
|         $canvas[0].removeChild(cacheEntry.rowNode);
 | |
|       }
 | |
|       
 | |
|       delete rowsCache[row];
 | |
|       delete postProcessedRows[row];
 | |
|       renderedRows--;
 | |
|       counter_rows_removed++;
 | |
|     }
 | |
| 
 | |
|     function invalidateRows(rows) {
 | |
|       var i, rl;
 | |
|       if (!rows || !rows.length) {
 | |
|         return;
 | |
|       }
 | |
|       vScrollDir = 0;
 | |
|       for (i = 0, rl = rows.length; i < rl; i++) {
 | |
|         if (currentEditor && activeRow === rows[i]) {
 | |
|           makeActiveCellNormal();
 | |
|         }
 | |
|         if (rowsCache[rows[i]]) {
 | |
|           removeRowFromCache(rows[i]);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function invalidateRow(row) {
 | |
|       invalidateRows([row]);
 | |
|     }
 | |
| 
 | |
|     function updateCell(row, cell) {
 | |
|       var cellNode = getCellNode(row, cell);
 | |
|       if (!cellNode) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       var m = columns[cell], d = getDataItem(row);
 | |
|       if (currentEditor && activeRow === row && activeCell === cell) {
 | |
|         currentEditor.loadValue(d);
 | |
|       } else {
 | |
|         cellNode.innerHTML = d ? getFormatter(row, m)(row, cell, getDataItemValueForColumn(d, m), m, d) : "";
 | |
|         invalidatePostProcessingResults(row);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function updateRow(row) {
 | |
|       var cacheEntry = rowsCache[row];
 | |
|       if (!cacheEntry) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       ensureCellNodesInRowsCache(row);
 | |
| 
 | |
|       var d = getDataItem(row);
 | |
| 
 | |
|       for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
 | |
|         if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         columnIdx = columnIdx | 0;
 | |
|         var m = columns[columnIdx],
 | |
|             node = cacheEntry.cellNodesByColumnIdx[columnIdx];
 | |
| 
 | |
|         if (row === activeRow && columnIdx === activeCell && currentEditor) {
 | |
|           currentEditor.loadValue(d);
 | |
|         } else if (d) {
 | |
|           node.innerHTML = getFormatter(row, m)(row, columnIdx, getDataItemValueForColumn(d, m), m, d);
 | |
|         } else {
 | |
|           node.innerHTML = "";
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       invalidatePostProcessingResults(row);
 | |
|     }
 | |
| 
 | |
|     function getViewportHeight() {
 | |
|       return parseFloat($.css($container[0], "height", true)) -
 | |
|           parseFloat($.css($container[0], "paddingTop", true)) -
 | |
|           parseFloat($.css($container[0], "paddingBottom", true)) -
 | |
|           parseFloat($.css($headerScroller[0], "height")) - getVBoxDelta($headerScroller) -
 | |
|           (options.showTopPanel ? options.topPanelHeight + getVBoxDelta($topPanelScroller) : 0) -
 | |
|           (options.showHeaderRow ? options.headerRowHeight + getVBoxDelta($headerRowScroller) : 0);
 | |
|     }
 | |
| 
 | |
|     function resizeCanvas() {
 | |
|       if (!initialized) { return; }
 | |
|       if (options.autoHeight) {
 | |
|         viewportH = options.rowHeight * getDataLengthIncludingAddNew();
 | |
|       } else {
 | |
|         viewportH = getViewportHeight();
 | |
|       }
 | |
| 
 | |
|       numVisibleRows = Math.ceil(viewportH / options.rowHeight);
 | |
|       viewportW = parseFloat($.css($container[0], "width", true));
 | |
|       if (!options.autoHeight) {
 | |
|         $viewport.height(viewportH);
 | |
|       }
 | |
| 
 | |
|       if (options.forceFitColumns) {
 | |
|         autosizeColumns();
 | |
|       }
 | |
| 
 | |
|       updateRowCount();
 | |
|       handleScroll();
 | |
|       // Since the width has changed, force the render() to reevaluate virtually rendered cells.
 | |
|       lastRenderedScrollLeft = -1;
 | |
|       render();
 | |
|     }
 | |
| 
 | |
|     function updateRowCount() {
 | |
|       if (!initialized) { return; }
 | |
| 
 | |
|       var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
 | |
|       var numberOfRows = dataLengthIncludingAddNew +
 | |
|           (options.leaveSpaceForNewRows ? numVisibleRows - 1 : 0);
 | |
| 
 | |
|       var oldViewportHasVScroll = viewportHasVScroll;
 | |
|       // with autoHeight, we do not need to accommodate the vertical scroll bar
 | |
|       viewportHasVScroll = !options.autoHeight && (numberOfRows * options.rowHeight > viewportH);
 | |
| 
 | |
|       makeActiveCellNormal();
 | |
| 
 | |
|       // remove the rows that are now outside of the data range
 | |
|       // this helps avoid redundant calls to .removeRow() when the size of the data decreased by thousands of rows
 | |
|       var l = dataLengthIncludingAddNew - 1;
 | |
|       for (var i in rowsCache) {
 | |
|         if (i >= l) {
 | |
|           removeRowFromCache(i);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (activeCellNode && activeRow > l) {
 | |
|         resetActiveCell();
 | |
|       }
 | |
| 
 | |
|       var oldH = h;
 | |
|       th = Math.max(options.rowHeight * numberOfRows, viewportH - scrollbarDimensions.height);
 | |
|       if (th < maxSupportedCssHeight) {
 | |
|         // just one page
 | |
|         h = ph = th;
 | |
|         n = 1;
 | |
|         cj = 0;
 | |
|       } else {
 | |
|         // break into pages
 | |
|         h = maxSupportedCssHeight;
 | |
|         ph = h / 100;
 | |
|         n = Math.floor(th / ph);
 | |
|         cj = (th - h) / (n - 1);
 | |
|       }
 | |
| 
 | |
|       if (h !== oldH) {
 | |
|         $canvas.css("height", h);
 | |
|         scrollTop = $viewport[0].scrollTop;
 | |
|       }
 | |
| 
 | |
|       var oldScrollTopInRange = (scrollTop + offset <= th - viewportH);
 | |
| 
 | |
|       if (th == 0 || scrollTop == 0) {
 | |
|         page = offset = 0;
 | |
|       } else if (oldScrollTopInRange) {
 | |
|         // maintain virtual position
 | |
|         scrollTo(scrollTop + offset);
 | |
|       } else {
 | |
|         // scroll to bottom
 | |
|         scrollTo(th - viewportH);
 | |
|       }
 | |
| 
 | |
|       if (h != oldH && options.autoHeight) {
 | |
|         resizeCanvas();
 | |
|       }
 | |
| 
 | |
|       if (options.forceFitColumns && oldViewportHasVScroll != viewportHasVScroll) {
 | |
|         autosizeColumns();
 | |
|       }
 | |
|       updateCanvasWidth(false);
 | |
|     }
 | |
| 
 | |
|     function getVisibleRange(viewportTop, viewportLeft) {
 | |
|       if (viewportTop == null) {
 | |
|         viewportTop = scrollTop;
 | |
|       }
 | |
|       if (viewportLeft == null) {
 | |
|         viewportLeft = scrollLeft;
 | |
|       }
 | |
| 
 | |
|       return {
 | |
|         top: getRowFromPosition(viewportTop),
 | |
|         bottom: getRowFromPosition(viewportTop + viewportH) + 1,
 | |
|         leftPx: viewportLeft,
 | |
|         rightPx: viewportLeft + viewportW
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     function getRenderedRange(viewportTop, viewportLeft) {
 | |
|       var range = getVisibleRange(viewportTop, viewportLeft);
 | |
|       var buffer = Math.round(viewportH / options.rowHeight);
 | |
|       var minBuffer = 3;
 | |
| 
 | |
|       if (vScrollDir == -1) {
 | |
|         range.top -= buffer;
 | |
|         range.bottom += minBuffer;
 | |
|       } else if (vScrollDir == 1) {
 | |
|         range.top -= minBuffer;
 | |
|         range.bottom += buffer;
 | |
|       } else {
 | |
|         range.top -= minBuffer;
 | |
|         range.bottom += minBuffer;
 | |
|       }
 | |
| 
 | |
|       range.top = Math.max(0, range.top);
 | |
|       range.bottom = Math.min(getDataLengthIncludingAddNew() - 1, range.bottom);
 | |
| 
 | |
|       range.leftPx -= viewportW;
 | |
|       range.rightPx += viewportW;
 | |
| 
 | |
|       range.leftPx = Math.max(0, range.leftPx);
 | |
|       range.rightPx = Math.min(canvasWidth, range.rightPx);
 | |
| 
 | |
|       return range;
 | |
|     }
 | |
| 
 | |
|     function ensureCellNodesInRowsCache(row) {
 | |
|       var cacheEntry = rowsCache[row];
 | |
|       if (cacheEntry) {
 | |
|         if (cacheEntry.cellRenderQueue.length) {
 | |
|           var lastChild = cacheEntry.rowNode.lastChild;
 | |
|           while (cacheEntry.cellRenderQueue.length) {
 | |
|             var columnIdx = cacheEntry.cellRenderQueue.pop();
 | |
|             cacheEntry.cellNodesByColumnIdx[columnIdx] = lastChild;
 | |
|             lastChild = lastChild.previousSibling;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function cleanUpCells(range, row) {
 | |
|       var totalCellsRemoved = 0;
 | |
|       var cacheEntry = rowsCache[row];
 | |
| 
 | |
|       // Remove cells outside the range.
 | |
|       var cellsToRemove = [];
 | |
|       for (var i in cacheEntry.cellNodesByColumnIdx) {
 | |
|         // I really hate it when people mess with Array.prototype.
 | |
|         if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(i)) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         // This is a string, so it needs to be cast back to a number.
 | |
|         i = i | 0;
 | |
| 
 | |
|         var colspan = cacheEntry.cellColSpans[i];
 | |
|         if (columnPosLeft[i] > range.rightPx ||
 | |
|           columnPosRight[Math.min(columns.length - 1, i + colspan - 1)] < range.leftPx) {
 | |
|           if (!(row == activeRow && i == activeCell)) {
 | |
|             cellsToRemove.push(i);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var cellToRemove;
 | |
|       while ((cellToRemove = cellsToRemove.pop()) != null) {
 | |
|         cacheEntry.rowNode.removeChild(cacheEntry.cellNodesByColumnIdx[cellToRemove]);
 | |
|         delete cacheEntry.cellColSpans[cellToRemove];
 | |
|         delete cacheEntry.cellNodesByColumnIdx[cellToRemove];
 | |
|         if (postProcessedRows[row]) {
 | |
|           delete postProcessedRows[row][cellToRemove];
 | |
|         }
 | |
|         totalCellsRemoved++;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function cleanUpAndRenderCells(range) {
 | |
|       var cacheEntry;
 | |
|       var stringArray = [];
 | |
|       var processedRows = [];
 | |
|       var cellsAdded;
 | |
|       var totalCellsAdded = 0;
 | |
|       var colspan;
 | |
| 
 | |
|       for (var row = range.top, btm = range.bottom; row <= btm; row++) {
 | |
|         cacheEntry = rowsCache[row];
 | |
|         if (!cacheEntry) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         // cellRenderQueue populated in renderRows() needs to be cleared first
 | |
|         ensureCellNodesInRowsCache(row);
 | |
| 
 | |
|         cleanUpCells(range, row);
 | |
| 
 | |
|         // Render missing cells.
 | |
|         cellsAdded = 0;
 | |
| 
 | |
|         var metadata = data.getItemMetadata && data.getItemMetadata(row);
 | |
|         metadata = metadata && metadata.columns;
 | |
| 
 | |
|         var d = getDataItem(row);
 | |
| 
 | |
|         // TODO:  shorten this loop (index? heuristics? binary search?)
 | |
|         for (var i = 0, ii = columns.length; i < ii; i++) {
 | |
|           // Cells to the right are outside the range.
 | |
|           if (columnPosLeft[i] > range.rightPx) {
 | |
|             break;
 | |
|           }
 | |
| 
 | |
|           // Already rendered.
 | |
|           if ((colspan = cacheEntry.cellColSpans[i]) != null) {
 | |
|             i += (colspan > 1 ? colspan - 1 : 0);
 | |
|             continue;
 | |
|           }
 | |
| 
 | |
|           colspan = 1;
 | |
|           if (metadata) {
 | |
|             var columnData = metadata[columns[i].id] || metadata[i];
 | |
|             colspan = (columnData && columnData.colspan) || 1;
 | |
|             if (colspan === "*") {
 | |
|               colspan = ii - i;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
 | |
|             appendCellHtml(stringArray, row, i, colspan, d);
 | |
|             cellsAdded++;
 | |
|           }
 | |
| 
 | |
|           i += (colspan > 1 ? colspan - 1 : 0);
 | |
|         }
 | |
| 
 | |
|         if (cellsAdded) {
 | |
|           totalCellsAdded += cellsAdded;
 | |
|           processedRows.push(row);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (!stringArray.length) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       var x = document.createElement("div");
 | |
|       x.innerHTML = stringArray.join("");
 | |
| 
 | |
|       var processedRow;
 | |
|       var node;
 | |
|       while ((processedRow = processedRows.pop()) != null) {
 | |
|         cacheEntry = rowsCache[processedRow];
 | |
|         var columnIdx;
 | |
|         while ((columnIdx = cacheEntry.cellRenderQueue.pop()) != null) {
 | |
|           node = x.lastChild;
 | |
|           cacheEntry.rowNode.appendChild(node);
 | |
|           cacheEntry.cellNodesByColumnIdx[columnIdx] = node;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function renderRows(range) {
 | |
|       var parentNode = $canvas[0],
 | |
|           stringArray = [],
 | |
|           rows = [],
 | |
|           needToReselectCell = false,
 | |
|           dataLength = getDataLength();
 | |
| 
 | |
|       for (var i = range.top, ii = range.bottom; i <= ii; i++) {
 | |
|         if (rowsCache[i]) {
 | |
|           continue;
 | |
|         }
 | |
|         renderedRows++;
 | |
|         rows.push(i);
 | |
| 
 | |
|         // Create an entry right away so that appendRowHtml() can
 | |
|         // start populatating it.
 | |
|         rowsCache[i] = {
 | |
|           "rowNode": null,
 | |
| 
 | |
|           // ColSpans of rendered cells (by column idx).
 | |
|           // Can also be used for checking whether a cell has been rendered.
 | |
|           "cellColSpans": [],
 | |
| 
 | |
|           // Cell nodes (by column idx).  Lazy-populated by ensureCellNodesInRowsCache().
 | |
|           "cellNodesByColumnIdx": [],
 | |
| 
 | |
|           // Column indices of cell nodes that have been rendered, but not yet indexed in
 | |
|           // cellNodesByColumnIdx.  These are in the same order as cell nodes added at the
 | |
|           // end of the row.
 | |
|           "cellRenderQueue": []
 | |
|         };
 | |
| 
 | |
|         appendRowHtml(stringArray, i, range, dataLength);
 | |
|         if (activeCellNode && activeRow === i) {
 | |
|           needToReselectCell = true;
 | |
|         }
 | |
|         counter_rows_rendered++;
 | |
|       }
 | |
| 
 | |
|       if (!rows.length) { return; }
 | |
| 
 | |
|       var x = document.createElement("div");
 | |
|       x.innerHTML = stringArray.join("");
 | |
| 
 | |
|       for (var i = 0, ii = rows.length; i < ii; i++) {
 | |
|         rowsCache[rows[i]].rowNode = parentNode.appendChild(x.firstChild);
 | |
|       }
 | |
| 
 | |
|       if (needToReselectCell) {
 | |
|         activeCellNode = getCellNode(activeRow, activeCell);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function startPostProcessing() {
 | |
|       if (!options.enableAsyncPostRender) {
 | |
|         return;
 | |
|       }
 | |
|       clearTimeout(h_postrender);
 | |
|       h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
 | |
|     }
 | |
| 
 | |
|     function invalidatePostProcessingResults(row) {
 | |
|       delete postProcessedRows[row];
 | |
|       postProcessFromRow = Math.min(postProcessFromRow, row);
 | |
|       postProcessToRow = Math.max(postProcessToRow, row);
 | |
|       startPostProcessing();
 | |
|     }
 | |
| 
 | |
|     function updateRowPositions() {
 | |
|       for (var row in rowsCache) {
 | |
|         rowsCache[row].rowNode.style.top = getRowTop(row) + "px";
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function render() {
 | |
|       if (!initialized) { return; }
 | |
|       var visible = getVisibleRange();
 | |
|       var rendered = getRenderedRange();
 | |
| 
 | |
|       // remove rows no longer in the viewport
 | |
|       cleanupRows(rendered);
 | |
| 
 | |
|       // add new rows & missing cells in existing rows
 | |
|       if (lastRenderedScrollLeft != scrollLeft) {
 | |
|         cleanUpAndRenderCells(rendered);
 | |
|       }
 | |
| 
 | |
|       // render missing rows
 | |
|       renderRows(rendered);
 | |
| 
 | |
|       postProcessFromRow = visible.top;
 | |
|       postProcessToRow = Math.min(getDataLengthIncludingAddNew() - 1, visible.bottom);
 | |
|       startPostProcessing();
 | |
| 
 | |
|       lastRenderedScrollTop = scrollTop;
 | |
|       lastRenderedScrollLeft = scrollLeft;
 | |
|       h_render = null;
 | |
|     }
 | |
| 
 | |
|     function handleHeaderRowScroll() {
 | |
|       var scrollLeft = $headerRowScroller[0].scrollLeft;
 | |
|       if (scrollLeft != $viewport[0].scrollLeft) {
 | |
|         $viewport[0].scrollLeft = scrollLeft;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function handleScroll() {
 | |
|       scrollTop = $viewport[0].scrollTop;
 | |
|       scrollLeft = $viewport[0].scrollLeft;
 | |
|       var vScrollDist = Math.abs(scrollTop - prevScrollTop);
 | |
|       var hScrollDist = Math.abs(scrollLeft - prevScrollLeft);
 | |
| 
 | |
|       if (hScrollDist) {
 | |
|         prevScrollLeft = scrollLeft;
 | |
|         $headerScroller[0].scrollLeft = scrollLeft;
 | |
|         $topPanelScroller[0].scrollLeft = scrollLeft;
 | |
|         $headerRowScroller[0].scrollLeft = scrollLeft;
 | |
|       }
 | |
| 
 | |
|       if (vScrollDist) {
 | |
|         vScrollDir = prevScrollTop < scrollTop ? 1 : -1;
 | |
|         prevScrollTop = scrollTop;
 | |
| 
 | |
|         // switch virtual pages if needed
 | |
|         if (vScrollDist < viewportH) {
 | |
|           scrollTo(scrollTop + offset);
 | |
|         } else {
 | |
|           var oldOffset = offset;
 | |
|           if (h == viewportH) {
 | |
|             page = 0;
 | |
|           } else {
 | |
|             page = Math.min(n - 1, Math.floor(scrollTop * ((th - viewportH) / (h - viewportH)) * (1 / ph)));
 | |
|           }
 | |
|           offset = Math.round(page * cj);
 | |
|           if (oldOffset != offset) {
 | |
|             invalidateAllRows();
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (hScrollDist || vScrollDist) {
 | |
|         if (h_render) {
 | |
|           clearTimeout(h_render);
 | |
|         }
 | |
| 
 | |
|         if (Math.abs(lastRenderedScrollTop - scrollTop) > 20 ||
 | |
|             Math.abs(lastRenderedScrollLeft - scrollLeft) > 20) {
 | |
|           if (options.forceSyncScrolling || (
 | |
|               Math.abs(lastRenderedScrollTop - scrollTop) < viewportH &&
 | |
|               Math.abs(lastRenderedScrollLeft - scrollLeft) < viewportW)) {
 | |
|             render();
 | |
|           } else {
 | |
|             h_render = setTimeout(render, 50);
 | |
|           }
 | |
| 
 | |
|           trigger(self.onViewportChanged, {});
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       trigger(self.onScroll, {scrollLeft: scrollLeft, scrollTop: scrollTop});
 | |
|     }
 | |
| 
 | |
|     function asyncPostProcessRows() {
 | |
|       var dataLength = getDataLength();
 | |
|       while (postProcessFromRow <= postProcessToRow) {
 | |
|         var row = (vScrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
 | |
|         var cacheEntry = rowsCache[row];
 | |
|         if (!cacheEntry || row >= dataLength) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (!postProcessedRows[row]) {
 | |
|           postProcessedRows[row] = {};
 | |
|         }
 | |
| 
 | |
|         ensureCellNodesInRowsCache(row);
 | |
|         for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
 | |
|           if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
 | |
|             continue;
 | |
|           }
 | |
| 
 | |
|           columnIdx = columnIdx | 0;
 | |
| 
 | |
|           var m = columns[columnIdx];
 | |
|           if (m.asyncPostRender && !postProcessedRows[row][columnIdx]) {
 | |
|             var node = cacheEntry.cellNodesByColumnIdx[columnIdx];
 | |
|             if (node) {
 | |
|               m.asyncPostRender(node, row, getDataItem(row), m);
 | |
|             }
 | |
|             postProcessedRows[row][columnIdx] = true;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function updateCellCssStylesOnRenderedRows(addedHash, removedHash) {
 | |
|       var node, columnId, addedRowHash, removedRowHash;
 | |
|       for (var row in rowsCache) {
 | |
|         removedRowHash = removedHash && removedHash[row];
 | |
|         addedRowHash = addedHash && addedHash[row];
 | |
| 
 | |
|         if (removedRowHash) {
 | |
|           for (columnId in removedRowHash) {
 | |
|             if (!addedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
 | |
|               node = getCellNode(row, getColumnIndex(columnId));
 | |
|               if (node) {
 | |
|                 $(node).removeClass(removedRowHash[columnId]);
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (addedRowHash) {
 | |
|           for (columnId in addedRowHash) {
 | |
|             if (!removedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
 | |
|               node = getCellNode(row, getColumnIndex(columnId));
 | |
|               if (node) {
 | |
|                 $(node).addClass(addedRowHash[columnId]);
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function addCellCssStyles(key, hash) {
 | |
|       if (cellCssClasses[key]) {
 | |
|         throw "addCellCssStyles: cell CSS hash with key '" + key + "' already exists.";
 | |
|       }
 | |
| 
 | |
|       cellCssClasses[key] = hash;
 | |
|       updateCellCssStylesOnRenderedRows(hash, null);
 | |
| 
 | |
|       trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash });
 | |
|     }
 | |
| 
 | |
|     function removeCellCssStyles(key) {
 | |
|       if (!cellCssClasses[key]) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       updateCellCssStylesOnRenderedRows(null, cellCssClasses[key]);
 | |
|       delete cellCssClasses[key];
 | |
| 
 | |
|       trigger(self.onCellCssStylesChanged, { "key": key, "hash": null });
 | |
|     }
 | |
| 
 | |
|     function setCellCssStyles(key, hash) {
 | |
|       var prevHash = cellCssClasses[key];
 | |
| 
 | |
|       cellCssClasses[key] = hash;
 | |
|       updateCellCssStylesOnRenderedRows(hash, prevHash);
 | |
| 
 | |
|       trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash });
 | |
|     }
 | |
| 
 | |
|     function getCellCssStyles(key) {
 | |
|       return cellCssClasses[key];
 | |
|     }
 | |
| 
 | |
|     function flashCell(row, cell, speed) {
 | |
|       speed = speed || 100;
 | |
|       if (rowsCache[row]) {
 | |
|         var $cell = $(getCellNode(row, cell));
 | |
| 
 | |
|         function toggleCellClass(times) {
 | |
|           if (!times) {
 | |
|             return;
 | |
|           }
 | |
|           setTimeout(function () {
 | |
|                 $cell.queue(function () {
 | |
|                   $cell.toggleClass(options.cellFlashingCssClass).dequeue();
 | |
|                   toggleCellClass(times - 1);
 | |
|                 });
 | |
|               },
 | |
|               speed);
 | |
|         }
 | |
| 
 | |
|         toggleCellClass(4);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     //////////////////////////////////////////////////////////////////////////////////////////////
 | |
|     // Interactivity
 | |
| 
 | |
|     function handleMouseWheel(e) {
 | |
|       var rowNode = $(e.target).closest(".slick-row")[0];
 | |
|       if (rowNode != rowNodeFromLastMouseWheelEvent) {
 | |
|         if (zombieRowNodeFromLastMouseWheelEvent && zombieRowNodeFromLastMouseWheelEvent != rowNode) {
 | |
|           $canvas[0].removeChild(zombieRowNodeFromLastMouseWheelEvent);
 | |
|           zombieRowNodeFromLastMouseWheelEvent = null;
 | |
|         }
 | |
|         rowNodeFromLastMouseWheelEvent = rowNode;      
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function handleDragInit(e, dd) {
 | |
|       var cell = getCellFromEvent(e);
 | |
|       if (!cell || !cellExists(cell.row, cell.cell)) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       var retval = trigger(self.onDragInit, dd, e);
 | |
|       if (e.isImmediatePropagationStopped()) {
 | |
|         return retval;
 | |
|       }
 | |
| 
 | |
|       // if nobody claims to be handling drag'n'drop by stopping immediate propagation,
 | |
|       // cancel out of it
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     function handleDragStart(e, dd) {
 | |
|       var cell = getCellFromEvent(e);
 | |
|       if (!cell || !cellExists(cell.row, cell.cell)) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       var retval = trigger(self.onDragStart, dd, e);
 | |
|       if (e.isImmediatePropagationStopped()) {
 | |
|         return retval;
 | |
|       }
 | |
| 
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     function handleDrag(e, dd) {
 | |
|       return trigger(self.onDrag, dd, e);
 | |
|     }
 | |
| 
 | |
|     function handleDragEnd(e, dd) {
 | |
|       trigger(self.onDragEnd, dd, e);
 | |
|     }
 | |
| 
 | |
|     function handleKeyDown(e) {
 | |
|       trigger(self.onKeyDown, {row: activeRow, cell: activeCell}, e);
 | |
|       var handled = e.isImmediatePropagationStopped();
 | |
| 
 | |
|       if (!handled) {
 | |
|         if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
 | |
|           if (e.which == 27) {
 | |
|             if (!getEditorLock().isActive()) {
 | |
|               return; // no editing mode to cancel, allow bubbling and default processing (exit without cancelling the event)
 | |
|             }
 | |
|             cancelEditAndSetFocus();
 | |
|           } else if (e.which == 34) {
 | |
|             navigatePageDown();
 | |
|             handled = true;           
 | |
|           } else if (e.which == 33) {
 | |
|             navigatePageUp();
 | |
|             handled = true;
 | |
|           } else if (e.which == 37) {
 | |
|             handled = navigateLeft();
 | |
|           } else if (e.which == 39) {
 | |
|             handled = navigateRight();
 | |
|           } else if (e.which == 38) {
 | |
|             handled = navigateUp();
 | |
|           } else if (e.which == 40) {
 | |
|             handled = navigateDown();
 | |
|           } else if (e.which == 9) {
 | |
|             handled = navigateNext();
 | |
|           } else if (e.which == 13) {
 | |
|             if (options.editable) {
 | |
|               if (currentEditor) {
 | |
|                 // adding new row
 | |
|                 if (activeRow === getDataLength()) {
 | |
|                   navigateDown();
 | |
|                 } else {
 | |
|                   commitEditAndSetFocus();
 | |
|                 }
 | |
|               } else {
 | |
|                 if (getEditorLock().commitCurrentEdit()) {
 | |
|                   makeActiveCellEditable();
 | |
|                 }
 | |
|               }
 | |
|             }
 | |
|             handled = true;
 | |
|           }
 | |
|         } else if (e.which == 9 && e.shiftKey && !e.ctrlKey && !e.altKey) {
 | |
|           handled = navigatePrev();
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (handled) {
 | |
|         // the event has been handled so don't let parent element (bubbling/propagation) or browser (default) handle it
 | |
|         e.stopPropagation();
 | |
|         e.preventDefault();
 | |
|         try {
 | |
|           e.originalEvent.keyCode = 0; // prevent default behaviour for special keys in IE browsers (F3, F5, etc.)
 | |
|         }
 | |
|         // ignore exceptions - setting the original event's keycode throws access denied exception for "Ctrl"
 | |
|         // (hitting control key only, nothing else), "Shift" (maybe others)
 | |
|         catch (error) {
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function handleClick(e) {
 | |
|       if (!currentEditor) {
 | |
|         // if this click resulted in some cell child node getting focus,
 | |
|         // don't steal it back - keyboard events will still bubble up
 | |
|         // IE9+ seems to default DIVs to tabIndex=0 instead of -1, so check for cell clicks directly.
 | |
|         if (e.target != document.activeElement || $(e.target).hasClass("slick-cell")) {
 | |
|           setFocus();
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var cell = getCellFromEvent(e);
 | |
|       if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       trigger(self.onClick, {row: cell.row, cell: cell.cell}, e);
 | |
|       if (e.isImmediatePropagationStopped()) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if ((activeCell != cell.cell || activeRow != cell.row) && canCellBeActive(cell.row, cell.cell)) {
 | |
|         if (!getEditorLock().isActive() || getEditorLock().commitCurrentEdit()) {
 | |
|           scrollRowIntoView(cell.row, false);
 | |
|           setActiveCellInternal(getCellNode(cell.row, cell.cell));
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function handleContextMenu(e) {
 | |
|       var $cell = $(e.target).closest(".slick-cell", $canvas);
 | |
|       if ($cell.length === 0) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // are we editing this cell?
 | |
|       if (activeCellNode === $cell[0] && currentEditor !== null) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       trigger(self.onContextMenu, {}, e);
 | |
|     }
 | |
| 
 | |
|     function handleDblClick(e) {
 | |
|       var cell = getCellFromEvent(e);
 | |
|       if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       trigger(self.onDblClick, {row: cell.row, cell: cell.cell}, e);
 | |
|       if (e.isImmediatePropagationStopped()) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (options.editable) {
 | |
|         gotoCell(cell.row, cell.cell, true);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function handleHeaderMouseEnter(e) {
 | |
|       trigger(self.onHeaderMouseEnter, {
 | |
|         "column": $(this).data("column")
 | |
|       }, e);
 | |
|     }
 | |
| 
 | |
|     function handleHeaderMouseLeave(e) {
 | |
|       trigger(self.onHeaderMouseLeave, {
 | |
|         "column": $(this).data("column")
 | |
|       }, e);
 | |
|     }
 | |
| 
 | |
|     function handleHeaderContextMenu(e) {
 | |
|       var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
 | |
|       var column = $header && $header.data("column");
 | |
|       trigger(self.onHeaderContextMenu, {column: column}, e);
 | |
|     }
 | |
| 
 | |
|     function handleHeaderClick(e) {
 | |
|       var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
 | |
|       var column = $header && $header.data("column");
 | |
|       if (column) {
 | |
|         trigger(self.onHeaderClick, {column: column}, e);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function handleMouseEnter(e) {
 | |
|       trigger(self.onMouseEnter, {}, e);
 | |
|     }
 | |
| 
 | |
|     function handleMouseLeave(e) {
 | |
|       trigger(self.onMouseLeave, {}, e);
 | |
|     }
 | |
| 
 | |
|     function cellExists(row, cell) {
 | |
|       return !(row < 0 || row >= getDataLength() || cell < 0 || cell >= columns.length);
 | |
|     }
 | |
| 
 | |
|     function getCellFromPoint(x, y) {
 | |
|       var row = getRowFromPosition(y);
 | |
|       var cell = 0;
 | |
| 
 | |
|       var w = 0;
 | |
|       for (var i = 0; i < columns.length && w < x; i++) {
 | |
|         w += columns[i].width;
 | |
|         cell++;
 | |
|       }
 | |
| 
 | |
|       if (cell < 0) {
 | |
|         cell = 0;
 | |
|       }
 | |
| 
 | |
|       return {row: row, cell: cell - 1};
 | |
|     }
 | |
| 
 | |
|     function getCellFromNode(cellNode) {
 | |
|       // read column number from .l<columnNumber> CSS class
 | |
|       var cls = /l\d+/.exec(cellNode.className);
 | |
|       if (!cls) {
 | |
|         throw "getCellFromNode: cannot get cell - " + cellNode.className;
 | |
|       }
 | |
|       return parseInt(cls[0].substr(1, cls[0].length - 1), 10);
 | |
|     }
 | |
| 
 | |
|     function getRowFromNode(rowNode) {
 | |
|       for (var row in rowsCache) {
 | |
|         if (rowsCache[row].rowNode === rowNode) {
 | |
|           return row | 0;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function getCellFromEvent(e) {
 | |
|       var $cell = $(e.target).closest(".slick-cell", $canvas);
 | |
|       if (!$cell.length) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       var row = getRowFromNode($cell[0].parentNode);
 | |
|       var cell = getCellFromNode($cell[0]);
 | |
| 
 | |
|       if (row == null || cell == null) {
 | |
|         return null;
 | |
|       } else {
 | |
|         return {
 | |
|           "row": row,
 | |
|           "cell": cell
 | |
|         };
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getCellNodeBox(row, cell) {
 | |
|       if (!cellExists(row, cell)) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       var y1 = getRowTop(row);
 | |
|       var y2 = y1 + options.rowHeight - 1;
 | |
|       var x1 = 0;
 | |
|       for (var i = 0; i < cell; i++) {
 | |
|         x1 += columns[i].width;
 | |
|       }
 | |
|       var x2 = x1 + columns[cell].width;
 | |
| 
 | |
|       return {
 | |
|         top: y1,
 | |
|         left: x1,
 | |
|         bottom: y2,
 | |
|         right: x2
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     //////////////////////////////////////////////////////////////////////////////////////////////
 | |
|     // Cell switching
 | |
| 
 | |
|     function resetActiveCell() {
 | |
|       setActiveCellInternal(null, false);
 | |
|     }
 | |
| 
 | |
|     function setFocus() {
 | |
|       if (tabbingDirection == -1) {
 | |
|         $focusSink[0].focus();
 | |
|       } else {
 | |
|         $focusSink2[0].focus();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function scrollCellIntoView(row, cell, doPaging) {
 | |
|       scrollRowIntoView(row, doPaging);
 | |
| 
 | |
|       var colspan = getColspan(row, cell);
 | |
|       var left = columnPosLeft[cell],
 | |
|         right = columnPosRight[cell + (colspan > 1 ? colspan - 1 : 0)],
 | |
|         scrollRight = scrollLeft + viewportW;
 | |
| 
 | |
|       if (left < scrollLeft) {
 | |
|         $viewport.scrollLeft(left);
 | |
|         handleScroll();
 | |
|         render();
 | |
|       } else if (right > scrollRight) {
 | |
|         $viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
 | |
|         handleScroll();
 | |
|         render();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function setActiveCellInternal(newCell, opt_editMode) {
 | |
|       if (activeCellNode !== null) {
 | |
|         makeActiveCellNormal();
 | |
|         $(activeCellNode).removeClass("active");
 | |
|         if (rowsCache[activeRow]) {
 | |
|           $(rowsCache[activeRow].rowNode).removeClass("active");
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var activeCellChanged = (activeCellNode !== newCell);
 | |
|       activeCellNode = newCell;
 | |
| 
 | |
|       if (activeCellNode != null) {
 | |
|         activeRow = getRowFromNode(activeCellNode.parentNode);
 | |
|         activeCell = activePosX = getCellFromNode(activeCellNode);
 | |
| 
 | |
|         if (opt_editMode == null) {
 | |
|           opt_editMode = (activeRow == getDataLength()) || options.autoEdit;
 | |
|         }
 | |
| 
 | |
|         $(activeCellNode).addClass("active");
 | |
|         $(rowsCache[activeRow].rowNode).addClass("active");
 | |
| 
 | |
|         if (options.editable && opt_editMode && isCellPotentiallyEditable(activeRow, activeCell)) {
 | |
|           clearTimeout(h_editorLoader);
 | |
| 
 | |
|           if (options.asyncEditorLoading) {
 | |
|             h_editorLoader = setTimeout(function () {
 | |
|               makeActiveCellEditable();
 | |
|             }, options.asyncEditorLoadDelay);
 | |
|           } else {
 | |
|             makeActiveCellEditable();
 | |
|           }
 | |
|         }
 | |
|       } else {
 | |
|         activeRow = activeCell = null;
 | |
|       }
 | |
| 
 | |
|       if (activeCellChanged) {
 | |
|         trigger(self.onActiveCellChanged, getActiveCell());
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function clearTextSelection() {
 | |
|       if (document.selection && document.selection.empty) {
 | |
|         try {
 | |
|           //IE fails here if selected element is not in dom
 | |
|           document.selection.empty();
 | |
|         } catch (e) { }
 | |
|       } else if (window.getSelection) {
 | |
|         var sel = window.getSelection();
 | |
|         if (sel && sel.removeAllRanges) {
 | |
|           sel.removeAllRanges();
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function isCellPotentiallyEditable(row, cell) {
 | |
|       var dataLength = getDataLength();
 | |
|       // is the data for this row loaded?
 | |
|       if (row < dataLength && !getDataItem(row)) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       // are we in the Add New row?  can we create new from this cell?
 | |
|       if (columns[cell].cannotTriggerInsert && row >= dataLength) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       // does this cell have an editor?
 | |
|       if (!getEditor(row, cell)) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     function makeActiveCellNormal() {
 | |
|       if (!currentEditor) {
 | |
|         return;
 | |
|       }
 | |
|       trigger(self.onBeforeCellEditorDestroy, {editor: currentEditor});
 | |
|       currentEditor.destroy();
 | |
|       currentEditor = null;
 | |
| 
 | |
|       if (activeCellNode) {
 | |
|         var d = getDataItem(activeRow);
 | |
|         $(activeCellNode).removeClass("editable invalid");
 | |
|         if (d) {
 | |
|           var column = columns[activeCell];
 | |
|           var formatter = getFormatter(activeRow, column);
 | |
|           activeCellNode.innerHTML = formatter(activeRow, activeCell, getDataItemValueForColumn(d, column), column, d);
 | |
|           invalidatePostProcessingResults(activeRow);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // if there previously was text selected on a page (such as selected text in the edit cell just removed),
 | |
|       // IE can't set focus to anything else correctly
 | |
|       if (navigator.userAgent.toLowerCase().match(/msie/)) {
 | |
|         clearTextSelection();
 | |
|       }
 | |
| 
 | |
|       getEditorLock().deactivate(editController);
 | |
|     }
 | |
| 
 | |
|     function makeActiveCellEditable(editor) {
 | |
|       if (!activeCellNode) {
 | |
|         return;
 | |
|       }
 | |
|       if (!options.editable) {
 | |
|         throw "Grid : makeActiveCellEditable : should never get called when options.editable is false";
 | |
|       }
 | |
| 
 | |
|       // cancel pending async call if there is one
 | |
|       clearTimeout(h_editorLoader);
 | |
| 
 | |
|       if (!isCellPotentiallyEditable(activeRow, activeCell)) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       var columnDef = columns[activeCell];
 | |
|       var item = getDataItem(activeRow);
 | |
| 
 | |
|       if (trigger(self.onBeforeEditCell, {row: activeRow, cell: activeCell, item: item, column: columnDef}) === false) {
 | |
|         setFocus();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       getEditorLock().activate(editController);
 | |
|       $(activeCellNode).addClass("editable");
 | |
| 
 | |
|       // don't clear the cell if a custom editor is passed through
 | |
|       if (!editor) {
 | |
|         activeCellNode.innerHTML = "";
 | |
|       }
 | |
| 
 | |
|       currentEditor = new (editor || getEditor(activeRow, activeCell))({
 | |
|         grid: self,
 | |
|         gridPosition: absBox($container[0]),
 | |
|         position: absBox(activeCellNode),
 | |
|         container: activeCellNode,
 | |
|         column: columnDef,
 | |
|         item: item || {},
 | |
|         commitChanges: commitEditAndSetFocus,
 | |
|         cancelChanges: cancelEditAndSetFocus
 | |
|       });
 | |
| 
 | |
|       if (item) {
 | |
|         currentEditor.loadValue(item);
 | |
|       }
 | |
| 
 | |
|       serializedEditorValue = currentEditor.serializeValue();
 | |
| 
 | |
|       if (currentEditor.position) {
 | |
|         handleActiveCellPositionChange();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function commitEditAndSetFocus() {
 | |
|       // if the commit fails, it would do so due to a validation error
 | |
|       // if so, do not steal the focus from the editor
 | |
|       if (getEditorLock().commitCurrentEdit()) {
 | |
|         setFocus();
 | |
|         if (options.autoEdit) {
 | |
|           navigateDown();
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function cancelEditAndSetFocus() {
 | |
|       if (getEditorLock().cancelCurrentEdit()) {
 | |
|         setFocus();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function absBox(elem) {
 | |
|       var box = {
 | |
|         top: elem.offsetTop,
 | |
|         left: elem.offsetLeft,
 | |
|         bottom: 0,
 | |
|         right: 0,
 | |
|         width: $(elem).outerWidth(),
 | |
|         height: $(elem).outerHeight(),
 | |
|         visible: true};
 | |
|       box.bottom = box.top + box.height;
 | |
|       box.right = box.left + box.width;
 | |
| 
 | |
|       // walk up the tree
 | |
|       var offsetParent = elem.offsetParent;
 | |
|       while ((elem = elem.parentNode) != document.body) {
 | |
|         if (box.visible && elem.scrollHeight != elem.offsetHeight && $(elem).css("overflowY") != "visible") {
 | |
|           box.visible = box.bottom > elem.scrollTop && box.top < elem.scrollTop + elem.clientHeight;
 | |
|         }
 | |
| 
 | |
|         if (box.visible && elem.scrollWidth != elem.offsetWidth && $(elem).css("overflowX") != "visible") {
 | |
|           box.visible = box.right > elem.scrollLeft && box.left < elem.scrollLeft + elem.clientWidth;
 | |
|         }
 | |
| 
 | |
|         box.left -= elem.scrollLeft;
 | |
|         box.top -= elem.scrollTop;
 | |
| 
 | |
|         if (elem === offsetParent) {
 | |
|           box.left += elem.offsetLeft;
 | |
|           box.top += elem.offsetTop;
 | |
|           offsetParent = elem.offsetParent;
 | |
|         }
 | |
| 
 | |
|         box.bottom = box.top + box.height;
 | |
|         box.right = box.left + box.width;
 | |
|       }
 | |
| 
 | |
|       return box;
 | |
|     }
 | |
| 
 | |
|     function getActiveCellPosition() {
 | |
|       return absBox(activeCellNode);
 | |
|     }
 | |
| 
 | |
|     function getGridPosition() {
 | |
|       return absBox($container[0])
 | |
|     }
 | |
| 
 | |
|     function handleActiveCellPositionChange() {
 | |
|       if (!activeCellNode) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       trigger(self.onActiveCellPositionChanged, {});
 | |
| 
 | |
|       if (currentEditor) {
 | |
|         var cellBox = getActiveCellPosition();
 | |
|         if (currentEditor.show && currentEditor.hide) {
 | |
|           if (!cellBox.visible) {
 | |
|             currentEditor.hide();
 | |
|           } else {
 | |
|             currentEditor.show();
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (currentEditor.position) {
 | |
|           currentEditor.position(cellBox);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getCellEditor() {
 | |
|       return currentEditor;
 | |
|     }
 | |
| 
 | |
|     function getActiveCell() {
 | |
|       if (!activeCellNode) {
 | |
|         return null;
 | |
|       } else {
 | |
|         return {row: activeRow, cell: activeCell};
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getActiveCellNode() {
 | |
|       return activeCellNode;
 | |
|     }
 | |
| 
 | |
|     function scrollRowIntoView(row, doPaging) {
 | |
|       var rowAtTop = row * options.rowHeight;
 | |
|       var rowAtBottom = (row + 1) * options.rowHeight - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0);
 | |
| 
 | |
|       // need to page down?
 | |
|       if ((row + 1) * options.rowHeight > scrollTop + viewportH + offset) {
 | |
|         scrollTo(doPaging ? rowAtTop : rowAtBottom);
 | |
|         render();
 | |
|       }
 | |
|       // or page up?
 | |
|       else if (row * options.rowHeight < scrollTop + offset) {
 | |
|         scrollTo(doPaging ? rowAtBottom : rowAtTop);
 | |
|         render();
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function scrollRowToTop(row) {
 | |
|       scrollTo(row * options.rowHeight);
 | |
|       render();
 | |
|     }
 | |
| 
 | |
|     function scrollPage(dir) {
 | |
|       var deltaRows = dir * numVisibleRows;
 | |
|       scrollTo((getRowFromPosition(scrollTop) + deltaRows) * options.rowHeight);
 | |
|       render();
 | |
| 
 | |
|       if (options.enableCellNavigation && activeRow != null) {
 | |
|         var row = activeRow + deltaRows;
 | |
|         var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
 | |
|         if (row >= dataLengthIncludingAddNew) {
 | |
|           row = dataLengthIncludingAddNew - 1;
 | |
|         }
 | |
|         if (row < 0) {
 | |
|           row = 0;
 | |
|         }
 | |
| 
 | |
|         var cell = 0, prevCell = null;
 | |
|         var prevActivePosX = activePosX;
 | |
|         while (cell <= activePosX) {
 | |
|           if (canCellBeActive(row, cell)) {
 | |
|             prevCell = cell;  
 | |
|           }
 | |
|           cell += getColspan(row, cell);
 | |
|         }
 | |
| 
 | |
|         if (prevCell !== null) {
 | |
|           setActiveCellInternal(getCellNode(row, prevCell));
 | |
|           activePosX = prevActivePosX;
 | |
|         } else {
 | |
|           resetActiveCell();
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function navigatePageDown() {
 | |
|       scrollPage(1);
 | |
|     }
 | |
| 
 | |
|     function navigatePageUp() {
 | |
|       scrollPage(-1);
 | |
|     }
 | |
| 
 | |
|     function getColspan(row, cell) {
 | |
|       var metadata = data.getItemMetadata && data.getItemMetadata(row);
 | |
|       if (!metadata || !metadata.columns) {
 | |
|         return 1;
 | |
|       }
 | |
| 
 | |
|       var columnData = metadata.columns[columns[cell].id] || metadata.columns[cell];
 | |
|       var colspan = (columnData && columnData.colspan);
 | |
|       if (colspan === "*") {
 | |
|         colspan = columns.length - cell;
 | |
|       } else {
 | |
|         colspan = colspan || 1;
 | |
|       }
 | |
| 
 | |
|       return colspan;
 | |
|     }
 | |
| 
 | |
|     function findFirstFocusableCell(row) {
 | |
|       var cell = 0;
 | |
|       while (cell < columns.length) {
 | |
|         if (canCellBeActive(row, cell)) {
 | |
|           return cell;
 | |
|         }
 | |
|         cell += getColspan(row, cell);
 | |
|       }
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function findLastFocusableCell(row) {
 | |
|       var cell = 0;
 | |
|       var lastFocusableCell = null;
 | |
|       while (cell < columns.length) {
 | |
|         if (canCellBeActive(row, cell)) {
 | |
|           lastFocusableCell = cell;
 | |
|         }
 | |
|         cell += getColspan(row, cell);
 | |
|       }
 | |
|       return lastFocusableCell;
 | |
|     }
 | |
| 
 | |
|     function gotoRight(row, cell, posX) {
 | |
|       if (cell >= columns.length) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       do {
 | |
|         cell += getColspan(row, cell);
 | |
|       }
 | |
|       while (cell < columns.length && !canCellBeActive(row, cell));
 | |
| 
 | |
|       if (cell < columns.length) {
 | |
|         return {
 | |
|           "row": row,
 | |
|           "cell": cell,
 | |
|           "posX": cell
 | |
|         };
 | |
|       }
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function gotoLeft(row, cell, posX) {
 | |
|       if (cell <= 0) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       var firstFocusableCell = findFirstFocusableCell(row);
 | |
|       if (firstFocusableCell === null || firstFocusableCell >= cell) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       var prev = {
 | |
|         "row": row,
 | |
|         "cell": firstFocusableCell,
 | |
|         "posX": firstFocusableCell
 | |
|       };
 | |
|       var pos;
 | |
|       while (true) {
 | |
|         pos = gotoRight(prev.row, prev.cell, prev.posX);
 | |
|         if (!pos) {
 | |
|           return null;
 | |
|         }
 | |
|         if (pos.cell >= cell) {
 | |
|           return prev;
 | |
|         }
 | |
|         prev = pos;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function gotoDown(row, cell, posX) {
 | |
|       var prevCell;
 | |
|       var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
 | |
|       while (true) {
 | |
|         if (++row >= dataLengthIncludingAddNew) {
 | |
|           return null;
 | |
|         }
 | |
| 
 | |
|         prevCell = cell = 0;
 | |
|         while (cell <= posX) {
 | |
|           prevCell = cell;
 | |
|           cell += getColspan(row, cell);
 | |
|         }
 | |
| 
 | |
|         if (canCellBeActive(row, prevCell)) {
 | |
|           return {
 | |
|             "row": row,
 | |
|             "cell": prevCell,
 | |
|             "posX": posX
 | |
|           };
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function gotoUp(row, cell, posX) {
 | |
|       var prevCell;
 | |
|       while (true) {
 | |
|         if (--row < 0) {
 | |
|           return null;
 | |
|         }
 | |
| 
 | |
|         prevCell = cell = 0;
 | |
|         while (cell <= posX) {
 | |
|           prevCell = cell;
 | |
|           cell += getColspan(row, cell);
 | |
|         }
 | |
| 
 | |
|         if (canCellBeActive(row, prevCell)) {
 | |
|           return {
 | |
|             "row": row,
 | |
|             "cell": prevCell,
 | |
|             "posX": posX
 | |
|           };
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function gotoNext(row, cell, posX) {
 | |
|       if (row == null && cell == null) {
 | |
|         row = cell = posX = 0;
 | |
|         if (canCellBeActive(row, cell)) {
 | |
|           return {
 | |
|             "row": row,
 | |
|             "cell": cell,
 | |
|             "posX": cell
 | |
|           };
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var pos = gotoRight(row, cell, posX);
 | |
|       if (pos) {
 | |
|         return pos;
 | |
|       }
 | |
| 
 | |
|       var firstFocusableCell = null;
 | |
|       var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
 | |
|       while (++row < dataLengthIncludingAddNew) {
 | |
|         firstFocusableCell = findFirstFocusableCell(row);
 | |
|         if (firstFocusableCell !== null) {
 | |
|           return {
 | |
|             "row": row,
 | |
|             "cell": firstFocusableCell,
 | |
|             "posX": firstFocusableCell
 | |
|           };
 | |
|         }
 | |
|       }
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function gotoPrev(row, cell, posX) {
 | |
|       if (row == null && cell == null) {
 | |
|         row = getDataLengthIncludingAddNew() - 1;
 | |
|         cell = posX = columns.length - 1;
 | |
|         if (canCellBeActive(row, cell)) {
 | |
|           return {
 | |
|             "row": row,
 | |
|             "cell": cell,
 | |
|             "posX": cell
 | |
|           };
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       var pos;
 | |
|       var lastSelectableCell;
 | |
|       while (!pos) {
 | |
|         pos = gotoLeft(row, cell, posX);
 | |
|         if (pos) {
 | |
|           break;
 | |
|         }
 | |
|         if (--row < 0) {
 | |
|           return null;
 | |
|         }
 | |
| 
 | |
|         cell = 0;
 | |
|         lastSelectableCell = findLastFocusableCell(row);
 | |
|         if (lastSelectableCell !== null) {
 | |
|           pos = {
 | |
|             "row": row,
 | |
|             "cell": lastSelectableCell,
 | |
|             "posX": lastSelectableCell
 | |
|           };
 | |
|         }
 | |
|       }
 | |
|       return pos;
 | |
|     }
 | |
| 
 | |
|     function navigateRight() {
 | |
|       return navigate("right");
 | |
|     }
 | |
| 
 | |
|     function navigateLeft() {
 | |
|       return navigate("left");
 | |
|     }
 | |
| 
 | |
|     function navigateDown() {
 | |
|       return navigate("down");
 | |
|     }
 | |
| 
 | |
|     function navigateUp() {
 | |
|       return navigate("up");
 | |
|     }
 | |
| 
 | |
|     function navigateNext() {
 | |
|       return navigate("next");
 | |
|     }
 | |
| 
 | |
|     function navigatePrev() {
 | |
|       return navigate("prev");
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param {string} dir Navigation direction.
 | |
|      * @return {boolean} Whether navigation resulted in a change of active cell.
 | |
|      */
 | |
|     function navigate(dir) {
 | |
|       if (!options.enableCellNavigation) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       if (!activeCellNode && dir != "prev" && dir != "next") {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       if (!getEditorLock().commitCurrentEdit()) {
 | |
|         return true;
 | |
|       }
 | |
|       setFocus();
 | |
| 
 | |
|       var tabbingDirections = {
 | |
|         "up": -1,
 | |
|         "down": 1,
 | |
|         "left": -1,
 | |
|         "right": 1,
 | |
|         "prev": -1,
 | |
|         "next": 1
 | |
|       };
 | |
|       tabbingDirection = tabbingDirections[dir];
 | |
| 
 | |
|       var stepFunctions = {
 | |
|         "up": gotoUp,
 | |
|         "down": gotoDown,
 | |
|         "left": gotoLeft,
 | |
|         "right": gotoRight,
 | |
|         "prev": gotoPrev,
 | |
|         "next": gotoNext
 | |
|       };
 | |
|       var stepFn = stepFunctions[dir];
 | |
|       var pos = stepFn(activeRow, activeCell, activePosX);
 | |
|       if (pos) {
 | |
|         var isAddNewRow = (pos.row == getDataLength());
 | |
|         scrollCellIntoView(pos.row, pos.cell, !isAddNewRow);
 | |
|         setActiveCellInternal(getCellNode(pos.row, pos.cell));
 | |
|         activePosX = pos.posX;
 | |
|         return true;
 | |
|       } else {
 | |
|         setActiveCellInternal(getCellNode(activeRow, activeCell));
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function getCellNode(row, cell) {
 | |
|       if (rowsCache[row]) {
 | |
|         ensureCellNodesInRowsCache(row);
 | |
|         return rowsCache[row].cellNodesByColumnIdx[cell];
 | |
|       }
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function setActiveCell(row, cell) {
 | |
|       if (!initialized) { return; }
 | |
|       if (row > getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (!options.enableCellNavigation) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       scrollCellIntoView(row, cell, false);
 | |
|       setActiveCellInternal(getCellNode(row, cell), false);
 | |
|     }
 | |
| 
 | |
|     function canCellBeActive(row, cell) {
 | |
|       if (!options.enableCellNavigation || row >= getDataLengthIncludingAddNew() ||
 | |
|           row < 0 || cell >= columns.length || cell < 0) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
 | |
|       if (rowMetadata && typeof rowMetadata.focusable === "boolean") {
 | |
|         return rowMetadata.focusable;
 | |
|       }
 | |
| 
 | |
|       var columnMetadata = rowMetadata && rowMetadata.columns;
 | |
|       if (columnMetadata && columnMetadata[columns[cell].id] && typeof columnMetadata[columns[cell].id].focusable === "boolean") {
 | |
|         return columnMetadata[columns[cell].id].focusable;
 | |
|       }
 | |
|       if (columnMetadata && columnMetadata[cell] && typeof columnMetadata[cell].focusable === "boolean") {
 | |
|         return columnMetadata[cell].focusable;
 | |
|       }
 | |
| 
 | |
|       return columns[cell].focusable;
 | |
|     }
 | |
| 
 | |
|     function canCellBeSelected(row, cell) {
 | |
|       if (row >= getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
 | |
|       if (rowMetadata && typeof rowMetadata.selectable === "boolean") {
 | |
|         return rowMetadata.selectable;
 | |
|       }
 | |
| 
 | |
|       var columnMetadata = rowMetadata && rowMetadata.columns && (rowMetadata.columns[columns[cell].id] || rowMetadata.columns[cell]);
 | |
|       if (columnMetadata && typeof columnMetadata.selectable === "boolean") {
 | |
|         return columnMetadata.selectable;
 | |
|       }
 | |
| 
 | |
|       return columns[cell].selectable;
 | |
|     }
 | |
| 
 | |
|     function gotoCell(row, cell, forceEdit) {
 | |
|       if (!initialized) { return; }
 | |
|       if (!canCellBeActive(row, cell)) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (!getEditorLock().commitCurrentEdit()) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       scrollCellIntoView(row, cell, false);
 | |
| 
 | |
|       var newCell = getCellNode(row, cell);
 | |
| 
 | |
|       // if selecting the 'add new' row, start editing right away
 | |
|       setActiveCellInternal(newCell, forceEdit || (row === getDataLength()) || options.autoEdit);
 | |
| 
 | |
|       // if no editor was created, set the focus back on the grid
 | |
|       if (!currentEditor) {
 | |
|         setFocus();
 | |
|       }
 | |
|     }
 | |
| 
 | |
| 
 | |
|     //////////////////////////////////////////////////////////////////////////////////////////////
 | |
|     // IEditor implementation for the editor lock
 | |
| 
 | |
|     function commitCurrentEdit() {
 | |
|       var item = getDataItem(activeRow);
 | |
|       var column = columns[activeCell];
 | |
| 
 | |
|       if (currentEditor) {
 | |
|         if (currentEditor.isValueChanged()) {
 | |
|           var validationResults = currentEditor.validate();
 | |
| 
 | |
|           if (validationResults.valid) {
 | |
|             if (activeRow < getDataLength()) {
 | |
|               var editCommand = {
 | |
|                 row: activeRow,
 | |
|                 cell: activeCell,
 | |
|                 editor: currentEditor,
 | |
|                 serializedValue: currentEditor.serializeValue(),
 | |
|                 prevSerializedValue: serializedEditorValue,
 | |
|                 execute: function () {
 | |
|                   this.editor.applyValue(item, this.serializedValue);
 | |
|                   updateRow(this.row);
 | |
|                   trigger(self.onCellChange, {
 | |
|                     row: activeRow,
 | |
|                     cell: activeCell,
 | |
|                     item: item
 | |
|                   });
 | |
|                 },
 | |
|                 undo: function () {
 | |
|                   this.editor.applyValue(item, this.prevSerializedValue);
 | |
|                   updateRow(this.row);
 | |
|                   trigger(self.onCellChange, {
 | |
|                     row: activeRow,
 | |
|                     cell: activeCell,
 | |
|                     item: item
 | |
|                   });
 | |
|                 }
 | |
|               };
 | |
| 
 | |
|               if (options.editCommandHandler) {
 | |
|                 makeActiveCellNormal();
 | |
|                 options.editCommandHandler(item, column, editCommand);
 | |
|               } else {
 | |
|                 editCommand.execute();
 | |
|                 makeActiveCellNormal();
 | |
|               }
 | |
| 
 | |
|             } else {
 | |
|               var newItem = {};
 | |
|               currentEditor.applyValue(newItem, currentEditor.serializeValue());
 | |
|               makeActiveCellNormal();
 | |
|               trigger(self.onAddNewRow, {item: newItem, column: column});
 | |
|             }
 | |
| 
 | |
|             // check whether the lock has been re-acquired by event handlers
 | |
|             return !getEditorLock().isActive();
 | |
|           } else {
 | |
|             // Re-add the CSS class to trigger transitions, if any.
 | |
|             $(activeCellNode).removeClass("invalid");
 | |
|             $(activeCellNode).width();  // force layout
 | |
|             $(activeCellNode).addClass("invalid");
 | |
| 
 | |
|             trigger(self.onValidationError, {
 | |
|               editor: currentEditor,
 | |
|               cellNode: activeCellNode,
 | |
|               validationResults: validationResults,
 | |
|               row: activeRow,
 | |
|               cell: activeCell,
 | |
|               column: column
 | |
|             });
 | |
| 
 | |
|             currentEditor.focus();
 | |
|             return false;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         makeActiveCellNormal();
 | |
|       }
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     function cancelCurrentEdit() {
 | |
|       makeActiveCellNormal();
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     function rowsToRanges(rows) {
 | |
|       var ranges = [];
 | |
|       var lastCell = columns.length - 1;
 | |
|       for (var i = 0; i < rows.length; i++) {
 | |
|         ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell));
 | |
|       }
 | |
|       return ranges;
 | |
|     }
 | |
| 
 | |
|     function getSelectedRows() {
 | |
|       if (!selectionModel) {
 | |
|         throw "Selection model is not set";
 | |
|       }
 | |
|       return selectedRows;
 | |
|     }
 | |
| 
 | |
|     function setSelectedRows(rows) {
 | |
|       if (!selectionModel) {
 | |
|         throw "Selection model is not set";
 | |
|       }
 | |
|       selectionModel.setSelectedRanges(rowsToRanges(rows));
 | |
|     }
 | |
| 
 | |
| 
 | |
|     //////////////////////////////////////////////////////////////////////////////////////////////
 | |
|     // Debug
 | |
| 
 | |
|     this.debug = function () {
 | |
|       var s = "";
 | |
| 
 | |
|       s += ("\n" + "counter_rows_rendered:  " + counter_rows_rendered);
 | |
|       s += ("\n" + "counter_rows_removed:  " + counter_rows_removed);
 | |
|       s += ("\n" + "renderedRows:  " + renderedRows);
 | |
|       s += ("\n" + "numVisibleRows:  " + numVisibleRows);
 | |
|       s += ("\n" + "maxSupportedCssHeight:  " + maxSupportedCssHeight);
 | |
|       s += ("\n" + "n(umber of pages):  " + n);
 | |
|       s += ("\n" + "(current) page:  " + page);
 | |
|       s += ("\n" + "page height (ph):  " + ph);
 | |
|       s += ("\n" + "vScrollDir:  " + vScrollDir);
 | |
| 
 | |
|       alert(s);
 | |
|     };
 | |
| 
 | |
|     // a debug helper to be able to access private members
 | |
|     this.eval = function (expr) {
 | |
|       return eval(expr);
 | |
|     };
 | |
| 
 | |
|     //////////////////////////////////////////////////////////////////////////////////////////////
 | |
|     // Public API
 | |
| 
 | |
|     $.extend(this, {
 | |
|       "slickGridVersion": "2.1",
 | |
| 
 | |
|       // Events
 | |
|       "onScroll": new Slick.Event(),
 | |
|       "onSort": new Slick.Event(),
 | |
|       "onHeaderMouseEnter": new Slick.Event(),
 | |
|       "onHeaderMouseLeave": new Slick.Event(),
 | |
|       "onHeaderContextMenu": new Slick.Event(),
 | |
|       "onHeaderClick": new Slick.Event(),
 | |
|       "onHeaderCellRendered": new Slick.Event(),
 | |
|       "onBeforeHeaderCellDestroy": new Slick.Event(),
 | |
|       "onHeaderRowCellRendered": new Slick.Event(),
 | |
|       "onBeforeHeaderRowCellDestroy": new Slick.Event(),
 | |
|       "onMouseEnter": new Slick.Event(),
 | |
|       "onMouseLeave": new Slick.Event(),
 | |
|       "onClick": new Slick.Event(),
 | |
|       "onDblClick": new Slick.Event(),
 | |
|       "onContextMenu": new Slick.Event(),
 | |
|       "onKeyDown": new Slick.Event(),
 | |
|       "onAddNewRow": new Slick.Event(),
 | |
|       "onValidationError": new Slick.Event(),
 | |
|       "onViewportChanged": new Slick.Event(),
 | |
|       "onColumnsReordered": new Slick.Event(),
 | |
|       "onColumnsResized": new Slick.Event(),
 | |
|       "onCellChange": new Slick.Event(),
 | |
|       "onBeforeEditCell": new Slick.Event(),
 | |
|       "onBeforeCellEditorDestroy": new Slick.Event(),
 | |
|       "onBeforeDestroy": new Slick.Event(),
 | |
|       "onActiveCellChanged": new Slick.Event(),
 | |
|       "onActiveCellPositionChanged": new Slick.Event(),
 | |
|       "onDragInit": new Slick.Event(),
 | |
|       "onDragStart": new Slick.Event(),
 | |
|       "onDrag": new Slick.Event(),
 | |
|       "onDragEnd": new Slick.Event(),
 | |
|       "onSelectedRowsChanged": new Slick.Event(),
 | |
|       "onCellCssStylesChanged": new Slick.Event(),
 | |
| 
 | |
|       // Methods
 | |
|       "registerPlugin": registerPlugin,
 | |
|       "unregisterPlugin": unregisterPlugin,
 | |
|       "getColumns": getColumns,
 | |
|       "setColumns": setColumns,
 | |
|       "getColumnIndex": getColumnIndex,
 | |
|       "updateColumnHeader": updateColumnHeader,
 | |
|       "setSortColumn": setSortColumn,
 | |
|       "setSortColumns": setSortColumns,
 | |
|       "getSortColumns": getSortColumns,
 | |
|       "autosizeColumns": autosizeColumns,
 | |
|       "getOptions": getOptions,
 | |
|       "setOptions": setOptions,
 | |
|       "getData": getData,
 | |
|       "getDataLength": getDataLength,
 | |
|       "getDataItem": getDataItem,
 | |
|       "setData": setData,
 | |
|       "getSelectionModel": getSelectionModel,
 | |
|       "setSelectionModel": setSelectionModel,
 | |
|       "getSelectedRows": getSelectedRows,
 | |
|       "setSelectedRows": setSelectedRows,
 | |
|       "getContainerNode": getContainerNode,
 | |
| 
 | |
|       "render": render,
 | |
|       "invalidate": invalidate,
 | |
|       "invalidateRow": invalidateRow,
 | |
|       "invalidateRows": invalidateRows,
 | |
|       "invalidateAllRows": invalidateAllRows,
 | |
|       "updateCell": updateCell,
 | |
|       "updateRow": updateRow,
 | |
|       "getViewport": getVisibleRange,
 | |
|       "getRenderedRange": getRenderedRange,
 | |
|       "resizeCanvas": resizeCanvas,
 | |
|       "updateRowCount": updateRowCount,
 | |
|       "scrollRowIntoView": scrollRowIntoView,
 | |
|       "scrollRowToTop": scrollRowToTop,
 | |
|       "scrollCellIntoView": scrollCellIntoView,
 | |
|       "getCanvasNode": getCanvasNode,
 | |
|       "focus": setFocus,
 | |
| 
 | |
|       "getCellFromPoint": getCellFromPoint,
 | |
|       "getCellFromEvent": getCellFromEvent,
 | |
|       "getActiveCell": getActiveCell,
 | |
|       "setActiveCell": setActiveCell,
 | |
|       "getActiveCellNode": getActiveCellNode,
 | |
|       "getActiveCellPosition": getActiveCellPosition,
 | |
|       "resetActiveCell": resetActiveCell,
 | |
|       "editActiveCell": makeActiveCellEditable,
 | |
|       "getCellEditor": getCellEditor,
 | |
|       "getCellNode": getCellNode,
 | |
|       "getCellNodeBox": getCellNodeBox,
 | |
|       "canCellBeSelected": canCellBeSelected,
 | |
|       "canCellBeActive": canCellBeActive,
 | |
|       "navigatePrev": navigatePrev,
 | |
|       "navigateNext": navigateNext,
 | |
|       "navigateUp": navigateUp,
 | |
|       "navigateDown": navigateDown,
 | |
|       "navigateLeft": navigateLeft,
 | |
|       "navigateRight": navigateRight,
 | |
|       "navigatePageUp": navigatePageUp,
 | |
|       "navigatePageDown": navigatePageDown,
 | |
|       "gotoCell": gotoCell,
 | |
|       "getTopPanel": getTopPanel,
 | |
|       "setTopPanelVisibility": setTopPanelVisibility,
 | |
|       "setHeaderRowVisibility": setHeaderRowVisibility,
 | |
|       "getHeaderRow": getHeaderRow,
 | |
|       "getHeaderRowColumn": getHeaderRowColumn,
 | |
|       "getGridPosition": getGridPosition,
 | |
|       "flashCell": flashCell,
 | |
|       "addCellCssStyles": addCellCssStyles,
 | |
|       "setCellCssStyles": setCellCssStyles,
 | |
|       "removeCellCssStyles": removeCellCssStyles,
 | |
|       "getCellCssStyles": getCellCssStyles,
 | |
| 
 | |
|       "init": finishInitialization,
 | |
|       "destroy": destroy,
 | |
| 
 | |
|       // IEditor implementation
 | |
|       "getEditorLock": getEditorLock,
 | |
|       "getEditController": getEditController
 | |
|     });
 | |
| 
 | |
|     init();
 | |
|   }
 | |
| }(jQuery));
 |