PageSourceSearch

https://sgjp.pl/static/js/lib/slick.grid.js?1773928782

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