3 * (c) 2009-2012 Michael Leibman
4 * michael{dot}leibman{at}gmail{dot}com
5 * http://github.com/mleibman/slickgrid
7 * Distributed under MIT license.
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.
19 // make sure required JavaScript modules are loaded
20 if (typeof jQuery === "undefined") {
21 throw "SlickGrid requires jquery module to be loaded";
23 if (!jQuery.fn.drag) {
24 throw "SlickGrid requires jquery.event.drag module to be loaded";
26 if (typeof Slick === "undefined") {
27 throw "slick.core.js not loaded";
33 $.extend(true, window, {
39 // shared across all grids on the page
40 var scrollbarDimensions;
41 var maxSupportedCssHeight; // browser's breaking point
43 //////////////////////////////////////////////////////////////////////////////////////////////
44 // SlickGrid class implementation (available as Slick.Grid)
47 * Creates a new instance of the grid.
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.
55 function SlickGrid(container, data, columns, options) {
58 explicitInitialization: false,
60 defaultColumnWidth: 80,
62 leaveSpaceForNewRows: false,
65 enableCellNavigation: true,
66 enableColumnReorder: true,
67 asyncEditorLoading: false,
68 asyncEditorLoadDelay: 100,
69 forceFitColumns: false,
70 enableAsyncPostRender: false,
71 asyncPostRenderDelay: 50,
73 editorLock: Slick.GlobalEditorLock,
78 formatterFactory: null,
80 cellFlashingCssClass: "flashing",
81 selectedCellCssClass: "selected",
83 enableTextSelectionOnCells: false,
84 dataItemColumnValueExtractor: null,
86 multiColumnSort: false,
87 defaultFormatter: defaultFormatter,
88 forceSyncScrolling: false
91 var columnDefaults = {
96 rerenderOnResize: false,
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
110 var page = 0; // current page
111 var offset = 0; // current page offset
115 var initialized = false;
117 var uid = "slickgrid_" + Math.round(1000000 * Math.random());
119 var $focusSink, $focusSink2;
122 var $headerRow, $headerRowScroller, $headerRowSpacer;
123 var $topPanelScroller;
129 var stylesheet, columnCssRulesL, columnCssRulesR;
130 var viewportH, viewportW;
132 var viewportHasHScroll, viewportHasVScroll;
133 var headerColumnWidthDiff = 0, headerColumnHeightDiff = 0, // border+padding
134 cellWidthDiff = 0, cellHeightDiff = 0;
135 var absoluteColumnMinWidth;
136 var numberOfRows = 0;
138 var tabbingDirection = 1;
140 var activeRow, activeCell;
141 var activeCellNode = null;
142 var currentEditor = null;
143 var serializedEditorValue;
147 var renderedRows = 0;
149 var prevScrollTop = 0;
151 var lastRenderedScrollTop = 0;
152 var lastRenderedScrollLeft = 0;
153 var prevScrollLeft = 0;
157 var selectedRows = [];
160 var cellCssClasses = {};
162 var columnsById = {};
163 var sortColumns = [];
164 var columnPosLeft = [];
165 var columnPosRight = [];
168 // async call handles
169 var h_editorLoader = null;
171 var h_postrender = null;
172 var postProcessedRows = {};
173 var postProcessToRow = null;
174 var postProcessFromRow = null;
177 var counter_rows_rendered = 0;
178 var counter_rows_removed = 0;
181 //////////////////////////////////////////////////////////////////////////////////////////////
185 $container = $(container);
186 if ($container.length < 1) {
187 throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM.");
190 // calculate these only once and share between grid instances
191 maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight();
192 scrollbarDimensions = scrollbarDimensions || measureScrollbar();
194 options = $.extend({}, defaults, options);
195 validateAndEnforceOptions();
196 columnDefaults.width = options.defaultColumnWidth;
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;
205 if (m.maxWidth && m.width > m.maxWidth) {
206 m.width = m.maxWidth;
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");
216 "commitCurrentEdit": commitCurrentEdit,
217 "cancelCurrentEdit": cancelCurrentEdit
222 .css("overflow", "hidden")
225 .addClass("ui-widget");
227 // set up a positioning container if needed
228 if (!/relative|absolute|fixed/.test($container.css("position"))) {
229 $container.css("position", "relative");
232 $focusSink = $("<div tabIndex='0' hideFocus style='position:fixed;width:0;height:0;top:0;left:0;outline:0;'></div>").appendTo($container);
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());
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);
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);
247 if (!options.showTopPanel) {
248 $topPanelScroller.hide();
251 if (!options.showHeaderRow) {
252 $headerRowScroller.hide();
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");
258 $canvas = $("<div class='grid-canvas' />").appendTo($viewport);
260 $focusSink2 = $focusSink.clone().appendTo($container);
262 if (!options.explicitInitialization) {
263 finishInitialization();
267 function finishInitialization() {
271 viewportW = parseFloat($.css($container[0], "width", true));
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();
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)
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");
292 updateColumnCaches();
293 createColumnHeaders();
297 bindAncestorScrollEvents();
300 .bind("resize.slickgrid", resizeCanvas);
302 .bind("scroll", handleScroll);
304 .bind("contextmenu", handleHeaderContextMenu)
305 .bind("click", handleHeaderClick)
306 .delegate(".slick-header-column", "mouseenter", handleHeaderMouseEnter)
307 .delegate(".slick-header-column", "mouseleave", handleHeaderMouseLeave);
309 .bind("scroll", handleHeaderRowScroll);
310 $focusSink.add($focusSink2)
311 .bind("keydown", handleKeyDown);
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);
326 function registerPlugin(plugin) {
327 plugins.unshift(plugin);
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();
337 plugins.splice(i, 1);
343 function setSelectionModel(model) {
344 if (selectionModel) {
345 selectionModel.onSelectedRangesChanged.unsubscribe(handleSelectedRangesChanged);
346 if (selectionModel.destroy) {
347 selectionModel.destroy();
351 selectionModel = model;
352 if (selectionModel) {
353 selectionModel.init(self);
354 selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged);
358 function getSelectionModel() {
359 return selectionModel;
362 function getCanvasNode() {
366 function measureScrollbar() {
367 var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
369 width: $c.width() - $c[0].clientWidth,
370 height: $c.height() - $c[0].clientHeight
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;
382 headersWidth += scrollbarDimensions.width;
383 return Math.max(headersWidth, viewportW) + 1000;
386 function getCanvasWidth() {
387 var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
389 var i = columns.length;
391 rowWidth += columns[i].width;
393 return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth;
396 function updateCanvasWidth(forceColumnWidthsUpdate) {
397 var oldCanvasWidth = canvasWidth;
398 canvasWidth = getCanvasWidth();
400 if (canvasWidth != oldCanvasWidth) {
401 $canvas.width(canvasWidth);
402 $headerRow.width(canvasWidth);
403 $headers.width(getHeadersWidth());
404 viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width);
407 $headerRowSpacer.width(canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0));
409 if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) {
414 function disableSelection($target) {
415 if ($target && $target.jquery) {
417 .attr("unselectable", "on")
418 .css("MozUserSelect", "none")
419 .bind("selectstart.ui", function () {
421 }); // from jquery:ui.core.js 1.7.2
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);
432 var test = supportedHeight * 2;
433 div.css("height", test);
434 if (test > testUpTo || div.height() !== test) {
437 supportedHeight = test;
442 return supportedHeight;
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) {
452 if (!$boundAncestors) {
453 $boundAncestors = $elem;
455 $boundAncestors = $boundAncestors.add($elem);
457 $elem.bind("scroll." + uid, handleActiveCellPositionChange);
462 function unbindAncestorScrollEvents() {
463 if (!$boundAncestors) {
466 $boundAncestors.unbind("scroll." + uid);
467 $boundAncestors = null;
470 function updateColumnHeader(columnId, title, toolTip) {
471 if (!initialized) { return; }
472 var idx = getColumnIndex(columnId);
477 var columnDef = columns[idx];
478 var $header = $headers.children().eq(idx);
480 if (title !== undefined) {
481 columns[idx].name = title;
483 if (toolTip !== undefined) {
484 columns[idx].toolTip = toolTip;
487 trigger(self.onBeforeHeaderCellDestroy, {
493 .attr("title", toolTip || "")
494 .children().eq(0).html(title);
496 trigger(self.onHeaderCellRendered, {
503 function getHeaderRow() {
504 return $headerRow[0];
507 function getHeaderRowColumn(columnId) {
508 var idx = getColumnIndex(columnId);
509 var $header = $headerRow.children().eq(idx);
510 return $header && $header[0];
513 function createColumnHeaders() {
514 function onMouseEnter() {
515 $(this).addClass("ui-state-hover");
518 function onMouseLeave() {
519 $(this).removeClass("ui-state-hover");
522 $headers.find(".slick-header-column")
524 var columnDef = $(this).data("column");
526 trigger(self.onBeforeHeaderCellDestroy, {
533 $headers.width(getHeadersWidth());
535 $headerRow.find(".slick-headerrow-column")
537 var columnDef = $(this).data("column");
539 trigger(self.onBeforeHeaderRowCellDestroy, {
547 for (var i = 0; i < columns.length; i++) {
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 || "")
556 .addClass(m.headerCssClass || "")
559 if (options.enableColumnReorder || m.sortable) {
561 .on('mouseenter', onMouseEnter)
562 .on('mouseleave', onMouseLeave);
566 header.addClass("slick-header-sortable");
567 header.append("<span class='slick-sort-indicator' />");
570 trigger(self.onHeaderCellRendered, {
575 if (options.showHeaderRow) {
576 var headerRowCell = $("<div class='ui-state-default slick-headerrow-column l" + i + " r" + i + "'></div>")
578 .appendTo($headerRow);
580 trigger(self.onHeaderRowCellRendered, {
581 "node": headerRowCell[0],
587 setSortColumns(sortColumns);
589 if (options.enableColumnReorder) {
590 setupColumnReorder();
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;
599 if ($(e.target).hasClass("slick-resizable-handle")) {
603 var $col = $(e.target).closest(".slick-header-column");
608 var column = $col.data("column");
609 if (column.sortable) {
610 if (!getEditorLock().commitCurrentEdit()) {
616 for (; i < sortColumns.length; i++) {
617 if (sortColumns[i].columnId == column.id) {
618 sortOpts = sortColumns[i];
619 sortOpts.sortAsc = !sortOpts.sortAsc;
624 if (e.metaKey && options.multiColumnSort) {
626 sortColumns.splice(i, 1);
630 if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) {
635 sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc };
636 sortColumns.push(sortOpts);
637 } else if (sortColumns.length == 0) {
638 sortColumns.push(sortOpts);
642 setSortColumns(sortColumns);
644 if (!options.multiColumnSort) {
645 trigger(self.onSort, {
646 multiColumnSort: false,
648 sortAsc: sortOpts.sortAsc}, e);
650 trigger(self.onSort, {
651 multiColumnSort: true,
652 sortCols: $.map(sortColumns, function(col) {
653 return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc };
660 function setupColumnReorder() {
661 $headers.filter(":ui-sortable").sortable("destroy");
663 containment: "parent",
667 tolerance: "intersection",
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");
674 beforeStop: function (e, ui) {
675 $(ui.helper).removeClass("slick-header-column-active");
678 if (!getEditorLock().commitCurrentEdit()) {
679 $(this).sortable("cancel");
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, ""))]);
688 setColumns(reorderedColumns);
690 trigger(self.onColumnsReordered, {});
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) {
709 if (firstResizable === undefined) {
712 columnElements.each(function (i, e) {
713 if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) {
717 $("<div class='slick-resizable-handle' />")
719 .bind("dragstart", function (e, dd) {
720 if (!getEditorLock().commitCurrentEdit()) {
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();
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++) {
737 if (stretchLeewayOnRight !== null) {
739 stretchLeewayOnRight += c.maxWidth - c.previousWidth;
741 stretchLeewayOnRight = null;
744 shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
748 var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0;
749 for (j = 0; j <= i; j++) {
750 // columns on left only affect minPageX
753 if (stretchLeewayOnLeft !== null) {
755 stretchLeewayOnLeft += c.maxWidth - c.previousWidth;
757 stretchLeewayOnLeft = null;
760 shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
763 if (shrinkLeewayOnRight === null) {
764 shrinkLeewayOnRight = 100000;
766 if (shrinkLeewayOnLeft === null) {
767 shrinkLeewayOnLeft = 100000;
769 if (stretchLeewayOnRight === null) {
770 stretchLeewayOnRight = 100000;
772 if (stretchLeewayOnLeft === null) {
773 stretchLeewayOnLeft = 100000;
775 maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
776 minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
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
782 for (j = i; j >= 0; j--) {
785 actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
786 if (x && c.previousWidth + x < actualMinWidth) {
787 x += c.previousWidth - actualMinWidth;
788 c.width = actualMinWidth;
790 c.width = c.previousWidth + x;
796 if (options.forceFitColumns) {
798 for (j = i + 1; j < columnElements.length; j++) {
801 if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
802 x -= c.maxWidth - c.previousWidth;
803 c.width = c.maxWidth;
805 c.width = c.previousWidth + x;
811 } else { // stretch column
813 for (j = i; j >= 0; j--) {
816 if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
817 x -= c.maxWidth - c.previousWidth;
818 c.width = c.maxWidth;
820 c.width = c.previousWidth + x;
826 if (options.forceFitColumns) {
828 for (j = i + 1; j < columnElements.length; j++) {
831 actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
832 if (x && c.previousWidth + x < actualMinWidth) {
833 x += c.previousWidth - actualMinWidth;
834 c.width = actualMinWidth;
836 c.width = c.previousWidth + x;
843 applyColumnHeaderWidths();
844 if (options.syncColumnCellResize) {
848 .bind("dragend", function (e, dd) {
850 $(this).parent().removeClass("slick-header-column-active");
851 for (j = 0; j < columnElements.length; j++) {
853 newWidth = $(columnElements[j]).outerWidth();
855 if (c.previousWidth !== newWidth && c.rerenderOnResize) {
859 updateCanvasWidth(true);
861 trigger(self.onColumnsResized, {});
866 function getVBoxDelta($el) {
867 var p = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
869 $.each(p, function (n, val) {
870 delta += parseFloat($el.css(val)) || 0;
875 function measureCellPaddingAndBorder() {
877 var h = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"];
878 var v = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
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;
885 $.each(v, function (n, val) {
886 headerColumnHeightDiff += parseFloat(el.css(val)) || 0;
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;
896 $.each(v, function (n, val) {
897 cellHeightDiff += parseFloat(el.css(val)) || 0;
901 absoluteColumnMinWidth = Math.max(headerColumnWidthDiff, cellWidthDiff);
904 function createCssRules() {
905 $style = $("<style type='text/css' rel='stylesheet' />").appendTo($("head"));
906 var rowHeight = (options.rowHeight - cellHeightDiff);
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; }"
915 for (var i = 0; i < columns.length; i++) {
916 rules.push("." + uid + " .l" + i + " { }");
917 rules.push("." + uid + " .r" + i + " { }");
920 if ($style[0].styleSheet) { // IE
921 $style[0].styleSheet.cssText = rules.join(" ");
923 $style[0].appendChild(document.createTextNode(rules.join(" ")));
927 function getColumnCssRules(idx) {
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];
938 throw new Error("Cannot find stylesheet.");
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];
959 "left": columnCssRulesL[idx],
960 "right": columnCssRulesR[idx]
964 function removeCssRules() {
970 getEditorLock().cancelCurrentEdit();
972 trigger(self.onBeforeDestroy, {});
974 var i = plugins.length;
976 unregisterPlugin(plugins[i]);
979 if (options.enableColumnReorder) {
980 $headers.filter(":ui-sortable").sortable("destroy");
983 unbindAncestorScrollEvents();
984 $container.unbind(".slickgrid");
987 $canvas.unbind("draginit dragstart dragend drag");
988 $container.empty().removeClass(uid);
992 //////////////////////////////////////////////////////////////////////////////////////////////
995 function trigger(evt, args, e) {
996 e = e || new Slick.EventData();
999 return evt.notify(args, e, self);
1002 function getEditorLock() {
1003 return options.editorLock;
1006 function getEditController() {
1007 return editController;
1010 function getColumnIndex(id) {
1011 return columnsById[id];
1014 function autosizeColumns() {
1020 availWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
1022 for (i = 0; i < columns.length; i++) {
1024 widths.push(c.width);
1027 shrinkLeeway += c.width - Math.max(c.minWidth, absoluteColumnMinWidth);
1033 while (total > availWidth && shrinkLeeway) {
1034 var shrinkProportion = (total - availWidth) / shrinkLeeway;
1035 for (i = 0; i < columns.length && total > availWidth; i++) {
1037 var width = widths[i];
1038 if (!c.resizable || width <= c.minWidth || width <= absoluteColumnMinWidth) {
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;
1048 if (prevTotal == total) { // avoid infinite loop
1056 while (total < availWidth) {
1057 var growProportion = availWidth / total;
1058 for (i = 0; i < columns.length && total < availWidth; i++) {
1060 if (!c.resizable || c.maxWidth <= c.width) {
1063 var growSize = Math.min(Math.floor(growProportion * c.width) - c.width, (c.maxWidth - c.width) || 1000000) || 1;
1065 widths[i] += growSize;
1067 if (prevTotal == total) { // avoid infinite loop
1073 var reRender = false;
1074 for (i = 0; i < columns.length; i++) {
1075 if (columns[i].rerenderOnResize && columns[i].width != widths[i]) {
1078 columns[i].width = widths[i];
1081 applyColumnHeaderWidths();
1082 updateCanvasWidth(true);
1084 invalidateAllRows();
1089 function applyColumnHeaderWidths() {
1090 if (!initialized) { return; }
1092 for (var i = 0, headers = $headers.children(), ii = headers.length; i < ii; i++) {
1094 if (h.width() !== columns[i].width - headerColumnWidthDiff) {
1095 h.width(columns[i].width - headerColumnWidthDiff);
1099 updateColumnCaches();
1102 function applyColumnWidths() {
1104 for (var i = 0; i < columns.length; i++) {
1105 w = columns[i].width;
1107 rule = getColumnCssRules(i);
1108 rule.left.style.left = x + "px";
1109 rule.right.style.right = (canvasWidth - x - w) + "px";
1111 x += columns[i].width;
1115 function setSortColumn(columnId, ascending) {
1116 setSortColumns([{ columnId: columnId, sortAsc: ascending}]);
1119 function setSortColumns(cols) {
1122 var headerColumnEls = $headers.children();
1124 .removeClass("slick-header-column-sorted")
1125 .find(".slick-sort-indicator")
1126 .removeClass("slick-sort-indicator-asc slick-sort-indicator-desc");
1128 $.each(sortColumns, function(i, col) {
1129 if (col.sortAsc == null) {
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");
1142 function getSortColumns() {
1146 function handleSelectedRangesChanged(e, ranges) {
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);
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;
1163 setCellCssStyles(options.selectedCellCssClass, hash);
1165 trigger(self.onSelectedRowsChanged, {rows: getSelectedRows()}, e);
1168 function getColumns() {
1172 function updateColumnCaches() {
1173 // Pre-calculate cell boundaries.
1175 columnPosRight = [];
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;
1184 function setColumns(columnDefinitions) {
1185 columns = columnDefinitions;
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;
1194 if (m.maxWidth && m.width > m.maxWidth) {
1195 m.width = m.maxWidth;
1199 updateColumnCaches();
1202 invalidateAllRows();
1203 createColumnHeaders();
1207 applyColumnWidths();
1212 function getOptions() {
1216 function setOptions(args) {
1217 if (!getEditorLock().commitCurrentEdit()) {
1221 makeActiveCellNormal();
1223 if (options.enableAddRow !== args.enableAddRow) {
1224 invalidateRow(getDataLength());
1227 options = $.extend(options, args);
1228 validateAndEnforceOptions();
1230 $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
1234 function validateAndEnforceOptions() {
1235 if (options.autoHeight) {
1236 options.leaveSpaceForNewRows = false;
1240 function setData(newData, scrollToTop) {
1242 invalidateAllRows();
1249 function getData() {
1253 function getDataLength() {
1254 if (data.getLength) {
1255 return data.getLength();
1261 function getDataItem(i) {
1263 return data.getItem(i);
1269 function getTopPanel() {
1270 return $topPanel[0];
1273 function setTopPanelVisibility(visible) {
1274 if (options.showTopPanel != visible) {
1275 options.showTopPanel = visible;
1277 $topPanelScroller.slideDown("fast", resizeCanvas);
1279 $topPanelScroller.slideUp("fast", resizeCanvas);
1284 function setHeaderRowVisibility(visible) {
1285 if (options.showHeaderRow != visible) {
1286 options.showHeaderRow = visible;
1288 $headerRowScroller.slideDown("fast", resizeCanvas);
1290 $headerRowScroller.slideUp("fast", resizeCanvas);
1295 function getContainerNode() {
1296 return $container.get(0);
1299 //////////////////////////////////////////////////////////////////////////////////////////////
1300 // Rendering / Scrolling
1302 function getRowTop(row) {
1303 return options.rowHeight * row - offset;
1306 function getRowFromPosition(y) {
1307 return Math.floor((y + offset) / options.rowHeight);
1310 function scrollTo(y) {
1312 y = Math.min(y, th - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0));
1314 var oldOffset = offset;
1316 page = Math.min(n - 1, Math.floor(y / ph));
1317 offset = Math.round(page * cj);
1318 var newScrollTop = y - offset;
1320 if (offset != oldOffset) {
1321 var range = getVisibleRange(newScrollTop);
1323 updateRowPositions();
1326 if (prevScrollTop != newScrollTop) {
1327 vScrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
1328 $viewport[0].scrollTop = (lastRenderedScrollTop = scrollTop = prevScrollTop = newScrollTop);
1330 trigger(self.onViewportChanged, {});
1334 function defaultFormatter(row, cell, value, columnDef, dataContext) {
1335 if (value == null) {
1338 return (value + "").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
1342 function getFormatter(row, column) {
1343 var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
1345 // look up by id, then index
1346 var columnOverrides = rowMetadata &&
1347 rowMetadata.columns &&
1348 (rowMetadata.columns[column.id] || rowMetadata.columns[getColumnIndex(column.id)]);
1350 return (columnOverrides && columnOverrides.formatter) ||
1351 (rowMetadata && rowMetadata.formatter) ||
1353 (options.formatterFactory && options.formatterFactory.getFormatter(column)) ||
1354 options.defaultFormatter;
1357 function getEditor(row, cell) {
1358 var column = columns[cell];
1359 var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
1360 var columnMetadata = rowMetadata && rowMetadata.columns;
1362 if (columnMetadata && columnMetadata[column.id] && columnMetadata[column.id].editor !== undefined) {
1363 return columnMetadata[column.id].editor;
1365 if (columnMetadata && columnMetadata[cell] && columnMetadata[cell].editor !== undefined) {
1366 return columnMetadata[cell].editor;
1369 return column.editor || (options.editorFactory && options.editorFactory.getEditor(column));
1372 function getDataItemValueForColumn(item, columnDef) {
1373 if (options.dataItemColumnValueExtractor) {
1374 return options.dataItemColumnValueExtractor(item, columnDef);
1376 return item[columnDef.field];
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");
1387 var metadata = data.getItemMetadata && data.getItemMetadata(row);
1389 if (metadata && metadata.cssClasses) {
1390 rowCss += " " + metadata.cssClasses;
1393 stringArray.push("<div class='ui-widget-content " + rowCss + "' style='top:" + getRowTop(row) + "px'>");
1396 for (var i = 0, ii = columns.length; i < ii; i++) {
1399 if (metadata && metadata.columns) {
1400 var columnData = metadata.columns[m.id] || metadata.columns[i];
1401 colspan = (columnData && columnData.colspan) || 1;
1402 if (colspan === "*") {
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.
1414 appendCellHtml(stringArray, row, i, colspan, d);
1422 stringArray.push("</div>");
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");
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]);
1440 stringArray.push("<div class='" + cellCss + "'>");
1442 // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
1444 var value = getDataItemValueForColumn(item, m);
1445 stringArray.push(getFormatter(row, m)(row, cell, value, m, item));
1448 stringArray.push("</div>");
1450 rowsCache[row].cellRenderQueue.push(cell);
1451 rowsCache[row].cellColSpans[cell] = colspan;
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);
1463 function invalidate() {
1465 invalidateAllRows();
1469 function invalidateAllRows() {
1470 if (currentEditor) {
1471 makeActiveCellNormal();
1473 for (var row in rowsCache) {
1474 removeRowFromCache(row);
1478 function removeRowFromCache(row) {
1479 var cacheEntry = rowsCache[row];
1483 $canvas[0].removeChild(cacheEntry.rowNode);
1484 delete rowsCache[row];
1485 delete postProcessedRows[row];
1487 counter_rows_removed++;
1490 function invalidateRows(rows) {
1492 if (!rows || !rows.length) {
1496 for (i = 0, rl = rows.length; i < rl; i++) {
1497 if (currentEditor && activeRow === rows[i]) {
1498 makeActiveCellNormal();
1500 if (rowsCache[rows[i]]) {
1501 removeRowFromCache(rows[i]);
1506 function invalidateRow(row) {
1507 invalidateRows([row]);
1510 function updateCell(row, cell) {
1511 var cellNode = getCellNode(row, cell);
1516 var m = columns[cell], d = getDataItem(row);
1517 if (currentEditor && activeRow === row && activeCell === cell) {
1518 currentEditor.loadValue(d);
1520 cellNode.innerHTML = d ? getFormatter(row, m)(row, cell, getDataItemValueForColumn(d, m), m, d) : "";
1521 invalidatePostProcessingResults(row);
1525 function updateRow(row) {
1526 var cacheEntry = rowsCache[row];
1531 ensureCellNodesInRowsCache(row);
1533 var d = getDataItem(row);
1535 for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
1536 if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
1540 columnIdx = columnIdx | 0;
1541 var m = columns[columnIdx],
1542 node = cacheEntry.cellNodesByColumnIdx[columnIdx];
1544 if (row === activeRow && columnIdx === activeCell && currentEditor) {
1545 currentEditor.loadValue(d);
1547 node.innerHTML = getFormatter(row, m)(row, columnIdx, getDataItemValueForColumn(d, m), m, d);
1549 node.innerHTML = "";
1553 invalidatePostProcessingResults(row);
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);
1565 function resizeCanvas() {
1566 if (!initialized) { return; }
1567 if (options.autoHeight) {
1568 viewportH = options.rowHeight * (getDataLength() + (options.enableAddRow ? 1 : 0));
1570 viewportH = getViewportHeight();
1573 numVisibleRows = Math.ceil(viewportH / options.rowHeight);
1574 viewportW = parseFloat($.css($container[0], "width", true));
1575 if (!options.autoHeight) {
1576 $viewport.height(viewportH);
1579 if (options.forceFitColumns) {
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);
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);
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) {
1604 removeRowFromCache(i);
1608 if (activeCellNode && activeRow > l) {
1613 th = Math.max(options.rowHeight * numberOfRows, viewportH - scrollbarDimensions.height);
1614 if (th < maxSupportedCssHeight) {
1621 h = maxSupportedCssHeight;
1623 n = Math.floor(th / ph);
1624 cj = (th - h) / (n - 1);
1628 $canvas.css("height", h);
1629 scrollTop = $viewport[0].scrollTop;
1632 var oldScrollTopInRange = (scrollTop + offset <= th - viewportH);
1634 if (th == 0 || scrollTop == 0) {
1636 } else if (oldScrollTopInRange) {
1637 // maintain virtual position
1638 scrollTo(scrollTop + offset);
1641 scrollTo(th - viewportH);
1644 if (h != oldH && options.autoHeight) {
1648 if (options.forceFitColumns && oldViewportHasVScroll != viewportHasVScroll) {
1651 updateCanvasWidth(false);
1654 function getVisibleRange(viewportTop, viewportLeft) {
1655 if (viewportTop == null) {
1656 viewportTop = scrollTop;
1658 if (viewportLeft == null) {
1659 viewportLeft = scrollLeft;
1663 top: getRowFromPosition(viewportTop),
1664 bottom: getRowFromPosition(viewportTop + viewportH) + 1,
1665 leftPx: viewportLeft,
1666 rightPx: viewportLeft + viewportW
1670 function getRenderedRange(viewportTop, viewportLeft) {
1671 var range = getVisibleRange(viewportTop, viewportLeft);
1672 var buffer = Math.round(viewportH / options.rowHeight);
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;
1682 range.top -= minBuffer;
1683 range.bottom += minBuffer;
1686 range.top = Math.max(0, range.top);
1687 range.bottom = Math.min(options.enableAddRow ? getDataLength() : getDataLength() - 1, range.bottom);
1689 range.leftPx -= viewportW;
1690 range.rightPx += viewportW;
1692 range.leftPx = Math.max(0, range.leftPx);
1693 range.rightPx = Math.min(canvasWidth, range.rightPx);
1698 function ensureCellNodesInRowsCache(row) {
1699 var cacheEntry = rowsCache[row];
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;
1712 function cleanUpCells(range, row) {
1713 var totalCellsRemoved = 0;
1714 var cacheEntry = rowsCache[row];
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)) {
1724 // This is a string, so it needs to be cast back to a number.
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);
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];
1744 totalCellsRemoved++;
1748 function cleanUpAndRenderCells(range) {
1750 var stringArray = [];
1751 var processedRows = [];
1753 var totalCellsAdded = 0;
1756 for (var row = range.top, btm = range.bottom; row <= btm; row++) {
1757 cacheEntry = rowsCache[row];
1762 // cellRenderQueue populated in renderRows() needs to be cleared first
1763 ensureCellNodesInRowsCache(row);
1765 cleanUpCells(range, row);
1767 // Render missing cells.
1770 var metadata = data.getItemMetadata && data.getItemMetadata(row);
1771 metadata = metadata && metadata.columns;
1773 var d = getDataItem(row);
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) {
1782 // Already rendered.
1783 if ((colspan = cacheEntry.cellColSpans[i]) != null) {
1784 i += (colspan > 1 ? colspan - 1 : 0);
1790 var columnData = metadata[columns[i].id] || metadata[i];
1791 colspan = (columnData && columnData.colspan) || 1;
1792 if (colspan === "*") {
1797 if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
1798 appendCellHtml(stringArray, row, i, colspan, d);
1802 i += (colspan > 1 ? colspan - 1 : 0);
1806 totalCellsAdded += cellsAdded;
1807 processedRows.push(row);
1811 if (!stringArray.length) {
1815 var x = document.createElement("div");
1816 x.innerHTML = stringArray.join("");
1820 while ((processedRow = processedRows.pop()) != null) {
1821 cacheEntry = rowsCache[processedRow];
1823 while ((columnIdx = cacheEntry.cellRenderQueue.pop()) != null) {
1825 cacheEntry.rowNode.appendChild(node);
1826 cacheEntry.cellNodesByColumnIdx[columnIdx] = node;
1831 function renderRows(range) {
1832 var parentNode = $canvas[0],
1835 needToReselectCell = false,
1836 dataLength = getDataLength();
1838 for (var i = range.top, ii = range.bottom; i <= ii; i++) {
1845 // Create an entry right away so that appendRowHtml() can
1846 // start populatating it.
1850 // ColSpans of rendered cells (by column idx).
1851 // Can also be used for checking whether a cell has been rendered.
1854 // Cell nodes (by column idx). Lazy-populated by ensureCellNodesInRowsCache().
1855 "cellNodesByColumnIdx": [],
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
1860 "cellRenderQueue": []
1863 appendRowHtml(stringArray, i, range, dataLength);
1864 if (activeCellNode && activeRow === i) {
1865 needToReselectCell = true;
1867 counter_rows_rendered++;
1870 if (!rows.length) { return; }
1872 var x = document.createElement("div");
1873 x.innerHTML = stringArray.join("");
1875 for (var i = 0, ii = rows.length; i < ii; i++) {
1876 rowsCache[rows[i]].rowNode = parentNode.appendChild(x.firstChild);
1879 if (needToReselectCell) {
1880 activeCellNode = getCellNode(activeRow, activeCell);
1884 function startPostProcessing() {
1885 if (!options.enableAsyncPostRender) {
1888 clearTimeout(h_postrender);
1889 h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
1892 function invalidatePostProcessingResults(row) {
1893 delete postProcessedRows[row];
1894 postProcessFromRow = Math.min(postProcessFromRow, row);
1895 postProcessToRow = Math.max(postProcessToRow, row);
1896 startPostProcessing();
1899 function updateRowPositions() {
1900 for (var row in rowsCache) {
1901 rowsCache[row].rowNode.style.top = getRowTop(row) + "px";
1906 if (!initialized) { return; }
1907 var visible = getVisibleRange();
1908 var rendered = getRenderedRange();
1910 // remove rows no longer in the viewport
1911 cleanupRows(rendered);
1913 // add new rows & missing cells in existing rows
1914 if (lastRenderedScrollLeft != scrollLeft) {
1915 cleanUpAndRenderCells(rendered);
1918 // render missing rows
1919 renderRows(rendered);
1921 postProcessFromRow = visible.top;
1922 postProcessToRow = Math.min(options.enableAddRow ? getDataLength() : getDataLength() - 1, visible.bottom);
1923 startPostProcessing();
1925 lastRenderedScrollTop = scrollTop;
1926 lastRenderedScrollLeft = scrollLeft;
1930 function handleHeaderRowScroll() {
1931 var scrollLeft = $headerRowScroller[0].scrollLeft;
1932 if (scrollLeft != $viewport[0].scrollLeft) {
1933 $viewport[0].scrollLeft = scrollLeft;
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);
1944 prevScrollLeft = scrollLeft;
1945 $headerScroller[0].scrollLeft = scrollLeft;
1946 $topPanelScroller[0].scrollLeft = scrollLeft;
1947 $headerRowScroller[0].scrollLeft = scrollLeft;
1951 vScrollDir = prevScrollTop < scrollTop ? 1 : -1;
1952 prevScrollTop = scrollTop;
1954 // switch virtual pages if needed
1955 if (vScrollDist < viewportH) {
1956 scrollTo(scrollTop + offset);
1958 var oldOffset = offset;
1959 if (h == viewportH) {
1962 page = Math.min(n - 1, Math.floor(scrollTop * ((th - viewportH) / (h - viewportH)) * (1 / ph)));
1964 offset = Math.round(page * cj);
1965 if (oldOffset != offset) {
1966 invalidateAllRows();
1971 if (hScrollDist || vScrollDist) {
1973 clearTimeout(h_render);
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)) {
1983 h_render = setTimeout(render, 50);
1986 trigger(self.onViewportChanged, {});
1990 trigger(self.onScroll, {scrollLeft: scrollLeft, scrollTop: scrollTop});
1993 function asyncPostProcessRows() {
1994 while (postProcessFromRow <= postProcessToRow) {
1995 var row = (vScrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
1996 var cacheEntry = rowsCache[row];
1997 if (!cacheEntry || row >= getDataLength()) {
2001 if (!postProcessedRows[row]) {
2002 postProcessedRows[row] = {};
2005 ensureCellNodesInRowsCache(row);
2006 for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
2007 if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
2011 columnIdx = columnIdx | 0;
2013 var m = columns[columnIdx];
2014 if (m.asyncPostRender && !postProcessedRows[row][columnIdx]) {
2015 var node = cacheEntry.cellNodesByColumnIdx[columnIdx];
2017 m.asyncPostRender(node, row, getDataItem(row), m);
2019 postProcessedRows[row][columnIdx] = true;
2023 h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
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];
2034 if (removedRowHash) {
2035 for (columnId in removedRowHash) {
2036 if (!addedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
2037 node = getCellNode(row, getColumnIndex(columnId));
2039 $(node).removeClass(removedRowHash[columnId]);
2046 for (columnId in addedRowHash) {
2047 if (!removedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
2048 node = getCellNode(row, getColumnIndex(columnId));
2050 $(node).addClass(addedRowHash[columnId]);
2058 function addCellCssStyles(key, hash) {
2059 if (cellCssClasses[key]) {
2060 throw "addCellCssStyles: cell CSS hash with key '" + key + "' already exists.";
2063 cellCssClasses[key] = hash;
2064 updateCellCssStylesOnRenderedRows(hash, null);
2066 trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash });
2069 function removeCellCssStyles(key) {
2070 if (!cellCssClasses[key]) {
2074 updateCellCssStylesOnRenderedRows(null, cellCssClasses[key]);
2075 delete cellCssClasses[key];
2077 trigger(self.onCellCssStylesChanged, { "key": key, "hash": null });
2080 function setCellCssStyles(key, hash) {
2081 var prevHash = cellCssClasses[key];
2083 cellCssClasses[key] = hash;
2084 updateCellCssStylesOnRenderedRows(hash, prevHash);
2086 trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash });
2089 function getCellCssStyles(key) {
2090 return cellCssClasses[key];
2093 function flashCell(row, cell, speed) {
2094 speed = speed || 100;
2095 if (rowsCache[row]) {
2096 var $cell = $(getCellNode(row, cell));
2098 function toggleCellClass(times) {
2102 setTimeout(function () {
2103 $cell.queue(function () {
2104 $cell.toggleClass(options.cellFlashingCssClass).dequeue();
2105 toggleCellClass(times - 1);
2115 //////////////////////////////////////////////////////////////////////////////////////////////
2118 function handleDragInit(e, dd) {
2119 var cell = getCellFromEvent(e);
2120 if (!cell || !cellExists(cell.row, cell.cell)) {
2124 var retval = trigger(self.onDragInit, dd, e);
2125 if (e.isImmediatePropagationStopped()) {
2129 // if nobody claims to be handling drag'n'drop by stopping immediate propagation,
2134 function handleDragStart(e, dd) {
2135 var cell = getCellFromEvent(e);
2136 if (!cell || !cellExists(cell.row, cell.cell)) {
2140 var retval = trigger(self.onDragStart, dd, e);
2141 if (e.isImmediatePropagationStopped()) {
2148 function handleDrag(e, dd) {
2149 return trigger(self.onDrag, dd, e);
2152 function handleDragEnd(e, dd) {
2153 trigger(self.onDragEnd, dd, e);
2156 function handleKeyDown(e) {
2157 trigger(self.onKeyDown, {row: activeRow, cell: activeCell}, e);
2158 var handled = e.isImmediatePropagationStopped();
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)
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) {
2181 if (activeRow === getDataLength()) {
2184 commitEditAndSetFocus();
2187 if (getEditorLock().commitCurrentEdit()) {
2188 makeActiveCellEditable();
2194 } else if (e.which == 9 && e.shiftKey && !e.ctrlKey && !e.altKey) {
2195 handled = navigatePrev();
2200 // the event has been handled so don't let parent element (bubbling/propagation) or browser (default) handle it
2201 e.stopPropagation();
2204 e.originalEvent.keyCode = 0; // prevent default behaviour for special keys in IE browsers (F3, F5, etc.)
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)
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) {
2222 var cell = getCellFromEvent(e);
2223 if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
2227 trigger(self.onClick, {row: cell.row, cell: cell.cell}, e);
2228 if (e.isImmediatePropagationStopped()) {
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);
2240 function handleContextMenu(e) {
2241 var $cell = $(e.target).closest(".slick-cell", $canvas);
2242 if ($cell.length === 0) {
2246 // are we editing this cell?
2247 if (activeCellNode === $cell[0] && currentEditor !== null) {
2251 trigger(self.onContextMenu, {}, e);
2254 function handleDblClick(e) {
2255 var cell = getCellFromEvent(e);
2256 if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
2260 trigger(self.onDblClick, {row: cell.row, cell: cell.cell}, e);
2261 if (e.isImmediatePropagationStopped()) {
2265 if (options.editable) {
2266 gotoCell(cell.row, cell.cell, true);
2270 function handleHeaderMouseEnter(e) {
2271 trigger(self.onHeaderMouseEnter, {
2272 "column": $(this).data("column")
2276 function handleHeaderMouseLeave(e) {
2277 trigger(self.onHeaderMouseLeave, {
2278 "column": $(this).data("column")
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);
2288 function handleHeaderClick(e) {
2289 var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
2290 var column = $header && $header.data("column");
2292 trigger(self.onHeaderClick, {column: column}, e);
2296 function handleMouseEnter(e) {
2297 trigger(self.onMouseEnter, {}, e);
2300 function handleMouseLeave(e) {
2301 trigger(self.onMouseLeave, {}, e);
2304 function cellExists(row, cell) {
2305 return !(row < 0 || row >= getDataLength() || cell < 0 || cell >= columns.length);
2308 function getCellFromPoint(x, y) {
2309 var row = getRowFromPosition(y);
2313 for (var i = 0; i < columns.length && w < x; i++) {
2314 w += columns[i].width;
2322 return {row: row, cell: cell - 1};
2325 function getCellFromNode(cellNode) {
2326 // read column number from .l<columnNumber> CSS class
2327 var cls = /l\d+/.exec(cellNode.className);
2329 throw "getCellFromNode: cannot get cell - " + cellNode.className;
2331 return parseInt(cls[0].substr(1, cls[0].length - 1), 10);
2334 function getRowFromNode(rowNode) {
2335 for (var row in rowsCache) {
2336 if (rowsCache[row].rowNode === rowNode) {
2344 function getCellFromEvent(e) {
2345 var $cell = $(e.target).closest(".slick-cell", $canvas);
2346 if (!$cell.length) {
2350 var row = getRowFromNode($cell[0].parentNode);
2351 var cell = getCellFromNode($cell[0]);
2353 if (row == null || cell == null) {
2363 function getCellNodeBox(row, cell) {
2364 if (!cellExists(row, cell)) {
2368 var y1 = getRowTop(row);
2369 var y2 = y1 + options.rowHeight - 1;
2371 for (var i = 0; i < cell; i++) {
2372 x1 += columns[i].width;
2374 var x2 = x1 + columns[cell].width;
2384 //////////////////////////////////////////////////////////////////////////////////////////////
2387 function resetActiveCell() {
2388 setActiveCellInternal(null, false);
2391 function setFocus() {
2392 if (tabbingDirection == -1) {
2393 $focusSink[0].focus();
2395 $focusSink2[0].focus();
2399 function scrollCellIntoView(row, cell, doPaging) {
2400 scrollRowIntoView(row, doPaging);
2402 var colspan = getColspan(row, cell);
2403 var left = columnPosLeft[cell],
2404 right = columnPosRight[cell + (colspan > 1 ? colspan - 1 : 0)],
2405 scrollRight = scrollLeft + viewportW;
2407 if (left < scrollLeft) {
2408 $viewport.scrollLeft(left);
2411 } else if (right > scrollRight) {
2412 $viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
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");
2427 var activeCellChanged = (activeCellNode !== newCell);
2428 activeCellNode = newCell;
2430 if (activeCellNode != null) {
2431 activeRow = getRowFromNode(activeCellNode.parentNode);
2432 activeCell = activePosX = getCellFromNode(activeCellNode);
2434 $(activeCellNode).addClass("active");
2435 $(rowsCache[activeRow].rowNode).addClass("active");
2437 if (options.editable && editMode && isCellPotentiallyEditable(activeRow, activeCell)) {
2438 clearTimeout(h_editorLoader);
2440 if (options.asyncEditorLoading) {
2441 h_editorLoader = setTimeout(function () {
2442 makeActiveCellEditable();
2443 }, options.asyncEditorLoadDelay);
2445 makeActiveCellEditable();
2449 activeRow = activeCell = null;
2452 if (activeCellChanged) {
2453 trigger(self.onActiveCellChanged, getActiveCell());
2457 function clearTextSelection() {
2458 if (document.selection && document.selection.empty) {
2460 //IE fails here if selected element is not in dom
2461 document.selection.empty();
2463 } else if (window.getSelection) {
2464 var sel = window.getSelection();
2465 if (sel && sel.removeAllRanges) {
2466 sel.removeAllRanges();
2471 function isCellPotentiallyEditable(row, cell) {
2472 // is the data for this row loaded?
2473 if (row < getDataLength() && !getDataItem(row)) {
2477 // are we in the Add New row? can we create new from this cell?
2478 if (columns[cell].cannotTriggerInsert && row >= getDataLength()) {
2482 // does this cell have an editor?
2483 if (!getEditor(row, cell)) {
2490 function makeActiveCellNormal() {
2491 if (!currentEditor) {
2494 trigger(self.onBeforeCellEditorDestroy, {editor: currentEditor});
2495 currentEditor.destroy();
2496 currentEditor = null;
2498 if (activeCellNode) {
2499 var d = getDataItem(activeRow);
2500 $(activeCellNode).removeClass("editable invalid");
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);
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();
2515 getEditorLock().deactivate(editController);
2518 function makeActiveCellEditable(editor) {
2519 if (!activeCellNode) {
2522 if (!options.editable) {
2523 throw "Grid : makeActiveCellEditable : should never get called when options.editable is false";
2526 // cancel pending async call if there is one
2527 clearTimeout(h_editorLoader);
2529 if (!isCellPotentiallyEditable(activeRow, activeCell)) {
2533 var columnDef = columns[activeCell];
2534 var item = getDataItem(activeRow);
2536 if (trigger(self.onBeforeEditCell, {row: activeRow, cell: activeCell, item: item, column: columnDef}) === false) {
2541 getEditorLock().activate(editController);
2542 $(activeCellNode).addClass("editable");
2544 // don't clear the cell if a custom editor is passed through
2546 activeCellNode.innerHTML = "";
2549 currentEditor = new (editor || getEditor(activeRow, activeCell))({
2551 gridPosition: absBox($container[0]),
2552 position: absBox(activeCellNode),
2553 container: activeCellNode,
2556 commitChanges: commitEditAndSetFocus,
2557 cancelChanges: cancelEditAndSetFocus
2561 currentEditor.loadValue(item);
2564 serializedEditorValue = currentEditor.serializeValue();
2566 if (currentEditor.position) {
2567 handleActiveCellPositionChange();
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()) {
2576 if (options.autoEdit) {
2582 function cancelEditAndSetFocus() {
2583 if (getEditorLock().cancelCurrentEdit()) {
2588 function absBox(elem) {
2590 top: elem.offsetTop,
2591 left: elem.offsetLeft,
2594 width: $(elem).outerWidth(),
2595 height: $(elem).outerHeight(),
2597 box.bottom = box.top + box.height;
2598 box.right = box.left + box.width;
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;
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;
2611 box.left -= elem.scrollLeft;
2612 box.top -= elem.scrollTop;
2614 if (elem === offsetParent) {
2615 box.left += elem.offsetLeft;
2616 box.top += elem.offsetTop;
2617 offsetParent = elem.offsetParent;
2620 box.bottom = box.top + box.height;
2621 box.right = box.left + box.width;
2627 function getActiveCellPosition() {
2628 return absBox(activeCellNode);
2631 function getGridPosition() {
2632 return absBox($container[0])
2635 function handleActiveCellPositionChange() {
2636 if (!activeCellNode) {
2640 trigger(self.onActiveCellPositionChanged, {});
2642 if (currentEditor) {
2643 var cellBox = getActiveCellPosition();
2644 if (currentEditor.show && currentEditor.hide) {
2645 if (!cellBox.visible) {
2646 currentEditor.hide();
2648 currentEditor.show();
2652 if (currentEditor.position) {
2653 currentEditor.position(cellBox);
2658 function getCellEditor() {
2659 return currentEditor;
2662 function getActiveCell() {
2663 if (!activeCellNode) {
2666 return {row: activeRow, cell: activeCell};
2670 function getActiveCellNode() {
2671 return activeCellNode;
2674 function scrollRowIntoView(row, doPaging) {
2675 var rowAtTop = row * options.rowHeight;
2676 var rowAtBottom = (row + 1) * options.rowHeight - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0);
2678 // need to page down?
2679 if ((row + 1) * options.rowHeight > scrollTop + viewportH + offset) {
2680 scrollTo(doPaging ? rowAtTop : rowAtBottom);
2684 else if (row * options.rowHeight < scrollTop + offset) {
2685 scrollTo(doPaging ? rowAtBottom : rowAtTop);
2690 function scrollRowToTop(row) {
2691 scrollTo(row * options.rowHeight);
2695 function getColspan(row, cell) {
2696 var metadata = data.getItemMetadata && data.getItemMetadata(row);
2697 if (!metadata || !metadata.columns) {
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;
2706 colspan = colspan || 1;
2712 function findFirstFocusableCell(row) {
2714 while (cell < columns.length) {
2715 if (canCellBeActive(row, cell)) {
2718 cell += getColspan(row, cell);
2723 function findLastFocusableCell(row) {
2725 var lastFocusableCell = null;
2726 while (cell < columns.length) {
2727 if (canCellBeActive(row, cell)) {
2728 lastFocusableCell = cell;
2730 cell += getColspan(row, cell);
2732 return lastFocusableCell;
2735 function gotoRight(row, cell, posX) {
2736 if (cell >= columns.length) {
2741 cell += getColspan(row, cell);
2743 while (cell < columns.length && !canCellBeActive(row, cell));
2745 if (cell < columns.length) {
2755 function gotoLeft(row, cell, posX) {
2760 var firstFocusableCell = findFirstFocusableCell(row);
2761 if (firstFocusableCell === null || firstFocusableCell >= cell) {
2767 "cell": firstFocusableCell,
2768 "posX": firstFocusableCell
2772 pos = gotoRight(prev.row, prev.cell, prev.posX);
2776 if (pos.cell >= cell) {
2783 function gotoDown(row, cell, posX) {
2786 if (++row >= getDataLength() + (options.enableAddRow ? 1 : 0)) {
2790 prevCell = cell = 0;
2791 while (cell <= posX) {
2793 cell += getColspan(row, cell);
2796 if (canCellBeActive(row, prevCell)) {
2806 function gotoUp(row, cell, posX) {
2813 prevCell = cell = 0;
2814 while (cell <= posX) {
2816 cell += getColspan(row, cell);
2819 if (canCellBeActive(row, prevCell)) {
2829 function gotoNext(row, cell, posX) {
2830 if (row == null && cell == null) {
2831 row = cell = posX = 0;
2832 if (canCellBeActive(row, cell)) {
2841 var pos = gotoRight(row, cell, posX);
2846 var firstFocusableCell = null;
2847 while (++row < getDataLength() + (options.enableAddRow ? 1 : 0)) {
2848 firstFocusableCell = findFirstFocusableCell(row);
2849 if (firstFocusableCell !== null) {
2852 "cell": firstFocusableCell,
2853 "posX": firstFocusableCell
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)) {
2874 var lastSelectableCell;
2876 pos = gotoLeft(row, cell, posX);
2885 lastSelectableCell = findLastFocusableCell(row);
2886 if (lastSelectableCell !== null) {
2889 "cell": lastSelectableCell,
2890 "posX": lastSelectableCell
2897 function navigateRight() {
2898 return navigate("right");
2901 function navigateLeft() {
2902 return navigate("left");
2905 function navigateDown() {
2906 return navigate("down");
2909 function navigateUp() {
2910 return navigate("up");
2913 function navigateNext() {
2914 return navigate("next");
2917 function navigatePrev() {
2918 return navigate("prev");
2922 * @param {string} dir Navigation direction.
2923 * @return {boolean} Whether navigation resulted in a change of active cell.
2925 function navigate(dir) {
2926 if (!options.enableCellNavigation) {
2930 if (!activeCellNode && dir != "prev" && dir != "next") {
2934 if (!getEditorLock().commitCurrentEdit()) {
2939 var tabbingDirections = {
2947 tabbingDirection = tabbingDirections[dir];
2949 var stepFunctions = {
2957 var stepFn = stepFunctions[dir];
2958 var pos = stepFn(activeRow, activeCell, activePosX);
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;
2966 setActiveCellInternal(getCellNode(activeRow, activeCell), (activeRow == getDataLength()) || options.autoEdit);
2971 function getCellNode(row, cell) {
2972 if (rowsCache[row]) {
2973 ensureCellNodesInRowsCache(row);
2974 return rowsCache[row].cellNodesByColumnIdx[cell];
2979 function setActiveCell(row, cell) {
2980 if (!initialized) { return; }
2981 if (row > getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
2985 if (!options.enableCellNavigation) {
2989 scrollCellIntoView(row, cell, false);
2990 setActiveCellInternal(getCellNode(row, cell), false);
2993 function canCellBeActive(row, cell) {
2994 if (!options.enableCellNavigation || row >= getDataLength() + (options.enableAddRow ? 1 : 0) ||
2995 row < 0 || cell >= columns.length || cell < 0) {
2999 var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
3000 if (rowMetadata && typeof rowMetadata.focusable === "boolean") {
3001 return rowMetadata.focusable;
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;
3008 if (columnMetadata && columnMetadata[cell] && typeof columnMetadata[cell].focusable === "boolean") {
3009 return columnMetadata[cell].focusable;
3012 return columns[cell].focusable;
3015 function canCellBeSelected(row, cell) {
3016 if (row >= getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
3020 var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
3021 if (rowMetadata && typeof rowMetadata.selectable === "boolean") {
3022 return rowMetadata.selectable;
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;
3030 return columns[cell].selectable;
3033 function gotoCell(row, cell, forceEdit) {
3034 if (!initialized) { return; }
3035 if (!canCellBeActive(row, cell)) {
3039 if (!getEditorLock().commitCurrentEdit()) {
3043 scrollCellIntoView(row, cell, false);
3045 var newCell = getCellNode(row, cell);
3047 // if selecting the 'add new' row, start editing right away
3048 setActiveCellInternal(newCell, forceEdit || (row === getDataLength()) || options.autoEdit);
3050 // if no editor was created, set the focus back on the grid
3051 if (!currentEditor) {
3057 //////////////////////////////////////////////////////////////////////////////////////////////
3058 // IEditor implementation for the editor lock
3060 function commitCurrentEdit() {
3061 var item = getDataItem(activeRow);
3062 var column = columns[activeCell];
3064 if (currentEditor) {
3065 if (currentEditor.isValueChanged()) {
3066 var validationResults = currentEditor.validate();
3068 if (validationResults.valid) {
3069 if (activeRow < getDataLength()) {
3073 editor: currentEditor,
3074 serializedValue: currentEditor.serializeValue(),
3075 prevSerializedValue: serializedEditorValue,
3076 execute: function () {
3077 this.editor.applyValue(item, this.serializedValue);
3078 updateRow(this.row);
3081 this.editor.applyValue(item, this.prevSerializedValue);
3082 updateRow(this.row);
3086 if (options.editCommandHandler) {
3087 makeActiveCellNormal();
3088 options.editCommandHandler(item, column, editCommand);
3090 editCommand.execute();
3091 makeActiveCellNormal();
3094 trigger(self.onCellChange, {
3101 currentEditor.applyValue(newItem, currentEditor.serializeValue());
3102 makeActiveCellNormal();
3103 trigger(self.onAddNewRow, {item: newItem, column: column});
3106 // check whether the lock has been re-acquired by event handlers
3107 return !getEditorLock().isActive();
3109 // Re-add the CSS class to trigger transitions, if any.
3110 $(activeCellNode).removeClass("invalid");
3111 $(activeCellNode).width(); // force layout
3112 $(activeCellNode).addClass("invalid");
3114 trigger(self.onValidationError, {
3115 editor: currentEditor,
3116 cellNode: activeCellNode,
3117 validationResults: validationResults,
3123 currentEditor.focus();
3128 makeActiveCellNormal();
3133 function cancelCurrentEdit() {
3134 makeActiveCellNormal();
3138 function rowsToRanges(rows) {
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));
3147 function getSelectedRows() {
3148 if (!selectionModel) {
3149 throw "Selection model is not set";
3151 return selectedRows;
3154 function setSelectedRows(rows) {
3155 if (!selectionModel) {
3156 throw "Selection model is not set";
3158 selectionModel.setSelectedRanges(rowsToRanges(rows));
3162 //////////////////////////////////////////////////////////////////////////////////////////////
3165 this.debug = function () {
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);
3181 // a debug helper to be able to access private members
3182 this.eval = function (expr) {
3186 //////////////////////////////////////////////////////////////////////////////////////////////
3190 "slickGridVersion": "2.1",
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(),
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,
3241 "getDataLength": getDataLength,
3242 "getDataItem": getDataItem,
3244 "getSelectionModel": getSelectionModel,
3245 "setSelectionModel": setSelectionModel,
3246 "getSelectedRows": getSelectedRows,
3247 "setSelectedRows": setSelectedRows,
3248 "getContainerNode": getContainerNode,
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,
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,
3299 "init": finishInitialization,
3302 // IEditor implementation
3303 "getEditorLock": getEditorLock,
3304 "getEditController": getEditController