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