Skip to content
This repository
Browse code

SlickGrid v2.1 - Implement virtual rendering for columns.

  • Loading branch information...
commit e4f25cbb63722475618f019b92d26aa935a0e1ad 1 parent 5b42d6b
Michael Leibman authored July 06, 2012
6  examples/example-spreadsheet.html
@@ -23,6 +23,7 @@
23 23
   <div class="options-panel">
24 24
     <h2>Demonstrates:</h2>
25 25
     <ul>
  26
+      <li>Virtual scrolling on both rows and columns.</li>
26 27
       <li>Select a range of cells with a mouse</li>
27 28
       <li>Use Ctrl-C and Ctrl-V keyboard shortcuts to cut and paste cells</li>
28 29
       <li>Use Esc to cancel a copy and paste operation</li>
@@ -66,10 +67,11 @@
66 67
     }
67 68
   ];
68 69
 
69  
-  for (var i = 0; i < 26; i++) {
  70
+  for (var i = 0; i < 100; i++) {
70 71
     columns.push({
71 72
       id: i,
72  
-      name: String.fromCharCode("A".charCodeAt(0) + i),
  73
+      name: String.fromCharCode("A".charCodeAt(0) + (i / 26) | 0) +
  74
+            String.fromCharCode("A".charCodeAt(0) + (i % 26)),
73 75
       field: i,
74 76
       width: 60,
75 77
       editor: FormulaEditor
1  slick.grid.css
@@ -12,7 +12,6 @@ classes should alter those!
12 12
 }
13 13
 
14 14
 .slick-header-columns, .slick-headerrow-columns {
15  
-  width: 999999px;
16 15
   position: relative;
17 16
   white-space: nowrap;
18 17
   cursor: default;
423  slick.grid.js
@@ -7,7 +7,7 @@
7 7
  * Distributed under MIT license.
8 8
  * All rights reserved.
9 9
  *
10  
- * SlickGrid v2.0
  10
+ * SlickGrid v2.1
11 11
  *
12 12
  * NOTES:
13 13
  *     Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods.
@@ -68,7 +68,7 @@ if (typeof Slick === "undefined") {
68 68
       asyncEditorLoadDelay: 100,
69 69
       forceFitColumns: false,
70 70
       enableAsyncPostRender: false,
71  
-      asyncPostRenderDelay: 60,
  71
+      asyncPostRenderDelay: 50,
72 72
       autoHeight: false,
73 73
       editorLock: Slick.GlobalEditorLock,
74 74
       showHeaderRow: false,
@@ -105,7 +105,7 @@ if (typeof Slick === "undefined") {
105 105
 
106 106
     var page = 0;       // current page
107 107
     var offset = 0;     // current page offset
108  
-    var scrollDir = 1;
  108
+    var vScrollDir = 1;
109 109
 
110 110
     // private
111 111
     var initialized = false;
@@ -143,7 +143,9 @@ if (typeof Slick === "undefined") {
143 143
     var prevScrollTop = 0;
144 144
     var scrollTop = 0;
145 145
     var lastRenderedScrollTop = 0;
  146
+    var lastRenderedScrollLeft = 0;
146 147
     var prevScrollLeft = 0;
  148
+    var scrollLeft = 0;
147 149
 
148 150
     var selectionModel;
149 151
     var selectedRows = [];
@@ -153,6 +155,8 @@ if (typeof Slick === "undefined") {
153 155
 
154 156
     var columnsById = {};
155 157
     var sortColumns = [];
  158
+    var columnPosLeft = [];
  159
+    var columnPosRight = [];
156 160
 
157 161
 
158 162
     // async call handles
@@ -184,6 +188,12 @@ if (typeof Slick === "undefined") {
184 188
       options = $.extend({}, defaults, options);
185 189
       columnDefaults.width = options.defaultColumnWidth;
186 190
 
  191
+      columnsById = {};
  192
+      for (var i = 0; i < columns.length; i++) {
  193
+        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
  194
+        columnsById[m.id] = i;
  195
+      }
  196
+
187 197
       // validate loaded JavaScript modules against requested options
188 198
       if (options.enableColumnReorder && !$.fn.sortable) {
189 199
         throw new Error("SlickGrid's 'enableColumnReorder = true' option requires jquery-ui.sortable module to be loaded");
@@ -209,7 +219,8 @@ if (typeof Slick === "undefined") {
209 219
       $focusSink = $("<div tabIndex='0' hideFocus style='position:fixed;width:0;height:0;top:0;left:0;outline:0;'></div>").appendTo($container);
210 220
 
211 221
       $headerScroller = $("<div class='slick-header ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
212  
-      $headers = $("<div class='slick-header-columns' style='width:10000px; left:-1000px' />").appendTo($headerScroller);
  222
+      $headers = $("<div class='slick-header-columns' style='left:-1000px' />").appendTo($headerScroller);
  223
+      $headers.width(getHeadersWidth());
213 224
 
214 225
       $headerRowScroller = $("<div class='slick-headerrow ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
215 226
       $headerRow = $("<div class='slick-headerrow-columns' />").appendTo($headerRowScroller);
@@ -260,6 +271,7 @@ if (typeof Slick === "undefined") {
260 271
           });
261 272
         }
262 273
 
  274
+        updateColumnCaches();
263 275
         createColumnHeaders();
264 276
         setupColumnSort();
265 277
         createCssRules();
@@ -341,12 +353,22 @@ if (typeof Slick === "undefined") {
341 353
       return dim;
342 354
     }
343 355
 
  356
+    function getHeadersWidth() {
  357
+      var headersWidth = 0;
  358
+      for (var i = 0, ii = columns.length; i < ii; i++) {
  359
+        var width = columns[i].width;
  360
+        headersWidth += width;
  361
+      }
  362
+      headersWidth += scrollbarDimensions.width;
  363
+      return Math.max(headersWidth, viewportW) + 1000;
  364
+    }
  365
+
344 366
     function getCanvasWidth() {
345 367
       var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
346 368
       var rowWidth = 0;
347 369
       var i = columns.length;
348 370
       while (i--) {
349  
-        rowWidth += (columns[i].width || columnDefaults.width);
  371
+        rowWidth += columns[i].width;
350 372
       }
351 373
       return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth;
352 374
     }
@@ -358,6 +380,7 @@ if (typeof Slick === "undefined") {
358 380
       if (canvasWidth != oldCanvasWidth) {
359 381
         $canvas.width(canvasWidth);
360 382
         $headerRow.width(canvasWidth);
  383
+        $headers.width(getHeadersWidth());
361 384
         viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width);
362 385
       }
363 386
 
@@ -472,12 +495,11 @@ if (typeof Slick === "undefined") {
472 495
         });
473 496
 
474 497
       $headers.empty();
  498
+      $headers.width(getHeadersWidth());
475 499
       $headerRow.empty();
476  
-      columnsById = {};
477 500
 
478 501
       for (var i = 0; i < columns.length; i++) {
479  
-        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
480  
-        columnsById[m.id] = i;
  502
+        var m = columns[i];
481 503
 
482 504
         var header = $("<div class='ui-state-default slick-header-column' id='" + uid + m.id + "' />")
483 505
             .html("<span class='slick-column-name'>" + m.name + "</span>")
@@ -1021,6 +1043,8 @@ if (typeof Slick === "undefined") {
1021 1043
           h.width(columns[i].width - headerColumnWidthDiff);
1022 1044
         }
1023 1045
       }
  1046
+
  1047
+      updateColumnCaches();
1024 1048
     }
1025 1049
 
1026 1050
     function applyColumnWidths() {
@@ -1093,8 +1117,29 @@ if (typeof Slick === "undefined") {
1093 1117
       return columns;
1094 1118
     }
1095 1119
 
  1120
+    function updateColumnCaches() {
  1121
+      // Pre-calculate cell boundaries.
  1122
+      columnPosLeft = [];
  1123
+      columnPosRight = [];
  1124
+      var x = 0;
  1125
+      for (var i = 0, ii = columns.length; i < ii; i++) {
  1126
+        columnPosLeft[i] = x;
  1127
+        columnPosRight[i] = x + columns[i].width;
  1128
+        x += columns[i].width;
  1129
+      }
  1130
+    }
  1131
+
1096 1132
     function setColumns(columnDefinitions) {
1097 1133
       columns = columnDefinitions;
  1134
+
  1135
+      columnsById = {};
  1136
+      for (var i = 0; i < columns.length; i++) {
  1137
+        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
  1138
+        columnsById[m.id] = i;
  1139
+      }
  1140
+
  1141
+      updateColumnCaches();
  1142
+
1098 1143
       if (initialized) {
1099 1144
         invalidateAllRows();
1100 1145
         createColumnHeaders();
@@ -1195,12 +1240,12 @@ if (typeof Slick === "undefined") {
1195 1240
 
1196 1241
       if (offset != oldOffset) {
1197 1242
         var range = getVisibleRange(newScrollTop);
1198  
-        cleanupRows(range.top, range.bottom);
  1243
+        cleanupRows(range);
1199 1244
         updateRowPositions();
1200 1245
       }
1201 1246
 
1202 1247
       if (prevScrollTop != newScrollTop) {
1203  
-        scrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
  1248
+        vScrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
1204 1249
         $viewport[0].scrollTop = (lastRenderedScrollTop = scrollTop = prevScrollTop = newScrollTop);
1205 1250
 
1206 1251
         trigger(self.onViewportChanged, {});
@@ -1252,7 +1297,8 @@ if (typeof Slick === "undefined") {
1252 1297
       return item[columnDef.field];
1253 1298
     }
1254 1299
 
1255  
-    function appendRowHtml(stringArray, row) {
  1300
+    function appendRowHtml(stringArray, row, range) {
  1301
+      var cacheEntry = rowsCache[row];
1256 1302
       var d = getDataItem(row);
1257 1303
       var dataLoading = row < getDataLength() && !d;
1258 1304
       var cellCss;
@@ -1269,39 +1315,66 @@ if (typeof Slick === "undefined") {
1269 1315
       stringArray.push("<div class='ui-widget-content " + rowCss + "' style='top:" + (options.rowHeight * row - offset) + "px'>");
1270 1316
 
1271 1317
       var colspan, m;
1272  
-      for (var i = 0, cols = columns.length; i < cols; i++) {
  1318
+      for (var i = 0, ii = columns.length; i < ii; i++) {
1273 1319
         m = columns[i];
1274  
-        colspan = getColspan(row, i);
1275  
-        cellCss = "slick-cell l" + i + " r" + Math.min(columns.length - 1, i + colspan - 1) + (m.cssClass ? " " + m.cssClass : "");
1276  
-        if (row === activeRow && i === activeCell) {
1277  
-          cellCss += (" active");
  1320
+        colspan = 1;
  1321
+        if (metadata && metadata.columns) {
  1322
+          var columnData = metadata.columns[m.id] || metadata.columns[i];
  1323
+          colspan = (columnData && columnData.colspan) || 1;
  1324
+          if (colspan === "*") {
  1325
+            colspan = ii - i;
  1326
+          }
1278 1327
         }
1279 1328
 
1280  
-        // TODO:  merge them together in the setter
1281  
-        for (var key in cellCssClasses) {
1282  
-          if (cellCssClasses[key][row] && cellCssClasses[key][row][m.id]) {
1283  
-            cellCss += (" " + cellCssClasses[key][row][m.id]);
  1329
+        // Do not render cells outside of the viewport.
  1330
+        if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
  1331
+          if (columnPosLeft[i] > range.rightPx) {
  1332
+            // All columns to the right are outside the range.
  1333
+            break;
1284 1334
           }
1285  
-        }
1286 1335
 
1287  
-        stringArray.push("<div class='" + cellCss + "'>");
  1336
+          appendCellHtml(stringArray, row, i, colspan);
  1337
+        }
1288 1338
 
1289  
-        // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
1290  
-        if (d) {
1291  
-          var value = getDataItemValueForColumn(d, m);
1292  
-          stringArray.push(getFormatter(row, m)(row, i, value, m, d));
  1339
+        if (colspan > 1) {
  1340
+          i += (colspan - 1);
1293 1341
         }
  1342
+      }
1294 1343
 
1295  
-        stringArray.push("</div>");
  1344
+      stringArray.push("</div>");
  1345
+    }
1296 1346
 
1297  
-        if (colspan) {
1298  
-          i += (colspan - 1);
  1347
+    function appendCellHtml(stringArray, row, cell, colspan) {
  1348
+      var m = columns[cell];
  1349
+      var d = getDataItem(row);
  1350
+      var cellCss = "slick-cell l" + cell + " r" + Math.min(columns.length - 1, cell + colspan - 1) +
  1351
+          (m.cssClass ? " " + m.cssClass : "");
  1352
+      if (row === activeRow && cell === activeCell) {
  1353
+        cellCss += (" active");
  1354
+      }
  1355
+
  1356
+      // TODO:  merge them together in the setter
  1357
+      for (var key in cellCssClasses) {
  1358
+        if (cellCssClasses[key][row] && cellCssClasses[key][row][m.id]) {
  1359
+          cellCss += (" " + cellCssClasses[key][row][m.id]);
1299 1360
         }
1300 1361
       }
1301 1362
 
  1363
+      stringArray.push("<div class='" + cellCss + "'>");
  1364
+
  1365
+      // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
  1366
+      if (d) {
  1367
+        var value = getDataItemValueForColumn(d, m);
  1368
+        stringArray.push(getFormatter(row, m)(row, cell, value, m, d));
  1369
+      }
  1370
+
1302 1371
       stringArray.push("</div>");
  1372
+
  1373
+      rowsCache[row].cellRenderQueue.push(cell);
  1374
+      rowsCache[row].cellColSpans[cell] = colspan;
1303 1375
     }
1304 1376
 
  1377
+
1305 1378
     function cleanupRows(rangeToKeep) {
1306 1379
       for (var i in rowsCache) {
1307 1380
         if (((i = parseInt(i, 10)) !== activeRow) && (i < rangeToKeep.top || i > rangeToKeep.bottom)) {
@@ -1342,7 +1415,7 @@ if (typeof Slick === "undefined") {
1342 1415
       if (!rows || !rows.length) {
1343 1416
         return;
1344 1417
       }
1345  
-      scrollDir = 0;
  1418
+      vScrollDir = 0;
1346 1419
       for (i = 0, rl = rows.length; i < rl; i++) {
1347 1420
         if (currentEditor && activeRow === rows[i]) {
1348 1421
           makeActiveCellNormal();
@@ -1487,26 +1560,31 @@ if (typeof Slick === "undefined") {
1487 1560
       updateCanvasWidth(false);
1488 1561
     }
1489 1562
 
1490  
-    function getVisibleRange(viewportTop) {
  1563
+    function getVisibleRange(viewportTop, viewportLeft) {
1491 1564
       if (viewportTop == null) {
1492 1565
         viewportTop = scrollTop;
1493 1566
       }
  1567
+      if (viewportLeft == null) {
  1568
+        viewportLeft = scrollLeft;
  1569
+      }
1494 1570
 
1495 1571
       return {
1496 1572
         top: Math.floor((viewportTop + offset) / options.rowHeight),
1497  
-        bottom: Math.ceil((viewportTop + offset + viewportH) / options.rowHeight)
  1573
+        bottom: Math.ceil((viewportTop + offset + viewportH) / options.rowHeight),
  1574
+        leftPx: viewportLeft,
  1575
+        rightPx: viewportLeft + viewportW
1498 1576
       };
1499 1577
     }
1500 1578
 
1501  
-    function getRenderedRange(viewportTop) {
1502  
-      var range = getVisibleRange(viewportTop);
  1579
+    function getRenderedRange(viewportTop, viewportLeft) {
  1580
+      var range = getVisibleRange(viewportTop, viewportLeft);
1503 1581
       var buffer = Math.round(viewportH / options.rowHeight);
1504 1582
       var minBuffer = 3;
1505 1583
 
1506  
-      if (scrollDir == -1) {
  1584
+      if (vScrollDir == -1) {
1507 1585
         range.top -= buffer;
1508 1586
         range.bottom += minBuffer;
1509  
-      } else if (scrollDir == 1) {
  1587
+      } else if (vScrollDir == 1) {
1510 1588
         range.top -= minBuffer;
1511 1589
         range.bottom += buffer;
1512 1590
       } else {
@@ -1517,25 +1595,146 @@ if (typeof Slick === "undefined") {
1517 1595
       range.top = Math.max(0, range.top);
1518 1596
       range.bottom = Math.min(options.enableAddRow ? getDataLength() : getDataLength() - 1, range.bottom);
1519 1597
 
  1598
+      range.leftPx -= viewportW;
  1599
+      range.rightPx += viewportW;
  1600
+
  1601
+      range.leftPx = Math.max(0, range.leftPx);
  1602
+      range.rightPx = Math.min(canvasWidth, range.rightPx);
  1603
+
1520 1604
       return range;
1521 1605
     }
1522 1606
 
1523 1607
     function ensureCellNodesInRowsCache(row) {
1524 1608
       var cacheEntry = rowsCache[row];
1525 1609
       if (cacheEntry) {
1526  
-        if (!cacheEntry.cellNodes) {
1527  
-          cacheEntry.cellNodes = [];
1528  
-          cacheEntry.cellNodesByColumnIdx = [];
1529  
-
1530  
-          var columnIdx = 0, cellNodes = cacheEntry.rowNode.childNodes;
1531  
-          for (var j = 0, jj = cellNodes.length; j < jj; j++) {
1532  
-            cacheEntry.cellNodesByColumnIdx[columnIdx] = cacheEntry.cellNodes[j] = cellNodes[j];
1533  
-            columnIdx += getColspan(row, columnIdx);
  1610
+        if (cacheEntry.cellRenderQueue.length) {
  1611
+          var lastChild = cacheEntry.rowNode.lastChild;
  1612
+          while (cacheEntry.cellRenderQueue.length) {
  1613
+            var columnIdx = cacheEntry.cellRenderQueue.pop();
  1614
+            cacheEntry.cellNodesByColumnIdx[columnIdx] = lastChild;
  1615
+            lastChild = lastChild.previousSibling;
1534 1616
           }
1535 1617
         }
1536 1618
       }
1537 1619
     }
1538 1620
 
  1621
+    function cleanUpCells(range, row) {
  1622
+      var totalCellsRemoved = 0;
  1623
+      var cacheEntry = rowsCache[row];
  1624
+
  1625
+      // Remove cells outside the range.
  1626
+      var cellsToRemove = [];
  1627
+      for (var i in cacheEntry.cellNodesByColumnIdx) {
  1628
+        // I really hate it when people mess with Array.prototype.
  1629
+        if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(i)) {
  1630
+          continue;
  1631
+        }
  1632
+
  1633
+        // This is a string, so it needs to be cast back to a number.
  1634
+        i = i | 0;
  1635
+
  1636
+        var colspan = cacheEntry.cellColSpans[i];
  1637
+        if (columnPosLeft[i] > range.rightPx ||
  1638
+          columnPosRight[Math.min(columns.length - 1, i + colspan - 1)] < range.leftPx) {
  1639
+          if (!(row == activeRow && i == activeCell)) {
  1640
+            cellsToRemove.push(i);
  1641
+          }
  1642
+        }
  1643
+      }
  1644
+
  1645
+      var cellToRemove;
  1646
+      while ((cellToRemove = cellsToRemove.pop()) != null) {
  1647
+        cacheEntry.rowNode.removeChild(cacheEntry.cellNodesByColumnIdx[cellToRemove]);
  1648
+        delete cacheEntry.cellColSpans[cellToRemove];
  1649
+        delete cacheEntry.cellNodesByColumnIdx[cellToRemove];
  1650
+        if (postProcessedRows[row]) {
  1651
+          delete postProcessedRows[row][cellToRemove];
  1652
+        }
  1653
+        totalCellsRemoved++;
  1654
+      }
  1655
+    }
  1656
+
  1657
+    function cleanUpAndRenderCells(range) {
  1658
+      var cacheEntry;
  1659
+      var stringArray = [];
  1660
+      var processedRows = [];
  1661
+      var cellsAdded;
  1662
+      var totalCellsAdded = 0;
  1663
+      var colspan;
  1664
+
  1665
+      for (var row = range.top; row <= range.bottom; row++) {
  1666
+        cacheEntry = rowsCache[row];
  1667
+        if (!cacheEntry) {
  1668
+          continue;
  1669
+        }
  1670
+
  1671
+        // cellRenderQueue populated in renderRows() needs to be cleared first
  1672
+        ensureCellNodesInRowsCache(row);
  1673
+
  1674
+        cleanUpCells(range, row);
  1675
+
  1676
+        // Render missing cells.
  1677
+        cellsAdded = 0;
  1678
+
  1679
+        var metadata = data.getItemMetadata && data.getItemMetadata(row);
  1680
+        metadata = metadata && metadata.columns;
  1681
+
  1682
+        // TODO:  shorten this loop (index? heuristics? binary search?)
  1683
+        for (var i = 0, ii = columns.length; i < ii; i++) {
  1684
+          // Cells to the right are outside the range.
  1685
+          if (columnPosLeft[i] > range.rightPx) {
  1686
+            break;
  1687
+          }
  1688
+
  1689
+          // Already rendered.
  1690
+          if ((colspan = cacheEntry.cellColSpans[i]) != null) {
  1691
+            i += (colspan > 1 ? colspan - 1 : 0);
  1692
+            continue;
  1693
+          }
  1694
+
  1695
+          colspan = 1;
  1696
+          if (metadata) {
  1697
+            var columnData = metadata[columns[i].id] || metadata[i];
  1698
+            colspan = (columnData && columnData.colspan) || 1;
  1699
+            if (colspan === "*") {
  1700
+              colspan = ii - i;
  1701
+            }
  1702
+          }
  1703
+
  1704
+          if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
  1705
+            appendCellHtml(stringArray, row, i, colspan);
  1706
+            cellsAdded++;
  1707
+          }
  1708
+
  1709
+          i += (colspan > 1 ? colspan - 1 : 0);
  1710
+        }
  1711
+
  1712
+        if (cellsAdded) {
  1713
+          totalCellsAdded += cellsAdded;
  1714
+          processedRows.push(row);
  1715
+        }
  1716
+      }
  1717
+
  1718
+      if (!stringArray.length) {
  1719
+        return;
  1720
+      }
  1721
+
  1722
+      var x = document.createElement("div");
  1723
+      x.innerHTML = stringArray.join("");
  1724
+
  1725
+      var processedRow;
  1726
+      var node;
  1727
+      while ((processedRow = processedRows.pop()) != null) {
  1728
+        cacheEntry = rowsCache[processedRow];
  1729
+        var columnIdx;
  1730
+        while ((columnIdx = cacheEntry.cellRenderQueue.pop()) != null) {
  1731
+          node = x.lastChild;
  1732
+          cacheEntry.rowNode.appendChild(node);
  1733
+          cacheEntry.cellNodesByColumnIdx[columnIdx] = node;
  1734
+        }
  1735
+      }
  1736
+    }
  1737
+
1539 1738
     function renderRows(range) {
1540 1739
       var parentNode = $canvas[0],
1541 1740
           stringArray = [],
@@ -1548,7 +1747,26 @@ if (typeof Slick === "undefined") {
1548 1747
         }
1549 1748
         renderedRows++;
1550 1749
         rows.push(i);
1551  
-        appendRowHtml(stringArray, i);
  1750
+
  1751
+        // Create an entry right away so that appendRowHtml() can
  1752
+        // start populatating it.
  1753
+        rowsCache[i] = {
  1754
+          "rowNode": null,
  1755
+
  1756
+          // ColSpans of rendered cells (by column idx).
  1757
+          // Can also be used for checking whether a cell has been rendered.
  1758
+          "cellColSpans": [],
  1759
+
  1760
+          // Cell nodes (by column idx).  Lazy-populated by ensureCellNodesInRowsCache().
  1761
+          "cellNodesByColumnIdx": [],
  1762
+
  1763
+          // Column indices of cell nodes that have been rendered, but not yet indexed in
  1764
+          // cellNodesByColumnIdx.  These are in the same order as cell nodes added at the
  1765
+          // end of the row.
  1766
+          "cellRenderQueue": []
  1767
+        };
  1768
+
  1769
+        appendRowHtml(stringArray, i, range);
1552 1770
         if (activeCellNode && activeRow === i) {
1553 1771
           needToReselectCell = true;
1554 1772
         }
@@ -1560,12 +1778,8 @@ if (typeof Slick === "undefined") {
1560 1778
       var x = document.createElement("div");
1561 1779
       x.innerHTML = stringArray.join("");
1562 1780
 
1563  
-      for (var i = 0, ii = x.childNodes.length; i < ii; i++) {
1564  
-        rowsCache[rows[i]] = {
1565  
-          "rowNode": parentNode.appendChild(x.firstChild),
1566  
-          "cellNodes": null,
1567  
-          "cellNodesByColumnIdx": null
1568  
-        };
  1781
+      for (var i = 0, ii = rows.length; i < ii; i++) {
  1782
+        rowsCache[rows[i]].rowNode = parentNode.appendChild(x.firstChild);
1569 1783
       }
1570 1784
 
1571 1785
       if (needToReselectCell) {
@@ -1602,7 +1816,12 @@ if (typeof Slick === "undefined") {
1602 1816
       // remove rows no longer in the viewport
1603 1817
       cleanupRows(rendered);
1604 1818
 
1605  
-      // add new rows
  1819
+      // add new rows & missing cells in existing rows
  1820
+      if (lastRenderedScrollLeft != scrollLeft) {
  1821
+        cleanUpAndRenderCells(rendered);
  1822
+      }
  1823
+
  1824
+      // render missing rows
1606 1825
       renderRows(rendered);
1607 1826
 
1608 1827
       postProcessFromRow = visible.top;
@@ -1610,27 +1829,29 @@ if (typeof Slick === "undefined") {
1610 1829
       startPostProcessing();
1611 1830
 
1612 1831
       lastRenderedScrollTop = scrollTop;
  1832
+      lastRenderedScrollLeft = scrollLeft;
1613 1833
       h_render = null;
1614 1834
     }
1615 1835
 
1616 1836
     function handleScroll() {
1617 1837
       scrollTop = $viewport[0].scrollTop;
1618  
-      var scrollLeft = $viewport[0].scrollLeft;
1619  
-      var scrollDist = Math.abs(scrollTop - prevScrollTop);
  1838
+      scrollLeft = $viewport[0].scrollLeft;
  1839
+      var vScrollDist = Math.abs(scrollTop - prevScrollTop);
  1840
+      var hScrollDist = Math.abs(scrollLeft - prevScrollLeft);
1620 1841
 
1621  
-      if (scrollLeft !== prevScrollLeft) {
  1842
+      if (hScrollDist) {
1622 1843
         prevScrollLeft = scrollLeft;
1623 1844
         $headerScroller[0].scrollLeft = scrollLeft;
1624 1845
         $topPanelScroller[0].scrollLeft = scrollLeft;
1625 1846
         $headerRowScroller[0].scrollLeft = scrollLeft;
1626 1847
       }
1627 1848
 
1628  
-      if (scrollDist) {
1629  
-        scrollDir = prevScrollTop < scrollTop ? 1 : -1;
  1849
+      if (vScrollDist) {
  1850
+        vScrollDir = prevScrollTop < scrollTop ? 1 : -1;
1630 1851
         prevScrollTop = scrollTop;
1631 1852
 
1632 1853
         // switch virtual pages if needed
1633  
-        if (scrollDist < viewportH) {
  1854
+        if (vScrollDist < viewportH) {
1634 1855
           scrollTo(scrollTop + offset);
1635 1856
         } else {
1636 1857
           var oldOffset = offset;
@@ -1640,20 +1861,24 @@ if (typeof Slick === "undefined") {
1640 1861
             invalidateAllRows();
1641 1862
           }
1642 1863
         }
  1864
+      }
1643 1865
 
  1866
+      if (hScrollDist || vScrollDist) {
1644 1867
         if (h_render) {
1645 1868
           clearTimeout(h_render);
1646 1869
         }
1647 1870
 
1648  
-        if (Math.abs(lastRenderedScrollTop - scrollTop) < viewportH) {
1649  
-          if (Math.abs(lastRenderedScrollTop - scrollTop) > 20) {
  1871
+        if (Math.abs(lastRenderedScrollTop - scrollTop) > 20 ||
  1872
+            Math.abs(lastRenderedScrollLeft - scrollLeft) > 20) {
  1873
+          if (Math.abs(lastRenderedScrollTop - scrollTop) < viewportH &&
  1874
+              Math.abs(lastRenderedScrollLeft - scrollLeft) < viewportW) {
1650 1875
             render();
  1876
+          } else {
  1877
+            h_render = setTimeout(render, 50);
1651 1878
           }
1652  
-        } else {
1653  
-          h_render = setTimeout(render, 50);
1654  
-        }
1655 1879
 
1656  
-        trigger(self.onViewportChanged, {});
  1880
+          trigger(self.onViewportChanged, {});
  1881
+        }
1657 1882
       }
1658 1883
 
1659 1884
       trigger(self.onScroll, {scrollLeft: scrollLeft, scrollTop: scrollTop});
@@ -1661,22 +1886,34 @@ if (typeof Slick === "undefined") {
1661 1886
 
1662 1887
     function asyncPostProcessRows() {
1663 1888
       while (postProcessFromRow <= postProcessToRow) {
1664  
-        var row = (scrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
  1889
+        var row = (vScrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
1665 1890
         var cacheEntry = rowsCache[row];
1666  
-        if (!cacheEntry || postProcessedRows[row] || row >= getDataLength()) {
  1891
+        if (!cacheEntry || row >= getDataLength()) {
1667 1892
           continue;
1668 1893
         }
1669 1894
 
  1895
+        if (!postProcessedRows[row]) {
  1896
+          postProcessedRows[row] = {};
  1897
+        }
  1898
+
1670 1899
         ensureCellNodesInRowsCache(row);
1671  
-        for (var i = 0; i < cacheEntry.cellNodesByColumnIdx.length; i++) {
1672  
-          var m = columns[i];
1673  
-          if (m.asyncPostRender) {
1674  
-            var node = cacheEntry.cellNodesByColumnIdx[i];
1675  
-            m.asyncPostRender(node, postProcessFromRow, getDataItem(row), m);
  1900
+        for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
  1901
+          if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
  1902
+            continue;
  1903
+          }
  1904
+
  1905
+          columnIdx = columnIdx | 0;
  1906
+
  1907
+          var m = columns[columnIdx];
  1908
+          if (m.asyncPostRender && !postProcessedRows[row][columnIdx]) {
  1909
+            var node = cacheEntry.cellNodesByColumnIdx[columnIdx];
  1910
+            if (node) {
  1911
+              m.asyncPostRender(node, postProcessFromRow, getDataItem(row), m);
  1912
+            }
  1913
+            postProcessedRows[row][columnIdx] = true;
1676 1914
           }
1677 1915
         }
1678 1916
 
1679  
-        postProcessedRows[row] = true;
1680 1917
         h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
1681 1918
         return;
1682 1919
       }
@@ -2051,18 +2288,26 @@ if (typeof Slick === "undefined") {
2051 2288
       $focusSink[0].focus();
2052 2289
     }
2053 2290
 
2054  
-    function scrollActiveCellIntoView() {
2055  
-      if (activeCellNode) {
2056  
-        var left = $(activeCellNode).position().left,
2057  
-            right = left + $(activeCellNode).outerWidth(),
2058  
-            scrollLeft = $viewport.scrollLeft(),
2059  
-            scrollRight = scrollLeft + $viewport.width();
  2291
+    function scrollCellIntoView(row, cell) {
  2292
+      var colspan = getColspan(row, cell);
  2293
+      var left = columnPosLeft[cell],
  2294
+        right = columnPosRight[cell + (colspan > 1 ? colspan - 1 : 0)],
  2295
+        scrollRight = scrollLeft + viewportW;
2060 2296
 
2061  
-        if (left < scrollLeft) {
2062  
-          $viewport.scrollLeft(left);
2063  
-        } else if (right > scrollRight) {
2064  
-          $viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
2065  
-        }
  2297
+      if (left < scrollLeft) {
  2298
+        $viewport.scrollLeft(left);
  2299
+        handleScroll();
  2300
+        render();
  2301
+      } else if (right > scrollRight) {
  2302
+        $viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
  2303
+        handleScroll();
  2304
+        render();
  2305
+      }
  2306
+    }
  2307
+
  2308
+    function scrollActiveCellIntoView() {
  2309
+      if (activeRow != null && activeCell != null) {
  2310
+        scrollCellIntoView(activeRow, activeCell);
2066 2311
       }
2067 2312
     }
2068 2313
 
@@ -2097,7 +2342,6 @@ if (typeof Slick === "undefined") {
2097 2342
       }
2098 2343
 
2099 2344
       if (activeCellChanged) {
2100  
-        scrollActiveCellIntoView();
2101 2345
         trigger(self.onActiveCellChanged, getActiveCell());
2102 2346
       }
2103 2347
     }
@@ -2347,8 +2591,11 @@ if (typeof Slick === "undefined") {
2347 2591
       var colspan = (columnData && columnData.colspan);
2348 2592
       if (colspan === "*") {
2349 2593
         colspan = columns.length - cell;
  2594
+      } else {
  2595
+        colspan = colspan || 1;
2350 2596
       }
2351  
-      return (colspan || 1);
  2597
+
  2598
+      return colspan;
2352 2599
     }
2353 2600
 
2354 2601
     function findFirstFocusableCell(row) {
@@ -2559,6 +2806,7 @@ if (typeof Slick === "undefined") {
2559 2806
       if (pos) {
2560 2807
         var isAddNewRow = (pos.row == getDataLength());
2561 2808
         scrollRowIntoView(pos.row, !isAddNewRow);
  2809
+        scrollCellIntoView(pos.row, pos.cell);
2562 2810
         setActiveCellInternal(getCellNode(pos.row, pos.cell), isAddNewRow || options.autoEdit);
2563 2811
         activePosX = pos.posX;
2564 2812
       } else {
@@ -2585,6 +2833,7 @@ if (typeof Slick === "undefined") {
2585 2833
       }
2586 2834
 
2587 2835
       scrollRowIntoView(row, false);
  2836
+      scrollCellIntoView(row, cell);
2588 2837
       setActiveCellInternal(getCellNode(row, cell), false);
2589 2838
     }
2590 2839
 
@@ -2647,6 +2896,7 @@ if (typeof Slick === "undefined") {
2647 2896
       }
2648 2897
 
2649 2898
       scrollRowIntoView(row, false);
  2899
+      scrollCellIntoView(row, cell);
2650 2900
 
2651 2901
       var newCell = getCellNode(row, cell);
2652 2902
 
@@ -2778,7 +3028,7 @@ if (typeof Slick === "undefined") {
2778 3028
       s += ("\n" + "n(umber of pages):  " + n);
2779 3029
       s += ("\n" + "(current) page:  " + page);
2780 3030
       s += ("\n" + "page height (ph):  " + ph);
2781  
-      s += ("\n" + "scrollDir:  " + scrollDir);
  3031
+      s += ("\n" + "vScrollDir:  " + vScrollDir);
2782 3032
 
2783 3033
       alert(s);
2784 3034
     };
@@ -2862,6 +3112,7 @@ if (typeof Slick === "undefined") {
2862 3112
       "updateRowCount": updateRowCount,
2863 3113
       "scrollRowIntoView": scrollRowIntoView,
2864 3114
       "scrollRowToTop": scrollRowToTop,
  3115
+      "scrollCellIntoView": scrollCellIntoView,
2865 3116
       "getCanvasNode": getCanvasNode,
2866 3117
       "focus": setFocus,
2867 3118
 
14  tests/scrolling benchmarks.html
@@ -39,7 +39,7 @@
39 39
 		<script>
40 40
 		var grid;
41 41
 		var data = [];
42  
-        var extraColumns = 0;
  42
+        var extraColumns = 100;
43 43
 
44 44
 		function testPostRender(cellNode, row, dataContext, colDef) {
45 45
 			cellNode.style.backgroundColor = 'yellow';
@@ -83,6 +83,18 @@
83 83
             }
84 84
 
85 85
 
  86
+            data.getItemMetadata = function (row) {
  87
+              if (row % 2 === 1) {
  88
+                return {
  89
+                  "columns": {
  90
+                    "duration": {
  91
+                      "colspan": 3
  92
+                    }
  93
+                  }
  94
+                };
  95
+              }
  96
+            };
  97
+
86 98
             // initialize the grid
87 99
             grid = new Slick.Grid("#myGrid", data, columns, options);
88 100
 

0 notes on commit e4f25cb

Please sign in to comment.
Something went wrong with that request. Please try again.