PageSourceSearch

https://portal.uicalaska.com/Scripts/SlickGrid/slick.grid.js

js uicalaska.com collected 2026-09-25 04:13:22 UTC 108,104 bytes, 3,422 lines download raw bytes

1/**
2 * @license
3 * (c) 2009-2013 Michael Leibman
4 * michael{dot}leibman{at}gmail{dot}com
5 * http://github.com/mleibman/slickgrid
6 *
7 * Distributed under MIT license.
8 * All rights reserved.
9 *
10 * SlickGrid v2.2
11 *
12 * NOTES:
13 *     Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods.
14 *     This increases the speed dramatically, but can only be done safely because there are no event handlers
15 *     or data associated with any cell/row DOM nodes.  Cell editors must make sure they implement .destroy()
16 *     and do proper cleanup.
17 */
18
19// make sure required JavaScript modules are loaded
20if (typeof jQuery === "undefined") {
21  throw "SlickGrid requires jquery module to be loaded";
22}
23if (!jQuery.fn.drag) {
24  throw "SlickGrid requires jquery.event.drag module to be loaded";
25}
26if (typeof Slick === "undefined") {
27  throw "slick.core.js not loaded";
28}
29
30
31(function ($) {
32  // Slick.Grid
33  $.extend(true, window, {
34    Slick: {
35      Grid: SlickGrid
36    }
37  });
38
39  // shared across all grids on the page
40  var scrollbarDimensions;
41  var maxSupportedCssHeight;  // browser's breaking point
42
43  //////////////////////////////////////////////////////////////////////////////////////////////
44  // SlickGrid class implementation (available as Slick.Grid)
45
46  /**
47   * Creates a new instance of the grid.
48   * @class SlickGrid
49   * @constructor
50   * @param {Node}              container   Container node to create the grid in.
51   * @param {Array,Object}      data        An array of objects for databinding.
52   * @param {Array}             columns     An array of column definitions.
53   * @param {Object}            options     Grid options.
54   **/
55  function SlickGrid(container, data, columns, options) {
56    // settings
57    var defaults = {
58      explicitInitialization: false,
59      rowHeight: 25,
60      defaultColumnWidth: 80,
61      enableAddRow: false,
62      leaveSpaceForNewRows: false,
63      editable: false,
64      autoEdit: true,
65      enableCellNavigation: true,
66      enableColumnReorder: true,
67      asyncEditorLoading: false,
68      asyncEditorLoadDelay: 100,
69      forceFitColumns: false,
70      enableAsyncPostRender: false,
71      asyncPostRenderDelay: 50,
72      autoHeight: false,
73      editorLock: Slick.GlobalEditorLock,
74      showHeaderRow: false,
75      headerRowHeight: 25,
76      showTopPanel: false,
77      topPanelHeight: 25,
78      formatterFactory: null,
79      editorFactory: null,
80      cellFlashingCssClass: "flashing",
81      selectedCellCssClass: "selected",
82      multiSelect: true,
83      enableTextSelectionOnCells: false,
84      dataItemColumnValueExtractor: null,
85      fullWidthRows: false,
86      multiColumnSort: false,
87      defaultFormatter: defaultFormatter,
88      forceSyncScrolling: false,
89      addNewRowCssClass: "new-row"
90    };
91
92    var columnDefaults = {
93      name: "",
94      resizable: true,
95      sortable: false,
96      minWidth: 30,
97      rerenderOnResize: false,
98      headerCssClass: null,
99      defaultSortAsc: true,
100      focusable: true,
101      selectable: true
102    };
103
104    // scroller
105    var th;   // virtual height
106    var h;    // real scrollable height
107    var ph;   // page height
108    var n;    // number of pages
109    var cj;   // "jumpiness" coefficient
110
111    var page = 0;       // current page
112    var offset = 0;     // current page offset
113    var vScrollDir = 1;
114
115    // private
116    var initialized = false;
117    var $container;
118    var uid = "slickgrid_" + Math.round(1000000 * Math.random());
119    var self = this;
120    var $focusSink, $focusSink2;
121    var $headerScroller;
122    var $headers;
123    var $headerRow, $headerRowScroller, $headerRowSpacer;
124    var $topPanelScroller;
125    var $topPanel;
126    var $viewport;
127    var $canvas;
128    var $style;
129    var $boundAncestors;
130    var stylesheet, columnCssRulesL, columnCssRulesR;
131    var viewportH, viewportW;
132    var canvasWidth;
133    var viewportHasHScroll, viewportHasVScroll;
134    var headerColumnWidthDiff = 0, headerColumnHeightDiff = 0, // border+padding
135        cellWidthDiff = 0, cellHeightDiff = 0;
136    var absoluteColumnMinWidth;
137
138    var tabbingDirection = 1;
139    var activePosX;
140    var activeRow, activeCell;
141    var activeCellNode = null;
142    var currentEditor = null;
143    var serializedEditorValue;
144    var editController;
145
146    var rowsCache = {};
147    var renderedRows = 0;
148    var numVisibleRows;
149    var prevScrollTop = 0;
150    var scrollTop = 0;
151    var lastRenderedScrollTop = 0;
152    var lastRenderedScrollLeft = 0;
153    var prevScrollLeft = 0;
154    var scrollLeft = 0;
155
156    var selectionModel;
157    var selectedRows = [];
158
159    var plugins = [];
160    var cellCssClasses = {};
161
162    var columnsById = {};
163    var sortColumns = [];
164    var columnPosLeft = [];
165    var columnPosRight = [];
166
167
168    // async call handles
169    var h_editorLoader = null;
170    var h_render = null;
171    var h_postrender = null;
172    var postProcessedRows = {};
173    var postProcessToRow = null;
174    var postProcessFromRow = null;
175
176    // perf counters
177    var counter_rows_rendered = 0;
178    var counter_rows_removed = 0;
179
180    // These two variables work around a bug with inertial scrolling in Webkit/Blink on Mac.
181    // See http://crbug.com/312427.
182    var rowNodeFromLastMouseWheelEvent;  // this node must not be deleted while inertial scrolling
183    var zombieRowNodeFromLastMouseWheelEvent;  // node that was hidden instead of getting deleted
184
185
186    //////////////////////////////////////////////////////////////////////////////////////////////
187    // Initialization
188
189    function init() {
190      $container = $(container);
191      if ($container.length < 1) {
192        throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM.");
193      }
194
195      // calculate these only once and share between grid instances
196      maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight();
197      scrollbarDimensions = scrollbarDimensions || measureScrollbar();
198
199      options = $.extend({}, defaults, options);
200      validateAndEnforceOptions();
201      columnDefaults.width = options.defaultColumnWidth;
202
203      columnsById = {};
204      for (var i = 0; i < columns.length; i++) {
205        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
206        columnsById[m.id] = i;
207        if (m.minWidth && m.width < m.minWidth) {
208          m.width = m.minWidth;
209        }
210        if (m.maxWidth && m.width > m.maxWidth) {
211          m.width = m.maxWidth;
212        }
213      }
214
215      // validate loaded JavaScript modules against requested options
216      if (options.enableColumnReorder && !$.fn.sortable) {
217        throw new Error("SlickGrid's 'enableColumnReorder = true' option requires jquery-ui.sortable module to be loaded");
218      }
219
220      editController = {
221        "commitCurrentEdit": commitCurrentEdit,
222        "cancelCurrentEdit": cancelCurrentEdit
223      };
224
225      $container
226          .empty()
227          .css("overflow", "hidden")
228          .css("outline", 0)
229          .addClass(uid)
230          .addClass("ui-widget");
231
232      // set up a positioning container if needed
233      if (!/relative|absolute|fixed/.test($container.css("position"))) {
234        $container.css("position", "relative");
235      }
236
237      $focusSink = $("<div tabIndex='0' hideFocus style='position:fixed;width:0;height:0;top:0;left:0;outline:0;'></div>").appendTo($container);
238
239      $headerScroller = $("<div class='slick-header ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
240      $headers = $("<div class='slick-header-columns' style='left:-1000px' />").appendTo($headerScroller);
241      $headers.width(getHeadersWidth());
242
243      $headerRowScroller = $("<div class='slick-headerrow ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
244      $headerRow = $("<div class='slick-headerrow-columns' />").appendTo($headerRowScroller);
245      $headerRowSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>")
246          .css("width", getCanvasWidth() + scrollbarDimensions.width + "px")
247          .appendTo($headerRowScroller);
248
249      $topPanelScroller = $("<div class='slick-top-panel-scroller ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
250      $topPanel = $("<div class='slick-top-panel' style='width:10000px' />").appendTo($topPanelScroller);
251
252      if (!options.showTopPanel) {
253        $topPanelScroller.hide();
254      }
255
256      if (!options.showHeaderRow) {
257        $headerRowScroller.hide();
258      }
259
260      $viewport = $("<div class='slick-viewport' style='width:100%;overflow:auto;outline:0;position:relative;;'>").appendTo($container);
261      $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
262
263      $canvas = $("<div class='grid-canvas' />").appendTo($viewport);
264
265      $focusSink2 = $focusSink.clone().appendTo($container);
266
267      if (!options.explicitInitialization) {
268        finishInitialization();
269      }
270    }
271
272    function finishInitialization() {
273      if (!initialized) {
274        initialized = true;
275
276        viewportW = parseFloat($.css($container[0], "width", true));
277
278        // header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?)
279        // calculate the diff so we can set consistent sizes
280        measureCellPaddingAndBorder();
281
282        // for usability reasons, all text selection in SlickGrid is disabled
283        // with the exception of input and textarea elements (selection must
284        // be enabled there so that editors work as expected); note that
285        // selection in grid cells (grid body) is already unavailable in
286        // all browsers except IE
287        disableSelection($headers); // disable all text selection in header (including input and textarea)
288
289        if (!options.enableTextSelectionOnCells) {
290          // disable text selection in grid cells except in input and textarea elements
291          // (this is IE-specific, because selectstart event will only fire in IE)
292          $viewport.bind("selectstart.ui", function (event) {
293            return $(event.target).is("input,textarea");
294          });
295        }
296
297        updateColumnCaches();
298        createColumnHeaders();
299        setupColumnSort();
300        createCssRules();
301        resizeCanvas();
302        bindAncestorScrollEvents();
303
304        $container
305            .bind("resize.slickgrid", resizeCanvas);
306        $viewport
307            //.bind("click", handleClick)
308            .bind("scroll", handleScroll);
309        $headerScroller
310            .bind("contextmenu", handleHeaderContextMenu)
311            .bind("click", handleHeaderClick)
312            .delegate(".slick-header-column", "mouseenter", handleHeaderMouseEnter)
313            .delegate(".slick-header-column", "mouseleave", handleHeaderMouseLeave);
314        $headerRowScroller
315            .bind("scroll", handleHeaderRowScroll);
316        $focusSink.add($focusSink2)
317            .bind("keydown", handleKeyDown);
318        $canvas
319            .bind("keydown", handleKeyDown)
320            .bind("click", handleClick)
321            .bind("dblclick", handleDblClick)
322            .bind("contextmenu", handleContextMenu)
323            .bind("draginit", handleDragInit)
324            .bind("dragstart", {distance: 3}, handleDragStart)
325            .bind("drag", handleDrag)
326            .bind("dragend", handleDragEnd)
327            .delegate(".slick-cell", "mouseenter", handleMouseEnter)
328            .delegate(".slick-cell", "mouseleave", handleMouseLeave);
329
330        // Work around http://crbug.com/312427.
331        if (navigator.userAgent.toLowerCase().match(/webkit/) &&
332            navigator.userAgent.toLowerCase().match(/macintosh/)) {
333          $canvas.bind("mousewheel", handleMouseWheel);
334        }
335      }
336    }
337
338    function registerPlugin(plugin) {
339      plugins.unshift(plugin);
340      plugin.init(self);
341    }
342
343    function unregisterPlugin(plugin) {
344      for (var i = plugins.length; i >= 0; i--) {
345        if (plugins[i] === plugin) {
346          if (plugins[i].destroy) {
347            plugins[i].destroy();
348          }
349          plugins.splice(i, 1);
350          break;
351        }
352      }
353    }
354
355    function setSelectionModel(model) {
356      if (selectionModel) {
357        selectionModel.onSelectedRangesChanged.unsubscribe(handleSelectedRangesChanged);
358        if (selectionModel.destroy) {
359          selectionModel.destroy();
360        }
361      }
362
363      selectionModel = model;
364      if (selectionModel) {
365        selectionModel.init(self);
366        selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged);
367      }
368    }
369
370    function getSelectionModel() {
371      return selectionModel;
372    }
373
374    function getCanvasNode() {
375      return $canvas[0];
376    }
377
378    function measureScrollbar() {
379      var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
380      var dim = {
381        width: $c.width() - $c[0].clientWidth,
382        height: $c.height() - $c[0].clientHeight
383      };
384      $c.remove();
385      return dim;
386    }
387
388    function getHeadersWidth() {
389      var headersWidth = 0;
390      for (var i = 0, ii = columns.length; i < ii; i++) {
391        var width = columns[i].width;
392        headersWidth += width;
393      }
394      headersWidth += scrollbarDimensions.width;
395      return Math.max(headersWidth, viewportW) + 1000;
396    }
397
398    function getCanvasWidth() {
399      var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
400      var rowWidth = 0;
401      var i = columns.length;
402      while (i--) {
403        rowWidth += columns[i].width;
404      }
405      return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth;
406    }
407
408    function updateCanvasWidth(forceColumnWidthsUpdate) {
409      var oldCanvasWidth = canvasWidth;
410      canvasWidth = getCanvasWidth();
411
412      if (canvasWidth != oldCanvasWidth) {
413        $canvas.width(canvasWidth);
414        $headerRow.width(canvasWidth);
415        $headers.width(getHeadersWidth());
416        viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width);
417      }
418
419      $headerRowSpacer.width(canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0));
420
421      if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) {
422        applyColumnWidths();
423      }
424    }
425
426    function disableSelection($target) {
427      if ($target && $target.jquery) {
428        $target
429            .attr("unselectable", "on")
430            .css("MozUserSelect", "none")
431            .bind("selectstart.ui", function () {
432              return false;
433            }); // from jquery:ui.core.js 1.7.2
434      }
435    }
436
437    function getMaxSupportedCssHeight() {
438      var supportedHeight = 1000000;
439      // FF reports the height back but still renders blank after ~6M px
440      var testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000;
441      var div = $("<div style='display:none' />").appendTo(document.body);
442
443      while (true) {
444        var test = supportedHeight * 2;
445        div.css("height", test);
446        if (test > testUpTo || div.height() !== test) {
447          break;
448        } else {
449          supportedHeight = test;
450        }
451      }
452
453      div.remove();
454      return supportedHeight;
455    }
456
457    // TODO:  this is static.  need to handle page mutation.
458    function bindAncestorScrollEvents() {
459      var elem = $canvas[0];
460      while ((elem = elem.parentNode) != document.body && elem != null) {
461        // bind to scroll containers only
462        if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight) {
463          var $elem = $(elem);
464          if (!$boundAncestors) {
465            $boundAncestors = $elem;
466          } else {
467            $boundAncestors = $boundAncestors.add($elem);
468          }
469          $elem.bind("scroll." + uid, handleActiveCellPositionChange);
470        }
471      }
472    }
473
474    function unbindAncestorScrollEvents() {
475      if (!$boundAncestors) {
476        return;
477      }
478      $boundAncestors.unbind("scroll." + uid);
479      $boundAncestors = null;
480    }
481
482    function updateColumnHeader(columnId, title, toolTip) {
483      if (!initialized) { return; }
484      var idx = getColumnIndex(columnId);
485      if (idx == null) {
486        return;
487      }
488
489      var columnDef = columns[idx];
490      var $header = $headers.children().eq(idx);
491      if ($header) {
492        if (title !== undefined) {
493          columns[idx].name = title;
494        }
495        if (toolTip !== undefined) {
496          columns[idx].toolTip = toolTip;
497        }
498
499        trigger(self.onBeforeHeaderCellDestroy, {
500          "node": $header[0],
501          "column": columnDef
502        });
503
504        $header
505            .attr("title", toolTip || "")
506            .children().eq(0).html(title);
507
508        trigger(self.onHeaderCellRendered, {
509          "node": $header[0],
510          "column": columnDef
511        });
512      }
513    }
514
515    function getHeaderRow() {
516      return $headerRow[0];
517    }
518
519    function getHeaderRowColumn(columnId) {
520      var idx = getColumnIndex(columnId);
521      var $header = $headerRow.children().eq(idx);
522      return $header && $header[0];
523    }
524
525    function createColumnHeaders() {
526      function onMouseEnter() {
527        $(this).addClass("ui-state-hover");
528      }
529
530      function onMouseLeave() {
531        $(this).removeClass("ui-state-hover");
532      }
533
534      $headers.find(".slick-header-column")
535        .each(function() {
536          var columnDef = $(this).data("column");
537          if (columnDef) {
538            trigger(self.onBeforeHeaderCellDestroy, {
539              "node": this,
540              "column": columnDef
541            });
542          }
543        });
544      $headers.empty();
545      $headers.width(getHeadersWidth());
546
547      $headerRow.find(".slick-headerrow-column")
548        .each(function() {
549          var columnDef = $(this).data("column");
550          if (columnDef) {
551            trigger(self.onBeforeHeaderRowCellDestroy, {
552              "node": this,
553              "column": columnDef
554            });
555          }
556        });
557      $headerRow.empty();
558
559      for (var i = 0; i < columns.length; i++) {
560        var m = columns[i];
561
562        var header = $("<div class='ui-state-default slick-header-column' />")
563            .html("<span class='slick-column-name'>" + m.name + "</span>")
564            .width(m.width - headerColumnWidthDiff)
565            .attr("id", "" + uid + m.id)
566            .attr("title", m.toolTip || "")
567            .data("column", m)
568            .addClass(m.headerCssClass || "")
569            .appendTo($headers);
570
571        if (options.enableColumnReorder || m.sortable) {
572          header
573            .on('mouseenter', onMouseEnter)
574            .on('mouseleave', onMouseLeave);
575        }
576
577        if (m.sortable) {
578          header.addClass("slick-header-sortable");
579          header.append("<span class='slick-sort-indicator' />");
580        }
581
582        trigger(self.onHeaderCellRendered, {
583          "node": header[0],
584          "column": m
585        });
586
587        if (options.showHeaderRow) {
588          var headerRowCell = $("<div class='ui-state-default slick-headerrow-column l" + i + " r" + i + "'></div>")
589              .data("column", m)
590              .appendTo($headerRow);
591
592          trigger(self.onHeaderRowCellRendered, {
593            "node": headerRowCell[0],
594            "column": m
595          });
596        }
597      }
598
599      setSortColumns(sortColumns);
600      setupColumnResize();
601      if (options.enableColumnReorder) {
602        setupColumnReorder();
603      }
604    }
605
606    function setupColumnSort() {
607      $headers.click(function (e) {
608        // temporary workaround for a bug in jQuery 1.7.1 (http://bugs.jquery.com/ticket/11328)
609        e.metaKey = e.metaKey || e.ctrlKey;
610
611        if ($(e.target).hasClass("slick-resizable-handle")) {
612          return;
613        }
614
615        var $col = $(e.target).closest(".slick-header-column");
616        if (!$col.length) {
617          return;
618        }
619
620        var column = $col.data("column");
621        if (column.sortable) {
622          if (!getEditorLock().commitCurrentEdit()) {
623            return;
624          }
625
626          var sortOpts = null;
627          var i = 0;
628          for (; i < sortColumns.length; i++) {
629            if (sortColumns[i].columnId == column.id) {
630              sortOpts = sortColumns[i];
631              sortOpts.sortAsc = !sortOpts.sortAsc;
632              break;
633            }
634          }
635
636          if (e.metaKey && options.multiColumnSort) {
637            if (sortOpts) {
638              sortColumns.splice(i, 1);
639            }
640          }
641          else {
642            if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) {
643              sortColumns = [];
644            }
645
646            if (!sortOpts) {
647              sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc };
648              sortColumns.push(sortOpts);
649            } else if (sortColumns.length == 0) {
650              sortColumns.push(sortOpts);
651            }
652          }
653
654          setSortColumns(sortColumns);
655
656          if (!options.multiColumnSort) {
657            trigger(self.onSort, {
658              multiColumnSort: false,
659              sortCol: column,
660              sortAsc: sortOpts.sortAsc}, e);
661          } else {
662            trigger(self.onSort, {
663              multiColumnSort: true,
664              sortCols: $.map(sortColumns, function(col) {
665                return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc };
666              })}, e);
667          }
668        }
669      });
670    }
671
672    function setupColumnReorder() {
673      $headers.filter(":ui-sortable").sortable("destroy");
674      $headers.sortable({
675        containment: "parent",
676        distance: 3,
677        axis: "x",
678        cursor: "default",
679        tolerance: "intersection",
680        helper: "clone",
681        placeholder: "slick-sortable-placeholder ui-state-default slick-header-column",
682        start: function (e, ui) {
683          ui.placeholder.width(ui.helper.outerWidth() - headerColumnWidthDiff);
684          $(ui.helper).addClass("slick-header-column-active");
685        },
686        beforeStop: function (e, ui) {
687          $(ui.helper).removeClass("slick-header-column-active");
688        },
689        stop: function (e) {
690          if (!getEditorLock().commitCurrentEdit()) {
691            $(this).sortable("cancel");
692            return;
693          }
694
695          var reorderedIds = $headers.sortable("toArray");
696          var reorderedColumns = [];
697          for (var i = 0; i < reorderedIds.length; i++) {
698            reorderedColumns.push(columns[getColumnIndex(reorderedIds[i].replace(uid, ""))]);
699          }
700          setColumns(reorderedColumns);
701
702          trigger(self.onColumnsReordered, {});
703          e.stopPropagation();
704          setupColumnResize();
705        }
706      });
707    }
708
709    function setupColumnResize() {
710      var $col, j, c, pageX, columnElements, minPageX, maxPageX, firstResizable, lastResizable;
711      columnElements = $headers.children();
712      columnElements.find(".slick-resizable-handle").remove();
713      columnElements.each(function (i, e) {
714        if (columns[i].resizable) {
715          if (firstResizable === undefined) {
716            firstResizable = i;
717          }
718          lastResizable = i;
719        }
720      });
721      if (firstResizable === undefined) {
722        return;
723      }
724      columnElements.each(function (i, e) {
725        if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) {
726          return;
727        }
728        $col = $(e);
729        $("<div class='slick-resizable-handle' />")
730            .appendTo(e)
731            .bind("dragstart", function (e, dd) {
732              if (!getEditorLock().commitCurrentEdit()) {
733                return false;
734              }
735              pageX = event.pageX;
736              $(this).parent().addClass("slick-header-column-active");
737              var shrinkLeewayOnRight = null, stretchLeewayOnRight = null;
738              // lock each column's width option to current width
739              columnElements.each(function (i, e) {
740                columns[i].previousWidth = $(e).outerWidth();
741              });
742              if (options.forceFitColumns) {
743                shrinkLeewayOnRight = 0;
744                stretchLeewayOnRight = 0;
745                // colums on right affect maxPageX/minPageX
746                for (j = i + 1; j < columnElements.length; j++) {
747                  c = columns[j];
748                  if (c.resizable) {
749                    if (stretchLeewayOnRight !== null) {
750                      if (c.maxWidth) {
751                        stretchLeewayOnRight += c.maxWidth - c.previousWidth;
752                      } else {
753                        stretchLeewayOnRight = null;
754                      }
755                    }
756                    shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
757                  }
758                }
759              }
760              var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0;
761              for (j = 0; j <= i; j++) {
762                // columns on left only affect minPageX
763                c = columns[j];
764                if (c.resizable) {
765                  if (stretchLeewayOnLeft !== null) {
766                    if (c.maxWidth) {
767                      stretchLeewayOnLeft += c.maxWidth - c.previousWidth;
768                    } else {
769                      stretchLeewayOnLeft = null;
770                    }
771                  }
772                  shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
773                }
774              }
775              if (shrinkLeewayOnRight === null) {
776                shrinkLeewayOnRight = 100000;
777              }
778              if (shrinkLeewayOnLeft === null) {
779                shrinkLeewayOnLeft = 100000;
780              }
781              if (stretchLeewayOnRight === null) {
782                stretchLeewayOnRight = 100000;
783              }
784              if (stretchLeewayOnLeft === null) {
785                stretchLeewayOnLeft = 100000;
786              }
787              maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
788              minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
789            })
790            .bind("drag", function (e, dd) {
791              var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, event.pageX)) - pageX, x;
792              if (d < 0) { // shrink column
793                x = d;
794                for (j = i; j >= 0; j--) {
795                  c = columns[j];
796                  if (c.resizable) {
797                    actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
798                    if (x && c.previousWidth + x < actualMinWidth) {
799                      x += c.previousWidth - actualMinWidth;
800                      c.width = actualMinWidth;
801                    } else {
802                      c.width = c.previousWidth + x;
803                      x = 0;
804                    }
805                  }
806                }
807
808                if (options.forceFitColumns) {
809                  x = -d;
810                  for (j = i + 1; j < columnElements.length; j++) {
811                    c = columns[j];
812                    if (c.resizable) {
813                      if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
814                        x -= c.maxWidth - c.previousWidth;
815                        c.width = c.maxWidth;
816                      } else {
817                        c.width = c.previousWidth + x;
818                        x = 0;
819                      }
820                    }
821                  }
822                }
823              } else { // stretch column
824                x = d;
825                for (j = i; j >= 0; j--) {
826                  c = columns[j];
827                  if (c.resizable) {
828                    if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
829                      x -= c.maxWidth - c.previousWidth;
830                      c.width = c.maxWidth;
831                    } else {
832                      c.width = c.previousWidth + x;
833                      x = 0;
834                    }
835                  }
836                }
837
838                if (options.forceFitColumns) {
839                  x = -d;
840                  for (j = i + 1; j < columnElements.length; j++) {
841                    c = columns[j];
842                    if (c.resizable) {
843                      actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
844                      if (x && c.previousWidth + x < actualMinWidth) {
845                        x += c.previousWidth - actualMinWidth;
846                        c.width = actualMinWidth;
847                      } else {
848                        c.width = c.previousWidth + x;
849                        x = 0;
850                      }
851                    }
852                  }
853                }
854              }
855              applyColumnHeaderWidths();
856              if (options.syncColumnCellResize) {
857                applyColumnWidths();
858              }
859            })
860            .bind("dragend", function (e, dd) {
861              var newWidth;
862              $(this).parent().removeClass("slick-header-column-active");
863              for (j = 0; j < columnElements.length; j++) {
864                c = columns[j];
865                newWidth = $(columnElements[j]).outerWidth();
866
867                if (c.previousWidth !== newWidth && c.rerenderOnResize) {
868                  invalidateAllRows();
869                }
870              }
871              updateCanvasWidth(true);
872              render();
873              trigger(self.onColumnsResized, {});
874            });
875      });
876    }
877
878    function getVBoxDelta($el) {
879      var p = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
880      var delta = 0;
881      $.each(p, function (n, val) {
882        delta += parseFloat($el.css(val)) || 0;
883      });
884      return delta;
885    }
886
887    function measureCellPaddingAndBorder() {
888      var el;
889      var h = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"];
890      var v = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
891
892      el = $("<div class='ui-state-default slick-header-column' style='visibility:hidden'>-</div>").appendTo($headers);
893      headerColumnWidthDiff = headerColumnHeightDiff = 0;
894      if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") {
895        $.each(h, function (n, val) {
896          headerColumnWidthDiff += parseFloat(el.css(val)) || 0;
897        });
898        $.each(v, function (n, val) {
899          headerColumnHeightDiff += parseFloat(el.css(val)) || 0;
900        });
901      }
902      el.remove();
903
904      var r = $("<div class='slick-row' />").appendTo($canvas);
905      el = $("<div class='slick-cell' id='' style='visibility:hidden'>-</div>").appendTo(r);
906      cellWidthDiff = cellHeightDiff = 0;
907      if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") {
908        $.each(h, function (n, val) {
909          cellWidthDiff += parseFloat(el.css(val)) || 0;
910        });
911        $.each(v, function (n, val) {
912          cellHeightDiff += parseFloat(el.css(val)) || 0;
913        });
914      }
915      r.remove();
916
917      absoluteColumnMinWidth = Math.max(headerColumnWidthDiff, cellWidthDiff);
918    }
919
920    function createCssRules() {
921      $style = $("<style type='text/css' rel='stylesheet' />").appendTo($("head"));
922      var rowHeight = (options.rowHeight - cellHeightDiff);
923      var rules = [
924        "." + uid + " .slick-header-column { left: 1000px; }",
925        "." + uid + " .slick-top-panel { height:" + options.topPanelHeight + "px; }",
926        "." + uid + " .slick-headerrow-columns { height:" + options.headerRowHeight + "px; }",
927        "." + uid + " .slick-cell { height:" + rowHeight + "px; }",
928        "." + uid + " .slick-row { height:" + options.rowHeight + "px; }"
929      ];
930
931      for (var i = 0; i < columns.length; i++) {
932        rules.push("." + uid + " .l" + i + " { }");
933        rules.push("." + uid + " .r" + i + " { }");
934      }
935
936      if ($style[0].styleSheet) { // IE
937        $style[0].styleSheet.cssText = rules.join(" ");
938      } else {
939        $style[0].appendChild(document.createTextNode(rules.join(" ")));
940      }
941    }
942
943    function getColumnCssRules(idx) {
944      if (!stylesheet) {
945        var sheets = document.styleSheets;
946        for (var i = 0; i < sheets.length; i++) {
947          if ((sheets[i].ownerNode || sheets[i].owningElement) == $style[0]) {
948            stylesheet = sheets[i];
949            break;
950          }
951        }
952
953        if (!stylesheet) {
954          throw new Error("Cannot find stylesheet.");
955        }
956
957        // find and cache column CSS rules
958        columnCssRulesL = [];
959        columnCssRulesR = [];
960        var cssRules = (stylesheet.cssRules || stylesheet.rules);
961        var matches, columnIdx;
962        for (var i = 0; i < cssRules.length; i++) {
963          var selector = cssRules[i].selectorText;
964          if (matches = /\.l\d+/.exec(selector)) {
965            columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10);
966            columnCssRulesL[columnIdx] = cssRules[i];
967          } else if (matches = /\.r\d+/.exec(selector)) {
968            columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10);
969            columnCssRulesR[columnIdx] = cssRules[i];
970          }
971        }
972      }
973
974      return {
975        "left": columnCssRulesL[idx],
976        "right": columnCssRulesR[idx]
977      };
978    }
979
980    function removeCssRules() {
981      $style.remove();
982      stylesheet = null;
983    }
984
985    function destroy() {
986      getEditorLock().cancelCurrentEdit();
987
988      trigger(self.onBeforeDestroy, {});
989
990      var i = plugins.length;
991      while(i--) {
992        unregisterPlugin(plugins[i]);
993      }
994
995      if (options.enableColumnReorder) {
996          $headers.filter(":ui-sortable").sortable("destroy");
997      }
998
999      unbindAncestorScrollEvents();
1000      $container.unbind(".slickgrid");
1001      removeCssRules();
1002
1003      $canvas.unbind("draginit dragstart dragend drag");
1004      $container.empty().removeClass(uid);
1005    }
1006
1007
1008    //////////////////////////////////////////////////////////////////////////////////////////////
1009    // General
1010
1011    function trigger(evt, args, e) {
1012      e = e || new Slick.EventData();
1013      args = args || {};
1014      args.grid = self;
1015      return evt.notify(args, e, self);
1016    }
1017
1018    function getEditorLock() {
1019      return options.editorLock;
1020    }
1021
1022    function getEditController() {
1023      return editController;
1024    }
1025
1026    function getColumnIndex(id) {
1027      return columnsById[id];
1028    }
1029
1030    function autosizeColumns() {
1031      var i, c,
1032          widths = [],
1033          shrinkLeeway = 0,
1034          total = 0,
1035          prevTotal,
1036          availWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
1037
1038      for (i = 0; i < columns.length; i++) {
1039        c = columns[i];
1040        widths.push(c.width);
1041        total += c.width;
1042        if (c.resizable) {
1043          shrinkLeeway += c.width - Math.max(c.minWidth, absoluteColumnMinWidth);
1044        }
1045      }
1046
1047      // shrink
1048      prevTotal = total;
1049      while (total > availWidth && shrinkLeeway) {
1050        var shrinkProportion = (total - availWidth) / shrinkLeeway;
1051        for (i = 0; i < columns.length && total > availWidth; i++) {
1052          c = columns[i];
1053          var width = widths[i];
1054          if (!c.resizable || width <= c.minWidth || width <= absoluteColumnMinWidth) {
1055            continue;
1056          }
1057          var absMinWidth = Math.max(c.minWidth, absoluteColumnMinWidth);
1058          var shrinkSize = Math.floor(shrinkProportion * (width - absMinWidth)) || 1;
1059          shrinkSize = Math.min(shrinkSize, width - absMinWidth);
1060          total -= shrinkSize;
1061          shrinkLeeway -= shrinkSize;
1062          widths[i] -= shrinkSize;
1063        }
1064        if (prevTotal <= total) {  // avoid infinite loop
1065          break;
1066        }
1067        prevTotal = total;
1068      }
1069
1070      // grow
1071      prevTotal = total;
1072      while (total < availWidth) {
1073        var growProportion = availWidth / total;
1074        for (i = 0; i < columns.length && total < availWidth; i++) {
1075          c = columns[i];
1076          var currentWidth = widths[i];
1077          var growSize;
1078
1079          if (!c.resizable || c.maxWidth <= currentWidth) {
1080            growSize = 0;
1081          } else {
1082            growSize = Math.min(Math.floor(growProportion * currentWidth) - currentWidth, (c.maxWidth - currentWidth) || 1000000) || 1;
1083          }
1084          total += growSize;
1085          widths[i] += growSize;
1086        }
1087        if (prevTotal >= total) {  // avoid infinite loop
1088          break;
1089        }
1090        prevTotal = total;
1091      }
1092
1093      var reRender = false;
1094      for (i = 0; i < columns.length; i++) {
1095        if (columns[i].rerenderOnResize && columns[i].width != widths[i]) {
1096          reRender = true;
1097        }
1098        columns[i].width = widths[i];
1099      }
1100
1101      applyColumnHeaderWidths();
1102      updateCanvasWidth(true);
1103      if (reRender) {
1104        invalidateAllRows();
1105        render();
1106      }
1107    }
1108
1109    function applyColumnHeaderWidths() {
1110      if (!initialized) { return; }
1111      var h;
1112      for (var i = 0, headers = $headers.children(), ii = headers.length; i < ii; i++) {
1113        h = $(headers[i]);
1114        if (h.width() !== columns[i].width - headerColumnWidthDiff) {
1115          h.width(columns[i].width - headerColumnWidthDiff);
1116        }
1117      }
1118
1119      updateColumnCaches();
1120    }
1121
1122    function applyColumnWidths() {
1123      var x = 0, w, rule;
1124      for (var i = 0; i < columns.length; i++) {
1125        w = columns[i].width;
1126
1127        rule = getColumnCssRules(i);
1128        rule.left.style.left = x + "px";
1129        rule.right.style.right = (canvasWidth - x - w) + "px";
1130
1131        x += columns[i].width;
1132      }
1133    }
1134
1135    function setSortColumn(columnId, ascending) {
1136      setSortColumns([{ columnId: columnId, sortAsc: ascending}]);
1137    }
1138
1139    function setSortColumns(cols) {
1140      sortColumns = cols;
1141
1142      var headerColumnEls = $headers.children();
1143      headerColumnEls
1144          .removeClass("slick-header-column-sorted")
1145          .find(".slick-sort-indicator")
1146              .removeClass("slick-sort-indicator-asc slick-sort-indicator-desc");
1147
1148      $.each(sortColumns, function(i, col) {
1149        if (col.sortAsc == null) {
1150          col.sortAsc = true;
1151        }
1152        var columnIndex = getColumnIndex(col.columnId);
1153        if (columnIndex != null) {
1154          headerColumnEls.eq(columnIndex)
1155              .addClass("slick-header-column-sorted")
1156              .find(".slick-sort-indicator")
1157                  .addClass(col.sortAsc ? "slick-sort-indicator-asc" : "slick-sort-indicator-desc");
1158        }
1159      });
1160    }
1161
1162    function getSortColumns() {
1163      return sortColumns;
1164    }
1165
1166    function handleSelectedRangesChanged(e, ranges) {
1167      selectedRows = [];
1168      var hash = {};
1169      for (var i = 0; i < ranges.length; i++) {
1170        for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
1171          if (!hash[j]) {  // prevent duplicates
1172            selectedRows.push(j);
1173            hash[j] = {};
1174          }
1175          for (var k = ranges[i].fromCell; k <= ranges[i].toCell; k++) {
1176            if (canCellBeSelected(j, k)) {
1177              hash[j][columns[k].id] = options.selectedCellCssClass;
1178            }
1179          }
1180        }
1181      }
1182
1183      setCellCssStyles(options.selectedCellCssClass, hash);
1184
1185      trigger(self.onSelectedRowsChanged, {rows: getSelectedRows()}, e);
1186    }
1187
1188    function getColumns() {
1189      return columns;
1190    }
1191
1192    function updateColumnCaches() {
1193      // Pre-calculate cell boundaries.
1194      columnPosLeft = [];
1195      columnPosRight = [];
1196      var x = 0;
1197      for (var i = 0, ii = columns.length; i < ii; i++) {
1198        columnPosLeft[i] = x;
1199        columnPosRight[i] = x + columns[i].width;
1200        x += columns[i].width;
1201      }
1202    }
1203
1204    function setColumns(columnDefinitions) {
1205      columns = columnDefinitions;
1206
1207      columnsById = {};
1208      for (var i = 0; i < columns.length; i++) {
1209        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
1210        columnsById[m.id] = i;
1211        if (m.minWidth && m.width < m.minWidth) {
1212          m.width = m.minWidth;
1213        }
1214        if (m.maxWidth && m.width > m.maxWidth) {
1215          m.width = m.maxWidth;
1216        }
1217      }
1218
1219      updateColumnCaches();
1220
1221      if (initialized) {
1222        invalidateAllRows();
1223        createColumnHeaders();
1224        removeCssRules();
1225        createCssRules();
1226        resizeCanvas();
1227        applyColumnWidths();
1228        handleScroll();
1229      }
1230    }
1231
1232    function getOptions() {
1233      return options;
1234    }
1235
1236    function setOptions(args) {
1237      if (!getEditorLock().commitCurrentEdit()) {
1238        return;
1239      }
1240
1241      makeActiveCellNormal();
1242
1243      if (options.enableAddRow !== args.enableAddRow) {
1244        invalidateRow(getDataLength());
1245      }
1246
1247      options = $.extend(options, args);
1248      validateAndEnforceOptions();
1249
1250      $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
1251      render();
1252    }
1253
1254    function validateAndEnforceOptions() {
1255      if (options.autoHeight) {
1256        options.leaveSpaceForNewRows = false;
1257      }
1258    }
1259
1260    function setData(newData, scrollToTop) {
1261      data = newData;
1262      invalidateAllRows();
1263      updateRowCount();
1264      if (scrollToTop) {
1265        scrollTo(0);
1266      }
1267    }
1268
1269    function getData() {
1270      return data;
1271    }
1272
1273    function getDataLength() {
1274      if (data.getLength) {
1275        return data.getLength();
1276      } else {
1277        return data.length;
1278      }
1279    }
1280
1281    function getDataLengthIncludingAddNew() {
1282      return getDataLength() + (options.enableAddRow ? 1 : 0);
1283    }
1284
1285    function getDataItem(i) {
1286      if (data.getItem) {
1287        return data.getItem(i);
1288      } else {
1289        return data[i];
1290      }
1291    }
1292
1293    function getTopPanel() {
1294      return $topPanel[0];
1295    }
1296
1297    function setTopPanelVisibility(visible) {
1298      if (options.showTopPanel != visible) {
1299        options.showTopPanel = visible;
1300        if (visible) {
1301          $topPanelScroller.slideDown("fast", resizeCanvas);
1302        } else {
1303          $topPanelScroller.slideUp("fast", resizeCanvas);
1304        }
1305      }
1306    }
1307
1308    function setHeaderRowVisibility(visible) {
1309      if (options.showHeaderRow != visible) {
1310        options.showHeaderRow = visible;
1311        if (visible) {
1312          $headerRowScroller.slideDown("fast", resizeCanvas);
1313        } else {
1314          $headerRowScroller.slideUp("fast", resizeCanvas);
1315        }
1316      }
1317    }
1318
1319    function getContainerNode() {
1320      return $container.get(0);
1321    }
1322
1323    //////////////////////////////////////////////////////////////////////////////////////////////
1324    // Rendering / Scrolling
1325
1326    function getRowTop(row) {
1327      return options.rowHeight * row - offset;
1328    }
1329
1330    function getRowFromPosition(y) {
1331      return Math.floor((y + offset) / options.rowHeight);
1332    }
1333
1334    function scrollTo(y) {
1335      y = Math.max(y, 0);
1336      y = Math.min(y, th - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0));
1337
1338      var oldOffset = offset;
1339
1340      page = Math.min(n - 1, Math.floor(y / ph));
1341      offset = Math.round(page * cj);
1342      var newScrollTop = y - offset;
1343
1344      if (offset != oldOffset) {
1345        var range = getVisibleRange(newScrollTop);
1346        cleanupRows(range);
1347        updateRowPositions();
1348      }
1349
1350      if (prevScrollTop != newScrollTop) {
1351        vScrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
1352        $viewport[0].scrollTop = (lastRenderedScrollTop = scrollTop = prevScrollTop = newScrollTop);
1353
1354        trigger(self.onViewportChanged, {});
1355      }
1356    }
1357
1358    function defaultFormatter(row, cell, value, columnDef, dataContext) {
1359      if (value == null) {
1360        return "";
1361      } else {
1362        return (value + "").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
1363      }
1364    }
1365
1366    function getFormatter(row, column) {
1367      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
1368
1369      // look up by id, then index
1370      var columnOverrides = rowMetadata &&
1371          rowMetadata.columns &&
1372          (rowMetadata.columns[column.id] || rowMetadata.columns[getColumnIndex(column.id)]);
1373
1374      return (columnOverrides && columnOverrides.formatter) ||
1375          (rowMetadata && rowMetadata.formatter) ||
1376          column.formatter ||
1377          (options.formatterFactory && options.formatterFactory.getFormatter(column)) ||
1378          options.defaultFormatter;
1379    }
1380
1381    function getEditor(row, cell) {
1382      var column = columns[cell];
1383      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
1384      var columnMetadata = rowMetadata && rowMetadata.columns;
1385
1386      if (columnMetadata && columnMetadata[column.id] && columnMetadata[column.id].editor !== undefined) {
1387        return columnMetadata[column.id].editor;
1388      }
1389      if (columnMetadata && columnMetadata[cell] && columnMetadata[cell].editor !== undefined) {
1390        return columnMetadata[cell].editor;
1391      }
1392
1393      return column.editor || (options.editorFactory && options.editorFactory.getEditor(column));
1394    }
1395
1396    function getDataItemValueForColumn(item, columnDef) {
1397      if (options.dataItemColumnValueExtractor) {
1398        return options.dataItemColumnValueExtractor(item, columnDef);
1399      }
1400      return item[columnDef.field];
1401    }
1402
1403    function appendRowHtml(stringArray, row, range, dataLength) {
1404      var d = getDataItem(row);
1405      var dataLoading = row < dataLength && !d;
1406      var rowCss = "slick-row" +
1407          (dataLoading ? " loading" : "") +
1408          (row === activeRow ? " active" : "") +
1409          (row % 2 == 1 ? " odd" : " even");
1410
1411      if (!d) {
1412        rowCss += " " + options.addNewRowCssClass;
1413      }
1414
1415      var metadata = data.getItemMetadata && data.getItemMetadata(row);
1416
1417      if (metadata && metadata.cssClasses) {
1418        rowCss += " " + metadata.cssClasses;
1419      }
1420
1421      stringArray.push("<div class='ui-widget-content " + rowCss + "' style='top:" + getRowTop(row) + "px'>");
1422
1423      var colspan, m;
1424      for (var i = 0, ii = columns.length; i < ii; i++) {
1425        m = columns[i];
1426        colspan = 1;
1427        if (metadata && metadata.columns) {
1428          var columnData = metadata.columns[m.id] || metadata.columns[i];
1429          colspan = (columnData && columnData.colspan) || 1;
1430          if (colspan === "*") {
1431            colspan = ii - i;
1432          }
1433        }
1434
1435        // Do not render cells outside of the viewport.
1436        if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
1437          if (columnPosLeft[i] > range.rightPx) {
1438            // All columns to the right are outside the range.
1439            break;
1440          }
1441
1442          appendCellHtml(stringArray, row, i, colspan, d);
1443        }
1444
1445        if (colspan > 1) {
1446          i += (colspan - 1);
1447        }
1448      }
1449
1450      stringArray.push("</div>");
1451    }
1452
1453    function appendCellHtml(stringArray, row, cell, colspan, item) {
1454      var m = columns[cell];
1455      var cellCss = "slick-cell l" + cell + " r" + Math.min(columns.length - 1, cell + colspan - 1) +
1456          (m.cssClass ? " " + m.cssClass : "");
1457      if (row === activeRow && cell === activeCell) {
1458        cellCss += (" active");
1459      }
1460
1461      // TODO:  merge them together in the setter
1462      for (var key in cellCssClasses) {
1463        if (cellCssClasses[key][row] && cellCssClasses[key][row][m.id]) {
1464          cellCss += (" " + cellCssClasses[key][row][m.id]);
1465        }
1466      }
1467
1468      stringArray.push("<div class='" + cellCss + "'>");
1469
1470      // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
1471      if (item) {
1472        var value = getDataItemValueForColumn(item, m);
1473        stringArray.push(getFormatter(row, m)(row, cell, value, m, item));
1474      }
1475
1476      stringArray.push("</div>");
1477
1478      rowsCache[row].cellRenderQueue.push(cell);
1479      rowsCache[row].cellColSpans[cell] = colspan;
1480    }
1481
1482
1483    function cleanupRows(rangeToKeep) {
1484      for (var i in rowsCache) {
1485        if (((i = parseInt(i, 10)) !== activeRow) && (i < rangeToKeep.top || i > rangeToKeep.bottom)) {
1486          removeRowFromCache(i);
1487        }
1488      }
1489    }
1490
1491    function invalidate() {
1492      updateRowCount();
1493      invalidateAllRows();
1494      render();
1495    }
1496
1497    function invalidateAllRows() {
1498      if (currentEditor) {
1499        makeActiveCellNormal();
1500      }
1501      for (var row in rowsCache) {
1502        removeRowFromCache(row);
1503      }
1504    }
1505
1506    function removeRowFromCache(row) {
1507      var cacheEntry = rowsCache[row];
1508      if (!cacheEntry) {
1509        return;
1510      }
1511
1512      if (rowNodeFromLastMouseWheelEvent == cacheEntry.rowNode) {
1513        cacheEntry.rowNode.style.display = 'none';
1514        zombieRowNodeFromLastMouseWheelEvent = rowNodeFromLastMouseWheelEvent;
1515      } else {
1516        $canvas[0].removeChild(cacheEntry.rowNode);
1517      }
1518      
1519      delete rowsCache[row];
1520      delete postProcessedRows[row];
1521      renderedRows--;
1522      counter_rows_removed++;
1523    }
1524
1525    function invalidateRows(rows) {
1526      var i, rl;
1527      if (!rows || !rows.length) {
1528        return;
1529      }
1530      vScrollDir = 0;
1531      for (i = 0, rl = rows.length; i < rl; i++) {
1532        if (currentEditor && activeRow === rows[i]) {
1533          makeActiveCellNormal();
1534        }
1535        if (rowsCache[rows[i]]) {
1536          removeRowFromCache(rows[i]);
1537        }
1538      }
1539    }
1540
1541    function invalidateRow(row) {
1542      invalidateRows([row]);
1543    }
1544
1545    function updateCell(row, cell) {
1546      var cellNode = getCellNode(row, cell);
1547      if (!cellNode) {
1548        return;
1549      }
1550
1551      var m = columns[cell], d = getDataItem(row);
1552      if (currentEditor && activeRow === row && activeCell === cell) {
1553        currentEditor.loadValue(d);
1554      } else {
1555        cellNode.innerHTML = d ? getFormatter(row, m)(row, cell, getDataItemValueForColumn(d, m), m, d) : "";
1556        invalidatePostProcessingResults(row);
1557      }
1558    }
1559
1560    function updateRow(row) {
1561      var cacheEntry = rowsCache[row];
1562      if (!cacheEntry) {
1563        return;
1564      }
1565
1566      ensureCellNodesInRowsCache(row);
1567
1568      var d = getDataItem(row);
1569
1570      for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
1571        if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
1572          continue;
1573        }
1574
1575        columnIdx = columnIdx | 0;
1576        var m = columns[columnIdx],
1577            node = cacheEntry.cellNodesByColumnIdx[columnIdx];
1578
1579        if (row === activeRow && columnIdx === activeCell && currentEditor) {
1580          currentEditor.loadValue(d);
1581        } else if (d) {
1582          node.innerHTML = getFormatter(row, m)(row, columnIdx, getDataItemValueForColumn(d, m), m, d);
1583        } else {
1584          node.innerHTML = "";
1585        }
1586      }
1587
1588      invalidatePostProcessingResults(row);
1589    }
1590
1591    function getViewportHeight() {
1592      return parseFloat($.css($container[0], "height", true)) -
1593          parseFloat($.css($container[0], "paddingTop", true)) -
1594          parseFloat($.css($container[0], "paddingBottom", true)) -
1595          parseFloat($.css($headerScroller[0], "height")) - getVBoxDelta($headerScroller) -
1596          (options.showTopPanel ? options.topPanelHeight + getVBoxDelta($topPanelScroller) : 0) -
1597          (options.showHeaderRow ? options.headerRowHeight + getVBoxDelta($headerRowScroller) : 0);
1598    }
1599
1600    function resizeCanvas() {
1601      if (!initialized) { return; }
1602      if (options.autoHeight) {
1603        viewportH = options.rowHeight * getDataLengthIncludingAddNew();
1604      } else {
1605        viewportH = getViewportHeight();
1606      }
1607
1608      numVisibleRows = Math.ceil(viewportH / options.rowHeight);
1609      viewportW = parseFloat($.css($container[0], "width", true));
1610      if (!options.autoHeight) {
1611        $viewport.height(viewportH);
1612      }
1613
1614      if (options.forceFitColumns) {
1615        autosizeColumns();
1616      }
1617
1618      updateRowCount();
1619      handleScroll();
1620      // Since the width has changed, force the render() to reevaluate virtually rendered cells.
1621      lastRenderedScrollLeft = -1;
1622      render();
1623    }
1624
1625    function updateRowCount() {
1626      if (!initialized) { return; }
1627
1628      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
1629      var numberOfRows = dataLengthIncludingAddNew +
1630          (options.leaveSpaceForNewRows ? numVisibleRows - 1 : 0);
1631
1632      var oldViewportHasVScroll = viewportHasVScroll;
1633      // with autoHeight, we do not need to accommodate the vertical scroll bar
1634      viewportHasVScroll = !options.autoHeight && (numberOfRows * options.rowHeight > viewportH);
1635
1636      makeActiveCellNormal();
1637
1638      // remove the rows that are now outside of the data range
1639      // this helps avoid redundant calls to .removeRow() when the size of the data decreased by thousands of rows
1640      var l = dataLengthIncludingAddNew - 1;
1641      for (var i in rowsCache) {
1642        if (i >= l) {
1643          removeRowFromCache(i);
1644        }
1645      }
1646
1647      if (activeCellNode && activeRow > l) {
1648        resetActiveCell();
1649      }
1650
1651      var oldH = h;
1652      th = Math.max(options.rowHeight * numberOfRows, viewportH - scrollbarDimensions.height);
1653      if (th < maxSupportedCssHeight) {
1654        // just one page
1655        h = ph = th;
1656        n = 1;
1657        cj = 0;
1658      } else {
1659        // break into pages
1660        h = maxSupportedCssHeight;
1661        ph = h / 100;
1662        n = Math.floor(th / ph);
1663        cj = (th - h) / (n - 1);
1664      }
1665
1666      if (h !== oldH) {
1667        $canvas.css("height", h);
1668        scrollTop = $viewport[0].scrollTop;
1669      }
1670
1671      var oldScrollTopInRange = (scrollTop + offset <= th - viewportH);
1672
1673      if (th == 0 || scrollTop == 0) {
1674        page = offset = 0;
1675      } else if (oldScrollTopInRange) {
1676        // maintain virtual position
1677        scrollTo(scrollTop + offset);
1678      } else {
1679        // scroll to bottom
1680        scrollTo(th - viewportH);
1681      }
1682
1683      if (h != oldH && options.autoHeight) {
1684        resizeCanvas();
1685      }
1686
1687      if (options.forceFitColumns && oldViewportHasVScroll != viewportHasVScroll) {
1688        autosizeColumns();
1689      }
1690      updateCanvasWidth(false);
1691    }
1692
1693    function getVisibleRange(viewportTop, viewportLeft) {
1694      if (viewportTop == null) {
1695        viewportTop = scrollTop;
1696      }
1697      if (viewportLeft == null) {
1698        viewportLeft = scrollLeft;
1699      }
1700
1701      return {
1702        top: getRowFromPosition(viewportTop),
1703        bottom: getRowFromPosition(viewportTop + viewportH) + 1,
1704        leftPx: viewportLeft,
1705        rightPx: viewportLeft + viewportW
1706      };
1707    }
1708
1709    function getRenderedRange(viewportTop, viewportLeft) {
1710      var range = getVisibleRange(viewportTop, viewportLeft);
1711      var buffer = Math.round(viewportH / options.rowHeight);
1712      var minBuffer = 3;
1713
1714      if (vScrollDir == -1) {
1715        range.top -= buffer;
1716        range.bottom += minBuffer;
1717      } else if (vScrollDir == 1) {
1718        range.top -= minBuffer;
1719        range.bottom += buffer;
1720      } else {
1721        range.top -= minBuffer;
1722        range.bottom += minBuffer;
1723      }
1724
1725      range.top = Math.max(0, range.top);
1726      range.bottom = Math.min(getDataLengthIncludingAddNew() - 1, range.bottom);
1727
1728      range.leftPx -= viewportW;
1729      range.rightPx += viewportW;
1730
1731      range.leftPx = Math.max(0, range.leftPx);
1732      range.rightPx = Math.min(canvasWidth, range.rightPx);
1733
1734      return range;
1735    }
1736
1737    function ensureCellNodesInRowsCache(row) {
1738      var cacheEntry = rowsCache[row];
1739      if (cacheEntry) {
1740        if (cacheEntry.cellRenderQueue.length) {
1741          var lastChild = cacheEntry.rowNode.lastChild;
1742          while (cacheEntry.cellRenderQueue.length) {
1743            var columnIdx = cacheEntry.cellRenderQueue.pop();
1744            cacheEntry.cellNodesByColumnIdx[columnIdx] = lastChild;
1745            lastChild = lastChild.previousSibling;
1746          }
1747        }
1748      }
1749    }
1750
1751    function cleanUpCells(range, row) {
1752      var totalCellsRemoved = 0;
1753      var cacheEntry = rowsCache[row];
1754
1755      // Remove cells outside the range.
1756      var cellsToRemove = [];
1757      for (var i in cacheEntry.cellNodesByColumnIdx) {
1758        // I really hate it when people mess with Array.prototype.
1759        if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(i)) {
1760          continue;
1761        }
1762
1763        // This is a string, so it needs to be cast back to a number.
1764        i = i | 0;
1765
1766        var colspan = cacheEntry.cellColSpans[i];
1767        if (columnPosLeft[i] > range.rightPx ||
1768          columnPosRight[Math.min(columns.length - 1, i + colspan - 1)] < range.leftPx) {
1769          if (!(row == activeRow && i == activeCell)) {
1770            cellsToRemove.push(i);
1771          }
1772        }
1773      }
1774
1775      var cellToRemove;
1776      while ((cellToRemove = cellsToRemove.pop()) != null) {
1777        cacheEntry.rowNode.removeChild(cacheEntry.cellNodesByColumnIdx[cellToRemove]);
1778        delete cacheEntry.cellColSpans[cellToRemove];
1779        delete cacheEntry.cellNodesByColumnIdx[cellToRemove];
1780        if (postProcessedRows[row]) {
1781          delete postProcessedRows[row][cellToRemove];
1782        }
1783        totalCellsRemoved++;
1784      }
1785    }
1786
1787    function cleanUpAndRenderCells(range) {
1788      var cacheEntry;
1789      var stringArray = [];
1790      var processedRows = [];
1791      var cellsAdded;
1792      var totalCellsAdded = 0;
1793      var colspan;
1794
1795      for (var row = range.top, btm = range.bottom; row <= btm; row++) {
1796        cacheEntry = rowsCache[row];
1797        if (!cacheEntry) {
1798          continue;
1799        }
1800
1801        // cellRenderQueue populated in renderRows() needs to be cleared first
1802        ensureCellNodesInRowsCache(row);
1803
1804        cleanUpCells(range, row);
1805
1806        // Render missing cells.
1807        cellsAdded = 0;
1808
1809        var metadata = data.getItemMetadata && data.getItemMetadata(row);
1810        metadata = metadata && metadata.columns;
1811
1812        var d = getDataItem(row);
1813
1814        // TODO:  shorten this loop (index? heuristics? binary search?)
1815        for (var i = 0, ii = columns.length; i < ii; i++) {
1816          // Cells to the right are outside the range.
1817          if (columnPosLeft[i] > range.rightPx) {
1818            break;
1819          }
1820
1821          // Already rendered.
1822          if ((colspan = cacheEntry.cellColSpans[i]) != null) {
1823            i += (colspan > 1 ? colspan - 1 : 0);
1824            continue;
1825          }
1826
1827          colspan = 1;
1828          if (metadata) {
1829            var columnData = metadata[columns[i].id] || metadata[i];
1830            colspan = (columnData && columnData.colspan) || 1;
1831            if (colspan === "*") {
1832              colspan = ii - i;
1833            }
1834          }
1835
1836          if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
1837            appendCellHtml(stringArray, row, i, colspan, d);
1838            cellsAdded++;
1839          }
1840
1841          i += (colspan > 1 ? colspan - 1 : 0);
1842        }
1843
1844        if (cellsAdded) {
1845          totalCellsAdded += cellsAdded;
1846          processedRows.push(row);
1847        }
1848      }
1849
1850      if (!stringArray.length) {
1851        return;
1852      }
1853
1854      var x = document.createElement("div");
1855      x.innerHTML = stringArray.join("");
1856
1857      var processedRow;
1858      var node;
1859      while ((processedRow = processedRows.pop()) != null) {
1860        cacheEntry = rowsCache[processedRow];
1861        var columnIdx;
1862        while ((columnIdx = cacheEntry.cellRenderQueue.pop()) != null) {
1863          node = x.lastChild;
1864          cacheEntry.rowNode.appendChild(node);
1865          cacheEntry.cellNodesByColumnIdx[columnIdx] = node;
1866        }
1867      }
1868    }
1869
1870    function renderRows(range) {
1871      var parentNode = $canvas[0],
1872          stringArray = [],
1873          rows = [],
1874          needToReselectCell = false,
1875          dataLength = getDataLength();
1876
1877      for (var i = range.top, ii = range.bottom; i <= ii; i++) {
1878        if (rowsCache[i]) {
1879          continue;
1880        }
1881        renderedRows++;
1882        rows.push(i);
1883
1884        // Create an entry right away so that appendRowHtml() can
1885        // start populatating it.
1886        rowsCache[i] = {
1887          "rowNode": null,
1888
1889          // ColSpans of rendered cells (by column idx).
1890          // Can also be used for checking whether a cell has been rendered.
1891          "cellColSpans": [],
1892
1893          // Cell nodes (by column idx).  Lazy-populated by ensureCellNodesInRowsCache().
1894          "cellNodesByColumnIdx": [],
1895
1896          // Column indices of cell nodes that have been rendered, but not yet indexed in
1897          // cellNodesByColumnIdx.  These are in the same order as cell nodes added at the
1898          // end of the row.
1899          "cellRenderQueue": []
1900        };
1901
1902        appendRowHtml(stringArray, i, range, dataLength);
1903        if (activeCellNode && activeRow === i) {
1904          needToReselectCell = true;
1905        }
1906        counter_rows_rendered++;
1907      }
1908
1909      if (!rows.length) { return; }
1910
1911      var x = document.createElement("div");
1912      x.innerHTML = stringArray.join("");
1913
1914      for (var i = 0, ii = rows.length; i < ii; i++) {
1915        rowsCache[rows[i]].rowNode = parentNode.appendChild(x.firstChild);
1916      }
1917
1918      if (needToReselectCell) {
1919        activeCellNode = getCellNode(activeRow, activeCell);
1920      }
1921    }
1922
1923    function startPostProcessing() {
1924      if (!options.enableAsyncPostRender) {
1925        return;
1926      }
1927      clearTimeout(h_postrender);
1928      h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
1929    }
1930
1931    function invalidatePostProcessingResults(row) {
1932      delete postProcessedRows[row];
1933      postProcessFromRow = Math.min(postProcessFromRow, row);
1934      postProcessToRow = Math.max(postProcessToRow, row);
1935      startPostProcessing();
1936    }
1937
1938    function updateRowPositions() {
1939      for (var row in rowsCache) {
1940        rowsCache[row].rowNode.style.top = getRowTop(row) + "px";
1941      }
1942    }
1943
1944    function render() {
1945      if (!initialized) { return; }
1946      var visible = getVisibleRange();
1947      var rendered = getRenderedRange();
1948
1949      // remove rows no longer in the viewport
1950      cleanupRows(rendered);
1951
1952      // add new rows & missing cells in existing rows
1953      if (lastRenderedScrollLeft != scrollLeft) {
1954        cleanUpAndRenderCells(rendered);
1955      }
1956
1957      // render missing rows
1958      renderRows(rendered);
1959
1960      postProcessFromRow = visible.top;
1961      postProcessToRow = Math.min(getDataLengthIncludingAddNew() - 1, visible.bottom);
1962      startPostProcessing();
1963
1964      lastRenderedScrollTop = scrollTop;
1965      lastRenderedScrollLeft = scrollLeft;
1966      h_render = null;
1967    }
1968
1969    function handleHeaderRowScroll() {
1970      var scrollLeft = $headerRowScroller[0].scrollLeft;
1971      if (scrollLeft != $viewport[0].scrollLeft) {
1972        $viewport[0].scrollLeft = scrollLeft;
1973      }
1974    }
1975
1976    function handleScroll() {
1977      scrollTop = $viewport[0].scrollTop;
1978      scrollLeft = $viewport[0].scrollLeft;
1979      var vScrollDist = Math.abs(scrollTop - prevScrollTop);
1980      var hScrollDist = Math.abs(scrollLeft - prevScrollLeft);
1981
1982      if (hScrollDist) {
1983        prevScrollLeft = scrollLeft;
1984        $headerScroller[0].scrollLeft = scrollLeft;
1985        $topPanelScroller[0].scrollLeft = scrollLeft;
1986        $headerRowScroller[0].scrollLeft = scrollLeft;
1987      }
1988
1989      if (vScrollDist) {
1990        vScrollDir = prevScrollTop < scrollTop ? 1 : -1;
1991        prevScrollTop = scrollTop;
1992
1993        // switch virtual pages if needed
1994        if (vScrollDist < viewportH) {
1995          scrollTo(scrollTop + offset);
1996        } else {
1997          var oldOffset = offset;
1998          if (h == viewportH) {
1999            page = 0;
2000          } else {
2001            page = Math.min(n - 1, Math.floor(scrollTop * ((th - viewportH) / (h - viewportH)) * (1 / ph)));
2002          }
2003          offset = Math.round(page * cj);
2004          if (oldOffset != offset) {
2005            invalidateAllRows();
2006          }
2007        }
2008      }
2009
2010      if (hScrollDist || vScrollDist) {
2011        if (h_render) {
2012          clearTimeout(h_render);
2013        }
2014
2015        if (Math.abs(lastRenderedScrollTop - scrollTop) > 20 ||
2016            Math.abs(lastRenderedScrollLeft - scrollLeft) > 20) {
2017          if (options.forceSyncScrolling || (
2018              Math.abs(lastRenderedScrollTop - scrollTop) < viewportH &&
2019              Math.abs(lastRenderedScrollLeft - scrollLeft) < viewportW)) {
2020            render();
2021          } else {
2022            h_render = setTimeout(render, 50);
2023          }
2024
2025          trigger(self.onViewportChanged, {});
2026        }
2027      }
2028
2029      trigger(self.onScroll, {scrollLeft: scrollLeft, scrollTop: scrollTop});
2030    }
2031
2032    function asyncPostProcessRows() {
2033      var dataLength = getDataLength();
2034      while (postProcessFromRow <= postProcessToRow) {
2035        var row = (vScrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
2036        var cacheEntry = rowsCache[row];
2037        if (!cacheEntry || row >= dataLength) {
2038          continue;
2039        }
2040
2041        if (!postProcessedRows[row]) {
2042          postProcessedRows[row] = {};
2043        }
2044
2045        ensureCellNodesInRowsCache(row);
2046        for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
2047          if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
2048            continue;
2049          }
2050
2051          columnIdx = columnIdx | 0;
2052
2053          var m = columns[columnIdx];
2054          if (m.asyncPostRender && !postProcessedRows[row][columnIdx]) {
2055            var node = cacheEntry.cellNodesByColumnIdx[columnIdx];
2056            if (node) {
2057              m.asyncPostRender(node, row, getDataItem(row), m);
2058            }
2059            postProcessedRows[row][columnIdx] = true;
2060          }
2061        }
2062
2063        h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
2064        return;
2065      }
2066    }
2067
2068    function updateCellCssStylesOnRenderedRows(addedHash, removedHash) {
2069      var node, columnId, addedRowHash, removedRowHash;
2070      for (var row in rowsCache) {
2071        removedRowHash = removedHash && removedHash[row];
2072        addedRowHash = addedHash && addedHash[row];
2073
2074        if (removedRowHash) {
2075          for (columnId in removedRowHash) {
2076            if (!addedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
2077              node = getCellNode(row, getColumnIndex(columnId));
2078              if (node) {
2079                $(node).removeClass(removedRowHash[columnId]);
2080              }
2081            }
2082          }
2083        }
2084
2085        if (addedRowHash) {
2086          for (columnId in addedRowHash) {
2087            if (!removedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
2088              node = getCellNode(row, getColumnIndex(columnId));
2089              if (node) {
2090                $(node).addClass(addedRowHash[columnId]);
2091              }
2092            }
2093          }
2094        }
2095      }
2096    }
2097
2098    function addCellCssStyles(key, hash) {
2099      if (cellCssClasses[key]) {
2100        throw "addCellCssStyles: cell CSS hash with key '" + key + "' already exists.";
2101      }
2102
2103      cellCssClasses[key] = hash;
2104      updateCellCssStylesOnRenderedRows(hash, null);
2105
2106      trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash });
2107    }
2108
2109    function removeCellCssStyles(key) {
2110      if (!cellCssClasses[key]) {
2111        return;
2112      }
2113
2114      updateCellCssStylesOnRenderedRows(null, cellCssClasses[key]);
2115      delete cellCssClasses[key];
2116
2117      trigger(self.onCellCssStylesChanged, { "key": key, "hash": null });
2118    }
2119
2120    function setCellCssStyles(key, hash) {
2121      var prevHash = cellCssClasses[key];
2122
2123      cellCssClasses[key] = hash;
2124      updateCellCssStylesOnRenderedRows(hash, prevHash);
2125
2126      trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash });
2127    }
2128
2129    function getCellCssStyles(key) {
2130      return cellCssClasses[key];
2131    }
2132
2133    function flashCell(row, cell, speed) {
2134      speed = speed || 100;
2135      if (rowsCache[row]) {
2136        var $cell = $(getCellNode(row, cell));
2137
2138        function toggleCellClass(times) {
2139          if (!times) {
2140            return;
2141          }
2142          setTimeout(function () {
2143                $cell.queue(function () {
2144                  $cell.toggleClass(options.cellFlashingCssClass).dequeue();
2145                  toggleCellClass(times - 1);
2146                });
2147              },
2148              speed);
2149        }
2150
2151        toggleCellClass(4);
2152      }
2153    }
2154
2155    //////////////////////////////////////////////////////////////////////////////////////////////
2156    // Interactivity
2157
2158    function handleMouseWheel(e) {
2159      var rowNode = $(e.target).closest(".slick-row")[0];
2160      if (rowNode != rowNodeFromLastMouseWheelEvent) {
2161        if (zombieRowNodeFromLastMouseWheelEvent && zombieRowNodeFromLastMouseWheelEvent != rowNode) {
2162          $canvas[0].removeChild(zombieRowNodeFromLastMouseWheelEvent);
2163          zombieRowNodeFromLastMouseWheelEvent = null;
2164        }
2165        rowNodeFromLastMouseWheelEvent = rowNode;      
2166      }
2167    }
2168
2169    function handleDragInit(e, dd) {
2170      var cell = getCellFromEvent(e);
2171      if (!cell || !cellExists(cell.row, cell.cell)) {
2172        return false;
2173      }
2174
2175      var retval = trigger(self.onDragInit, dd, e);
2176      if (e.isImmediatePropagationStopped()) {
2177        return retval;
2178      }
2179
2180      // if nobody claims to be handling drag'n'drop by stopping immediate propagation,
2181      // cancel out of it
2182      return false;
2183    }
2184
2185    function handleDragStart(e, dd) {
2186      var cell = getCellFromEvent(e);
2187      if (!cell || !cellExists(cell.row, cell.cell)) {
2188        return false;
2189      }
2190
2191      var retval = trigger(self.onDragStart, dd, e);
2192      if (e.isImmediatePropagationStopped()) {
2193        return retval;
2194      }
2195
2196      return false;
2197    }
2198
2199    function handleDrag(e, dd) {
2200      return trigger(self.onDrag, dd, e);
2201    }
2202
2203    function handleDragEnd(e, dd) {
2204      trigger(self.onDragEnd, dd, e);
2205    }
2206
2207    function handleKeyDown(e) {
2208      trigger(self.onKeyDown, {row: activeRow, cell: activeCell}, e);
2209      var handled = e.isImmediatePropagationStopped();
2210
2211      if (!handled) {
2212        if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
2213          if (e.which == 27) {
2214            if (!getEditorLock().isActive()) {
2215              return; // no editing mode to cancel, allow bubbling and default processing (exit without cancelling the event)
2216            }
2217            cancelEditAndSetFocus();
2218          } else if (e.which == 34) {
2219            navigatePageDown();
2220            handled = true;           
2221          } else if (e.which == 33) {
2222            navigatePageUp();
2223            handled = true;
2224          } else if (e.which == 37) {
2225            handled = navigateLeft();
2226          } else if (e.which == 39) {
2227            handled = navigateRight();
2228          } else if (e.which == 38) {
2229            handled = navigateUp();
2230          } else if (e.which == 40) {
2231            handled = navigateDown();
2232          } else if (e.which == 9) {
2233            handled = navigateNext();
2234          } else if (e.which == 13) {
2235            if (options.editable) {
2236              if (currentEditor) {
2237                // adding new row
2238                if (activeRow === getDataLength()) {
2239                  navigateDown();
2240                } else {
2241                  commitEditAndSetFocus();
2242                }
2243              } else {
2244                if (getEditorLock().commitCurrentEdit()) {
2245                  makeActiveCellEditable();
2246                }
2247              }
2248            }
2249            handled = true;
2250          }
2251        } else if (e.which == 9 && e.shiftKey && !e.ctrlKey && !e.altKey) {
2252          handled = navigatePrev();
2253        }
2254      }
2255
2256      if (handled) {
2257        // the event has been handled so don't let parent element (bubbling/propagation) or browser (default) handle it
2258        e.stopPropagation();
2259        e.preventDefault();
2260        try {
2261          e.originalEvent.keyCode = 0; // prevent default behaviour for special keys in IE browsers (F3, F5, etc.)
2262        }
2263        // ignore exceptions - setting the original event's keycode throws access denied exception for "Ctrl"
2264        // (hitting control key only, nothing else), "Shift" (maybe others)
2265        catch (error) {
2266        }
2267      }
2268    }
2269
2270    function handleClick(e) {
2271      if (!currentEditor) {
2272        // if this click resulted in some cell child node getting focus,
2273        // don't steal it back - keyboard events will still bubble up
2274        // IE9+ seems to default DIVs to tabIndex=0 instead of -1, so check for cell clicks directly.
2275        if (e.target != document.activeElement || $(e.target).hasClass("slick-cell")) {
2276          setFocus();
2277        }
2278      }
2279
2280      var cell = getCellFromEvent(e);
2281      if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
2282        return;
2283      }
2284
2285      trigger(self.onClick, {row: cell.row, cell: cell.cell}, e);
2286      if (e.isImmediatePropagationStopped()) {
2287        return;
2288      }
2289
2290      if ((activeCell != cell.cell || activeRow != cell.row) && canCellBeActive(cell.row, cell.cell)) {
2291        if (!getEditorLock().isActive() || getEditorLock().commitCurrentEdit()) {
2292          scrollRowIntoView(cell.row, false);
2293          setActiveCellInternal(getCellNode(cell.row, cell.cell));
2294        }
2295      }
2296    }
2297
2298    function handleContextMenu(e) {
2299      var $cell = $(e.target).closest(".slick-cell", $canvas);
2300      if ($cell.length === 0) {
2301        return;
2302      }
2303
2304      // are we editing this cell?
2305      if (activeCellNode === $cell[0] && currentEditor !== null) {
2306        return;
2307      }
2308
2309      trigger(self.onContextMenu, {}, e);
2310    }
2311
2312    function handleDblClick(e) {
2313      var cell = getCellFromEvent(e);
2314      if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
2315        return;
2316      }
2317
2318      trigger(self.onDblClick, {row: cell.row, cell: cell.cell}, e);
2319      if (e.isImmediatePropagationStopped()) {
2320        return;
2321      }
2322
2323      if (options.editable) {
2324        gotoCell(cell.row, cell.cell, true);
2325      }
2326    }
2327
2328    function handleHeaderMouseEnter(e) {
2329      trigger(self.onHeaderMouseEnter, {
2330        "column": $(this).data("column")
2331      }, e);
2332    }
2333
2334    function handleHeaderMouseLeave(e) {
2335      trigger(self.onHeaderMouseLeave, {
2336        "column": $(this).data("column")
2337      }, e);
2338    }
2339
2340    function handleHeaderContextMenu(e) {
2341      var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
2342      var column = $header && $header.data("column");
2343      trigger(self.onHeaderContextMenu, {column: column}, e);
2344    }
2345
2346    function handleHeaderClick(e) {
2347      var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
2348      var column = $header && $header.data("column");
2349      if (column) {
2350        trigger(self.onHeaderClick, {column: column}, e);
2351      }
2352    }
2353
2354    function handleMouseEnter(e) {
2355      trigger(self.onMouseEnter, {}, e);
2356    }
2357
2358    function handleMouseLeave(e) {
2359      trigger(self.onMouseLeave, {}, e);
2360    }
2361
2362    function cellExists(row, cell) {
2363      return !(row < 0 || row >= getDataLength() || cell < 0 || cell >= columns.length);
2364    }
2365
2366    function getCellFromPoint(x, y) {
2367      var row = getRowFromPosition(y);
2368      var cell = 0;
2369
2370      var w = 0;
2371      for (var i = 0; i < columns.length && w < x; i++) {
2372        w += columns[i].width;
2373        cell++;
2374      }
2375
2376      if (cell < 0) {
2377        cell = 0;
2378      }
2379
2380      return {row: row, cell: cell - 1};
2381    }
2382
2383    function getCellFromNode(cellNode) {
2384      // read column number from .l<columnNumber> CSS class
2385      var cls = /l\d+/.exec(cellNode.className);
2386      if (!cls) {
2387        throw "getCellFromNode: cannot get cell - " + cellNode.className;
2388      }
2389      return parseInt(cls[0].substr(1, cls[0].length - 1), 10);
2390    }
2391
2392    function getRowFromNode(rowNode) {
2393      for (var row in rowsCache) {
2394        if (rowsCache[row].rowNode === rowNode) {
2395          return row | 0;
2396        }
2397      }
2398
2399      return null;
2400    }
2401
2402    function getCellFromEvent(e) {
2403      var $cell = $(e.target).closest(".slick-cell", $canvas);
2404      if (!$cell.length) {
2405        return null;
2406      }
2407
2408      var row = getRowFromNode($cell[0].parentNode);
2409      var cell = getCellFromNode($cell[0]);
2410
2411      if (row == null || cell == null) {
2412        return null;
2413      } else {
2414        return {
2415          "row": row,
2416          "cell": cell
2417        };
2418      }
2419    }
2420
2421    function getCellNodeBox(row, cell) {
2422      if (!cellExists(row, cell)) {
2423        return null;
2424      }
2425
2426      var y1 = getRowTop(row);
2427      var y2 = y1 + options.rowHeight - 1;
2428      var x1 = 0;
2429      for (var i = 0; i < cell; i++) {
2430        x1 += columns[i].width;
2431      }
2432      var x2 = x1 + columns[cell].width;
2433
2434      return {
2435        top: y1,
2436        left: x1,
2437        bottom: y2,
2438        right: x2
2439      };
2440    }
2441
2442    //////////////////////////////////////////////////////////////////////////////////////////////
2443    // Cell switching
2444
2445    function resetActiveCell() {
2446      setActiveCellInternal(null, false);
2447    }
2448
2449    function setFocus() {
2450      if (tabbingDirection == -1) {
2451        $focusSink[0].focus();
2452      } else {
2453        $focusSink2[0].focus();
2454      }
2455    }
2456
2457    function scrollCellIntoView(row, cell, doPaging) {
2458      scrollRowIntoView(row, doPaging);
2459
2460      var colspan = getColspan(row, cell);
2461      var left = columnPosLeft[cell],
2462        right = columnPosRight[cell + (colspan > 1 ? colspan - 1 : 0)],
2463        scrollRight = scrollLeft + viewportW;
2464
2465      if (left < scrollLeft) {
2466        $viewport.scrollLeft(left);
2467        handleScroll();
2468        render();
2469      } else if (right > scrollRight) {
2470        $viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
2471        handleScroll();
2472        render();
2473      }
2474    }
2475
2476    function setActiveCellInternal(newCell, opt_editMode) {
2477      if (activeCellNode !== null) {
2478        makeActiveCellNormal();
2479        $(activeCellNode).removeClass("active");
2480        if (rowsCache[activeRow]) {
2481          $(rowsCache[activeRow].rowNode).removeClass("active");
2482        }
2483      }
2484
2485      var activeCellChanged = (activeCellNode !== newCell);
2486      activeCellNode = newCell;
2487
2488      if (activeCellNode != null) {
2489        activeRow = getRowFromNode(activeCellNode.parentNode);
2490        activeCell = activePosX = getCellFromNode(activeCellNode);
2491
2492        if (opt_editMode == null) {
2493          opt_editMode = (activeRow == getDataLength()) || options.autoEdit;
2494        }
2495
2496        $(activeCellNode).addClass("active");
2497        $(rowsCache[activeRow].rowNode).addClass("active");
2498
2499        if (options.editable && opt_editMode && isCellPotentiallyEditable(activeRow, activeCell)) {
2500          clearTimeout(h_editorLoader);
2501
2502          if (options.asyncEditorLoading) {
2503            h_editorLoader = setTimeout(function () {
2504              makeActiveCellEditable();
2505            }, options.asyncEditorLoadDelay);
2506          } else {
2507            makeActiveCellEditable();
2508          }
2509        }
2510      } else {
2511        activeRow = activeCell = null;
2512      }
2513
2514      if (activeCellChanged) {
2515        trigger(self.onActiveCellChanged, getActiveCell());
2516      }
2517    }
2518
2519    function clearTextSelection() {
2520      if (document.selection && document.selection.empty) {
2521        try {
2522          //IE fails here if selected element is not in dom
2523          document.selection.empty();
2524        } catch (e) { }
2525      } else if (window.getSelection) {
2526        var sel = window.getSelection();
2527        if (sel && sel.removeAllRanges) {
2528          sel.removeAllRanges();
2529        }
2530      }
2531    }
2532
2533    function isCellPotentiallyEditable(row, cell) {
2534      var dataLength = getDataLength();
2535      // is the data for this row loaded?
2536      if (row < dataLength && !getDataItem(row)) {
2537        return false;
2538      }
2539
2540      // are we in the Add New row?  can we create new from this cell?
2541      if (columns[cell].cannotTriggerInsert && row >= dataLength) {
2542        return false;
2543      }
2544
2545      // does this cell have an editor?
2546      if (!getEditor(row, cell)) {
2547        return false;
2548      }
2549
2550      return true;
2551    }
2552
2553    function makeActiveCellNormal() {
2554      if (!currentEditor) {
2555        return;
2556      }
2557      trigger(self.onBeforeCellEditorDestroy, {editor: currentEditor});
2558      currentEditor.destroy();
2559      currentEditor = null;
2560
2561      if (activeCellNode) {
2562        var d = getDataItem(activeRow);
2563        $(activeCellNode).removeClass("editable invalid");
2564        if (d) {
2565          var column = columns[activeCell];
2566          var formatter = getFormatter(activeRow, column);
2567          activeCellNode.innerHTML = formatter(activeRow, activeCell, getDataItemValueForColumn(d, column), column, d);
2568          invalidatePostProcessingResults(activeRow);
2569        }
2570      }
2571
2572      // if there previously was text selected on a page (such as selected text in the edit cell just removed),
2573      // IE can't set focus to anything else correctly
2574      if (navigator.userAgent.toLowerCase().match(/msie/)) {
2575        clearTextSelection();
2576      }
2577
2578      getEditorLock().deactivate(editController);
2579    }
2580
2581    function makeActiveCellEditable(editor) {
2582      if (!activeCellNode) {
2583        return;
2584      }
2585      if (!options.editable) {
2586        throw "Grid : makeActiveCellEditable : should never get called when options.editable is false";
2587      }
2588
2589      // cancel pending async call if there is one
2590      clearTimeout(h_editorLoader);
2591
2592      if (!isCellPotentiallyEditable(activeRow, activeCell)) {
2593        return;
2594      }
2595
2596      var columnDef = columns[activeCell];
2597      var item = getDataItem(activeRow);
2598
2599      if (trigger(self.onBeforeEditCell, {row: activeRow, cell: activeCell, item: item, column: columnDef}) === false) {
2600        setFocus();
2601        return;
2602      }
2603
2604      getEditorLock().activate(editController);
2605      $(activeCellNode).addClass("editable");
2606
2607      // don't clear the cell if a custom editor is passed through
2608      if (!editor) {
2609        activeCellNode.innerHTML = "";
2610      }
2611
2612      currentEditor = new (editor || getEditor(activeRow, activeCell))({
2613        grid: self,
2614        gridPosition: absBox($container[0]),
2615        position: absBox(activeCellNode),
2616        container: activeCellNode,
2617        column: columnDef,
2618        item: item || {},
2619        commitChanges: commitEditAndSetFocus,
2620        cancelChanges: cancelEditAndSetFocus
2621      });
2622
2623      if (item) {
2624        currentEditor.loadValue(item);
2625      }
2626
2627      serializedEditorValue = currentEditor.serializeValue();
2628
2629      if (currentEditor.position) {
2630        handleActiveCellPositionChange();
2631      }
2632    }
2633
2634    function commitEditAndSetFocus() {
2635      // if the commit fails, it would do so due to a validation error
2636      // if so, do not steal the focus from the editor
2637      if (getEditorLock().commitCurrentEdit()) {
2638        setFocus();
2639        if (options.autoEdit) {
2640          navigateDown();
2641        }
2642      }
2643    }
2644
2645    function cancelEditAndSetFocus() {
2646      if (getEditorLock().cancelCurrentEdit()) {
2647        setFocus();
2648      }
2649    }
2650
2651    function absBox(elem) {
2652      var box = {
2653        top: elem.offsetTop,
2654        left: elem.offsetLeft,
2655        bottom: 0,
2656        right: 0,
2657        width: $(elem).outerWidth(),
2658        height: $(elem).outerHeight(),
2659        visible: true};
2660      box.bottom = box.top + box.height;
2661      box.right = box.left + box.width;
2662
2663      // walk up the tree
2664      var offsetParent = elem.offsetParent;
2665      while ((elem = elem.parentNode) != document.body) {
2666        if (box.visible && elem.scrollHeight != elem.offsetHeight && $(elem).css("overflowY") != "visible") {
2667          box.visible = box.bottom > elem.scrollTop && box.top < elem.scrollTop + elem.clientHeight;
2668        }
2669
2670        if (box.visible && elem.scrollWidth != elem.offsetWidth && $(elem).css("overflowX") != "visible") {
2671          box.visible = box.right > elem.scrollLeft && box.left < elem.scrollLeft + elem.clientWidth;
2672        }
2673
2674        box.left -= elem.scrollLeft;
2675        box.top -= elem.scrollTop;
2676
2677        if (elem === offsetParent) {
2678          box.left += elem.offsetLeft;
2679          box.top += elem.offsetTop;
2680          offsetParent = elem.offsetParent;
2681        }
2682
2683        box.bottom = box.top + box.height;
2684        box.right = box.left + box.width;
2685      }
2686
2687      return box;
2688    }
2689
2690    function getActiveCellPosition() {
2691      return absBox(activeCellNode);
2692    }
2693
2694    function getGridPosition() {
2695      return absBox($container[0])
2696    }
2697
2698    function handleActiveCellPositionChange() {
2699      if (!activeCellNode) {
2700        return;
2701      }
2702
2703      trigger(self.onActiveCellPositionChanged, {});
2704
2705      if (currentEditor) {
2706        var cellBox = getActiveCellPosition();
2707        if (currentEditor.show && currentEditor.hide) {
2708          if (!cellBox.visible) {
2709            currentEditor.hide();
2710          } else {
2711            currentEditor.show();
2712          }
2713        }
2714
2715        if (currentEditor.position) {
2716          currentEditor.position(cellBox);
2717        }
2718      }
2719    }
2720
2721    function getCellEditor() {
2722      return currentEditor;
2723    }
2724
2725    function getActiveCell() {
2726      if (!activeCellNode) {
2727        return null;
2728      } else {
2729        return {row: activeRow, cell: activeCell};
2730      }
2731    }
2732
2733    function getActiveCellNode() {
2734      return activeCellNode;
2735    }
2736
2737    function scrollRowIntoView(row, doPaging) {
2738      var rowAtTop = row * options.rowHeight;
2739      var rowAtBottom = (row + 1) * options.rowHeight - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0);
2740
2741      // need to page down?
2742      if ((row + 1) * options.rowHeight > scrollTop + viewportH + offset) {
2743        scrollTo(doPaging ? rowAtTop : rowAtBottom);
2744        render();
2745      }
2746      // or page up?
2747      else if (row * options.rowHeight < scrollTop + offset) {
2748        scrollTo(doPaging ? rowAtBottom : rowAtTop);
2749        render();
2750      }
2751    }
2752
2753    function scrollRowToTop(row) {
2754      scrollTo(row * options.rowHeight);
2755      render();
2756    }
2757
2758    function scrollPage(dir) {
2759      var deltaRows = dir * numVisibleRows;
2760      scrollTo((getRowFromPosition(scrollTop) + deltaRows) * options.rowHeight);
2761      render();
2762
2763      if (options.enableCellNavigation && activeRow != null) {
2764        var row = activeRow + deltaRows;
2765        var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
2766        if (row >= dataLengthIncludingAddNew) {
2767          row = dataLengthIncludingAddNew - 1;
2768        }
2769        if (row < 0) {
2770          row = 0;
2771        }
2772
2773        var cell = 0, prevCell = null;
2774        var prevActivePosX = activePosX;
2775        while (cell <= activePosX) {
2776          if (canCellBeActive(row, cell)) {
2777            prevCell = cell;  
2778          }
2779          cell += getColspan(row, cell);
2780        }
2781
2782        if (prevCell !== null) {
2783          setActiveCellInternal(getCellNode(row, prevCell));
2784          activePosX = prevActivePosX;
2785        } else {
2786          resetActiveCell();
2787        }
2788      }
2789    }
2790
2791    function navigatePageDown() {
2792      scrollPage(1);
2793    }
2794
2795    function navigatePageUp() {
2796      scrollPage(-1);
2797    }
2798
2799    function getColspan(row, cell) {
2800      var metadata = data.getItemMetadata && data.getItemMetadata(row);
2801      if (!metadata || !metadata.columns) {
2802        return 1;
2803      }
2804
2805      var columnData = metadata.columns[columns[cell].id] || metadata.columns[cell];
2806      var colspan = (columnData && columnData.colspan);
2807      if (colspan === "*") {
2808        colspan = columns.length - cell;
2809      } else {
2810        colspan = colspan || 1;
2811      }
2812
2813      return colspan;
2814    }
2815
2816    function findFirstFocusableCell(row) {
2817      var cell = 0;
2818      while (cell < columns.length) {
2819        if (canCellBeActive(row, cell)) {
2820          return cell;
2821        }
2822        cell += getColspan(row, cell);
2823      }
2824      return null;
2825    }
2826
2827    function findLastFocusableCell(row) {
2828      var cell = 0;
2829      var lastFocusableCell = null;
2830      while (cell < columns.length) {
2831        if (canCellBeActive(row, cell)) {
2832          lastFocusableCell = cell;
2833        }
2834        cell += getColspan(row, cell);
2835      }
2836      return lastFocusableCell;
2837    }
2838
2839    function gotoRight(row, cell, posX) {
2840      if (cell >= columns.length) {
2841        return null;
2842      }
2843
2844      do {
2845        cell += getColspan(row, cell);
2846      }
2847      while (cell < columns.length && !canCellBeActive(row, cell));
2848
2849      if (cell < columns.length) {
2850        return {
2851          "row": row,
2852          "cell": cell,
2853          "posX": cell
2854        };
2855      }
2856      return null;
2857    }
2858
2859    function gotoLeft(row, cell, posX) {
2860      if (cell <= 0) {
2861        return null;
2862      }
2863
2864      var firstFocusableCell = findFirstFocusableCell(row);
2865      if (firstFocusableCell === null || firstFocusableCell >= cell) {
2866        return null;
2867      }
2868
2869      var prev = {
2870        "row": row,
2871        "cell": firstFocusableCell,
2872        "posX": firstFocusableCell
2873      };
2874      var pos;
2875      while (true) {
2876        pos = gotoRight(prev.row, prev.cell, prev.posX);
2877        if (!pos) {
2878          return null;
2879        }
2880        if (pos.cell >= cell) {
2881          return prev;
2882        }
2883        prev = pos;
2884      }
2885    }
2886
2887    function gotoDown(row, cell, posX) {
2888      var prevCell;
2889      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
2890      while (true) {
2891        if (++row >= dataLengthIncludingAddNew) {
2892          return null;
2893        }
2894
2895        prevCell = cell = 0;
2896        while (cell <= posX) {
2897          prevCell = cell;
2898          cell += getColspan(row, cell);
2899        }
2900
2901        if (canCellBeActive(row, prevCell)) {
2902          return {
2903            "row": row,
2904            "cell": prevCell,
2905            "posX": posX
2906          };
2907        }
2908      }
2909    }
2910
2911    function gotoUp(row, cell, posX) {
2912      var prevCell;
2913      while (true) {
2914        if (--row < 0) {
2915          return null;
2916        }
2917
2918        prevCell = cell = 0;
2919        while (cell <= posX) {
2920          prevCell = cell;
2921          cell += getColspan(row, cell);
2922        }
2923
2924        if (canCellBeActive(row, prevCell)) {
2925          return {
2926            "row": row,
2927            "cell": prevCell,
2928            "posX": posX
2929          };
2930        }
2931      }
2932    }
2933
2934    function gotoNext(row, cell, posX) {
2935      if (row == null && cell == null) {
2936        row = cell = posX = 0;
2937        if (canCellBeActive(row, cell)) {
2938          return {
2939            "row": row,
2940            "cell": cell,
2941            "posX": cell
2942          };
2943        }
2944      }
2945
2946      var pos = gotoRight(row, cell, posX);
2947      if (pos) {
2948        return pos;
2949      }
2950
2951      var firstFocusableCell = null;
2952      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
2953      while (++row < dataLengthIncludingAddNew) {
2954        firstFocusableCell = findFirstFocusableCell(row);
2955        if (firstFocusableCell !== null) {
2956          return {
2957            "row": row,
2958            "cell": firstFocusableCell,
2959            "posX": firstFocusableCell
2960          };
2961        }
2962      }
2963      return null;
2964    }
2965
2966    function gotoPrev(row, cell, posX) {
2967      if (row == null && cell == null) {
2968        row = getDataLengthIncludingAddNew() - 1;
2969        cell = posX = columns.length - 1;
2970        if (canCellBeActive(row, cell)) {
2971          return {
2972            "row": row,
2973            "cell": cell,
2974            "posX": cell
2975          };
2976        }
2977      }
2978
2979      var pos;
2980      var lastSelectableCell;
2981      while (!pos) {
2982        pos = gotoLeft(row, cell, posX);
2983        if (pos) {
2984          break;
2985        }
2986        if (--row < 0) {
2987          return null;
2988        }
2989
2990        cell = 0;
2991        lastSelectableCell = findLastFocusableCell(row);
2992        if (lastSelectableCell !== null) {
2993          pos = {
2994            "row": row,
2995            "cell": lastSelectableCell,
2996            "posX": lastSelectableCell
2997          };
2998        }
2999      }
3000      return pos;
3001    }
3002
3003    function navigateRight() {
3004      return navigate("right");
3005    }
3006
3007    function navigateLeft() {
3008      return navigate("left");
3009    }
3010
3011    function navigateDown() {
3012      return navigate("down");
3013    }
3014
3015    function navigateUp() {
3016      return navigate("up");
3017    }
3018
3019    function navigateNext() {
3020      return navigate("next");
3021    }
3022
3023    function navigatePrev() {
3024      return navigate("prev");
3025    }
3026
3027    /**
3028     * @param {string} dir Navigation direction.
3029     * @return {boolean} Whether navigation resulted in a change of active cell.
3030     */
3031    function navigate(dir) {
3032      if (!options.enableCellNavigation) {
3033        return false;
3034      }
3035
3036      if (!activeCellNode && dir != "prev" && dir != "next") {
3037        return false;
3038      }
3039
3040      if (!getEditorLock().commitCurrentEdit()) {
3041        return true;
3042      }
3043      setFocus();
3044
3045      var tabbingDirections = {
3046        "up": -1,
3047        "down": 1,
3048        "left": -1,
3049        "right": 1,
3050        "prev": -1,
3051        "next": 1
3052      };
3053      tabbingDirection = tabbingDirections[dir];
3054
3055      var stepFunctions = {
3056        "up": gotoUp,
3057        "down": gotoDown,
3058        "left": gotoLeft,
3059        "right": gotoRight,
3060        "prev": gotoPrev,
3061        "next": gotoNext
3062      };
3063      var stepFn = stepFunctions[dir];
3064      var pos = stepFn(activeRow, activeCell, activePosX);
3065      if (pos) {
3066        var isAddNewRow = (pos.row == getDataLength());
3067        scrollCellIntoView(pos.row, pos.cell, !isAddNewRow);
3068        setActiveCellInternal(getCellNode(pos.row, pos.cell));
3069        activePosX = pos.posX;
3070        return true;
3071      } else {
3072        setActiveCellInternal(getCellNode(activeRow, activeCell));
3073        return false;
3074      }
3075    }
3076
3077    function getCellNode(row, cell) {
3078      if (rowsCache[row]) {
3079        ensureCellNodesInRowsCache(row);
3080        return rowsCache[row].cellNodesByColumnIdx[cell];
3081      }
3082      return null;
3083    }
3084
3085    function setActiveCell(row, cell) {
3086      if (!initialized) { return; }
3087      if (row > getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
3088        return;
3089      }
3090
3091      if (!options.enableCellNavigation) {
3092        return;
3093      }
3094
3095      scrollCellIntoView(row, cell, false);
3096      setActiveCellInternal(getCellNode(row, cell), false);
3097    }
3098
3099    function canCellBeActive(row, cell) {
3100      if (!options.enableCellNavigation || row >= getDataLengthIncludingAddNew() ||
3101          row < 0 || cell >= columns.length || cell < 0) {
3102        return false;
3103      }
3104
3105      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
3106      if (rowMetadata && typeof rowMetadata.focusable === "boolean") {
3107        return rowMetadata.focusable;
3108      }
3109
3110      var columnMetadata = rowMetadata && rowMetadata.columns;
3111      if (columnMetadata && columnMetadata[columns[cell].id] && typeof columnMetadata[columns[cell].id].focusable === "boolean") {
3112        return columnMetadata[columns[cell].id].focusable;
3113      }
3114      if (columnMetadata && columnMetadata[cell] && typeof columnMetadata[cell].focusable === "boolean") {
3115        return columnMetadata[cell].focusable;
3116      }
3117
3118      return columns[cell].focusable;
3119    }
3120
3121    function canCellBeSelected(row, cell) {
3122      if (row >= getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
3123        return false;
3124      }
3125
3126      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
3127      if (rowMetadata && typeof rowMetadata.selectable === "boolean") {
3128        return rowMetadata.selectable;
3129      }
3130
3131      var columnMetadata = rowMetadata && rowMetadata.columns && (rowMetadata.columns[columns[cell].id] || rowMetadata.columns[cell]);
3132      if (columnMetadata && typeof columnMetadata.selectable === "boolean") {
3133        return columnMetadata.selectable;
3134      }
3135
3136      return columns[cell].selectable;
3137    }
3138
3139    function gotoCell(row, cell, forceEdit) {
3140      if (!initialized) { return; }
3141      if (!canCellBeActive(row, cell)) {
3142        return;
3143      }
3144
3145      if (!getEditorLock().commitCurrentEdit()) {
3146        return;
3147      }
3148
3149      scrollCellIntoView(row, cell, false);
3150
3151      var newCell = getCellNode(row, cell);
3152
3153      // if selecting the 'add new' row, start editing right away
3154      setActiveCellInternal(newCell, forceEdit || (row === getDataLength()) || options.autoEdit);
3155
3156      // if no editor was created, set the focus back on the grid
3157      if (!currentEditor) {
3158        setFocus();
3159      }
3160    }
3161
3162
3163    //////////////////////////////////////////////////////////////////////////////////////////////
3164    // IEditor implementation for the editor lock
3165
3166    function commitCurrentEdit() {
3167      var item = getDataItem(activeRow);
3168      var column = columns[activeCell];
3169
3170      if (currentEditor) {
3171        if (currentEditor.isValueChanged()) {
3172          var validationResults = currentEditor.validate();
3173
3174          if (validationResults.valid) {
3175            if (activeRow < getDataLength()) {
3176              var editCommand = {
3177                row: activeRow,
3178                cell: activeCell,
3179                editor: currentEditor,
3180                serializedValue: currentEditor.serializeValue(),
3181                prevSerializedValue: serializedEditorValue,
3182                execute: function () {
3183                  this.editor.applyValue(item, this.serializedValue);
3184                  updateRow(this.row);
3185                  trigger(self.onCellChange, {
3186                    row: activeRow,
3187                    cell: activeCell,
3188                    item: item
3189                  });
3190                },
3191                undo: function () {
3192                  this.editor.applyValue(item, this.prevSerializedValue);
3193                  updateRow(this.row);
3194                  trigger(self.onCellChange, {
3195                    row: activeRow,
3196                    cell: activeCell,
3197                    item: item
3198                  });
3199                }
3200              };
3201
3202              if (options.editCommandHandler) {
3203                makeActiveCellNormal();
3204                options.editCommandHandler(item, column, editCommand);
3205              } else {
3206                editCommand.execute();
3207                makeActiveCellNormal();
3208              }
3209
3210            } else {
3211              var newItem = {};
3212              currentEditor.applyValue(newItem, currentEditor.serializeValue());
3213              makeActiveCellNormal();
3214              trigger(self.onAddNewRow, {item: newItem, column: column});
3215            }
3216
3217            // check whether the lock has been re-acquired by event handlers
3218            return !getEditorLock().isActive();
3219          } else {
3220            // Re-add the CSS class to trigger transitions, if any.
3221            $(activeCellNode).removeClass("invalid");
3222            $(activeCellNode).width();  // force layout
3223            $(activeCellNode).addClass("invalid");
3224
3225            trigger(self.onValidationError, {
3226              editor: currentEditor,
3227              cellNode: activeCellNode,
3228              validationResults: validationResults,
3229              row: activeRow,
3230              cell: activeCell,
3231              column: column
3232            });
3233
3234            currentEditor.focus();
3235            return false;
3236          }
3237        }
3238
3239        makeActiveCellNormal();
3240      }
3241      return true;
3242    }
3243
3244    function cancelCurrentEdit() {
3245      makeActiveCellNormal();
3246      return true;
3247    }
3248
3249    function rowsToRanges(rows) {
3250      var ranges = [];
3251      var lastCell = columns.length - 1;
3252      for (var i = 0; i < rows.length; i++) {
3253        ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell));
3254      }
3255      return ranges;
3256    }
3257
3258    function getSelectedRows() {
3259      if (!selectionModel) {
3260        throw "Selection model is not set";
3261      }
3262      return selectedRows;
3263    }
3264
3265    function setSelectedRows(rows) {
3266      if (!selectionModel) {
3267        throw "Selection model is not set";
3268      }
3269      selectionModel.setSelectedRanges(rowsToRanges(rows));
3270    }
3271
3272
3273    //////////////////////////////////////////////////////////////////////////////////////////////
3274    // Debug
3275
3276    this.debug = function () {
3277      var s = "";
3278
3279      s += ("\n" + "counter_rows_rendered:  " + counter_rows_rendered);
3280      s += ("\n" + "counter_rows_removed:  " + counter_rows_removed);
3281      s += ("\n" + "renderedRows:  " + renderedRows);
3282      s += ("\n" + "numVisibleRows:  " + numVisibleRows);
3283      s += ("\n" + "maxSupportedCssHeight:  " + maxSupportedCssHeight);
3284      s += ("\n" + "n(umber of pages):  " + n);
3285      s += ("\n" + "(current) page:  " + page);
3286      s += ("\n" + "page height (ph):  " + ph);
3287      s += ("\n" + "vScrollDir:  " + vScrollDir);
3288
3289      alert(s);
3290    };
3291
3292    // a debug helper to be able to access private members
3293    this.eval = function (expr) {
3294      return eval(expr);
3295    };
3296
3297    //////////////////////////////////////////////////////////////////////////////////////////////
3298    // Public API
3299
3300    $.extend(this, {
3301      "slickGridVersion": "2.1",
3302
3303      // Events
3304      "onScroll": new Slick.Event(),
3305      "onSort": new Slick.Event(),
3306      "onHeaderMouseEnter": new Slick.Event(),
3307      "onHeaderMouseLeave": new Slick.Event(),
3308      "onHeaderContextMenu": new Slick.Event(),
3309      "onHeaderClick": new Slick.Event(),
3310      "onHeaderCellRendered": new Slick.Event(),
3311      "onBeforeHeaderCellDestroy": new Slick.Event(),
3312      "onHeaderRowCellRendered": new Slick.Event(),
3313      "onBeforeHeaderRowCellDestroy": new Slick.Event(),
3314      "onMouseEnter": new Slick.Event(),
3315      "onMouseLeave": new Slick.Event(),
3316      "onClick": new Slick.Event(),
3317      "onDblClick": new Slick.Event(),
3318      "onContextMenu": new Slick.Event(),
3319      "onKeyDown": new Slick.Event(),
3320      "onAddNewRow": new Slick.Event(),
3321      "onValidationError": new Slick.Event(),
3322      "onViewportChanged": new Slick.Event(),
3323      "onColumnsReordered": new Slick.Event(),
3324      "onColumnsResized": new Slick.Event(),
3325      "onCellChange": new Slick.Event(),
3326      "onBeforeEditCell": new Slick.Event(),
3327      "onBeforeCellEditorDestroy": new Slick.Event(),
3328      "onBeforeDestroy": new Slick.Event(),
3329      "onActiveCellChanged": new Slick.Event(),
3330      "onActiveCellPositionChanged": new Slick.Event(),
3331      "onDragInit": new Slick.Event(),
3332      "onDragStart": new Slick.Event(),
3333      "onDrag": new Slick.Event(),
3334      "onDragEnd": new Slick.Event(),
3335      "onSelectedRowsChanged": new Slick.Event(),
3336      "onCellCssStylesChanged": new Slick.Event(),
3337
3338      // Methods
3339      "registerPlugin": registerPlugin,
3340      "unregisterPlugin": unregisterPlugin,
3341      "getColumns": getColumns,
3342      "setColumns": setColumns,
3343      "getColumnIndex": getColumnIndex,
3344      "updateColumnHeader": updateColumnHeader,
3345      "setSortColumn": setSortColumn,
3346      "setSortColumns": setSortColumns,
3347      "getSortColumns": getSortColumns,
3348      "autosizeColumns": autosizeColumns,
3349      "getOptions": getOptions,
3350      "setOptions": setOptions,
3351      "getData": getData,
3352      "getDataLength": getDataLength,
3353      "getDataItem": getDataItem,
3354      "setData": setData,
3355      "getSelectionModel": getSelectionModel,
3356      "setSelectionModel": setSelectionModel,
3357      "getSelectedRows": getSelectedRows,
3358      "setSelectedRows": setSelectedRows,
3359      "getContainerNode": getContainerNode,
3360
3361      "render": render,
3362      "invalidate": invalidate,
3363      "invalidateRow": invalidateRow,
3364      "invalidateRows": invalidateRows,
3365      "invalidateAllRows": invalidateAllRows,
3366      "updateCell": updateCell,
3367      "updateRow": updateRow,
3368      "getViewport": getVisibleRange,
3369      "getRenderedRange": getRenderedRange,
3370      "resizeCanvas": resizeCanvas,
3371      "updateRowCount": updateRowCount,
3372      "scrollRowIntoView": scrollRowIntoView,
3373      "scrollRowToTop": scrollRowToTop,
3374      "scrollCellIntoView": scrollCellIntoView,
3375      "getCanvasNode": getCanvasNode,
3376      "focus": setFocus,
3377
3378      "getCellFromPoint": getCellFromPoint,
3379      "getCellFromEvent": getCellFromEvent,
3380      "getActiveCell": getActiveCell,
3381      "setActiveCell": setActiveCell,
3382      "getActiveCellNode": getActiveCellNode,
3383      "getActiveCellPosition": getActiveCellPosition,
3384      "resetActiveCell": resetActiveCell,
3385      "editActiveCell": makeActiveCellEditable,
3386      "getCellEditor": getCellEditor,
3387      "getCellNode": getCellNode,
3388      "getCellNodeBox": getCellNodeBox,
3389      "canCellBeSelected": canCellBeSelected,
3390      "canCellBeActive": canCellBeActive,
3391      "navigatePrev": navigatePrev,
3392      "navigateNext": navigateNext,
3393      "navigateUp": navigateUp,
3394      "navigateDown": navigateDown,
3395      "navigateLeft": navigateLeft,
3396      "navigateRight": navigateRight,
3397      "navigatePageUp": navigatePageUp,
3398      "navigatePageDown": navigatePageDown,
3399      "gotoCell": gotoCell,
3400      "getTopPanel": getTopPanel,
3401      "setTopPanelVisibility": setTopPanelVisibility,
3402      "setHeaderRowVisibility": setHeaderRowVisibility,
3403      "getHeaderRow": getHeaderRow,
3404      "getHeaderRowColumn": getHeaderRowColumn,
3405      "getGridPosition": getGridPosition,
3406      "flashCell": flashCell,
3407      "addCellCssStyles": addCellCssStyles,
3408      "setCellCssStyles": setCellCssStyles,
3409      "removeCellCssStyles": removeCellCssStyles,
3410      "getCellCssStyles": getCellCssStyles,
3411
3412      "init": finishInitialization,
3413      "destroy": destroy,
3414
3415      // IEditor implementation
3416      "getEditorLock": getEditorLock,
3417      "getEditController": getEditController
3418    });
3419
3420    init();
3421  }
3422}(jQuery));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.