You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by hn...@apache.org on 2020/03/04 13:08:45 UTC

[myfaces-tobago] branch tobago-4.x updated: TOBAGO-2021: Sheet should be able to lazy load rows by scroll events (#28)

This is an automated email from the ASF dual-hosted git repository.

hnoeth pushed a commit to branch tobago-4.x
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git


The following commit(s) were added to refs/heads/tobago-4.x by this push:
     new 96e8d95  TOBAGO-2021: Sheet should be able to lazy load rows by scroll events (#28)
96e8d95 is described below

commit 96e8d95d02370ddb6c892ab5d9e2d9486a195b35
Author: Udo Schnurpfeil <lo...@apache.org>
AuthorDate: Wed Mar 4 14:08:37 2020 +0100

    TOBAGO-2021: Sheet should be able to lazy load rows by scroll events (#28)
    
    TOBAGO-2021: Sheet should be able to lazy load rows by scroll events
---
 .../myfaces/tobago/component/Attributes.java       |   1 +
 .../apache/myfaces/tobago/event/SheetAction.java   |   7 +-
 .../tobago/internal/component/AbstractUISheet.java |  18 ++
 .../internal/renderkit/renderer/SheetRenderer.java |  16 +-
 .../taglib/component/SheetTagDeclaration.java      |   8 +
 .../tobago/renderkit/html/DataAttributes.java      |  20 +-
 .../tobago/example/demo/SheetController.java       |   9 +-
 .../apache/myfaces/tobago/example/demo/Demo.xml    |   1 +
 .../080-sheet/90-lazy/sheet_lazy.xhtml}            |   6 +-
 .../tobago-bootstrap/_version/js/tobago-sheet.js   | 270 ++++++++++++++++-----
 10 files changed, 289 insertions(+), 67 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
index dddc245..65e2723 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
@@ -145,6 +145,7 @@ public enum Attributes {
   labelWidth,
   large,
   layoutOrder,
+  lazy,
   left,
   level,
   lang,
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/event/SheetAction.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/event/SheetAction.java
index 277ab29..5580176 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/event/SheetAction.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/event/SheetAction.java
@@ -56,7 +56,12 @@ public enum SheetAction {
   /**
    * Sorting
    */
-  sort;
+  sort,
+
+  /**
+   * A lazy load is requested
+   */
+  lazy;
 
   private String bundleKey;
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java
index 52c1601..83cdfbb 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java
@@ -91,6 +91,21 @@ public abstract class AbstractUISheet extends AbstractUIData
   @Override
   public void encodeAll(FacesContext facesContext) throws IOException {
 
+    if (isLazy()) {
+      if (getRows() == 0) {
+        LOG.warn("Sheet id={} has lazy=true set, but not set the rows attribute!", getClientId(facesContext));
+      }
+      if (getShowRowRange() != ShowPosition.none) {
+        LOG.warn("Sheet id={} has lazy=true set, but also set showRowRange!=none!", getClientId(facesContext));
+      }
+      if (getShowPageRange() != ShowPosition.none) {
+        LOG.warn("Sheet id={} has lazy=true set, but also set showPageRange!=none!", getClientId(facesContext));
+      }
+      if (getShowDirectLinks() != ShowPosition.none) {
+        LOG.warn("Sheet id={} has lazy=true set, but also set showDirectLinks!=none!", getClientId(facesContext));
+      }
+    }
+
     final AbstractUIReload reload = ComponentUtils.getReloadFacet(this);
 
     if (reload != null && AjaxUtils.isAjaxRequest(facesContext) && reload.isRendered() && !reload.isUpdate()) {
@@ -544,6 +559,7 @@ public abstract class AbstractUISheet extends AbstractUIData
         first = getFirstRowIndexOfLastPage();
         break;
       case toRow:
+      case lazy:
         first = pageEvent.getValue() - 1;
         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
           first = getFirstRowIndexOfLastPage();
@@ -609,4 +625,6 @@ public abstract class AbstractUISheet extends AbstractUIData
   public abstract ShowPosition getShowPageRange();
 
   public abstract ShowPosition getShowDirectLinks();
+
+  public abstract boolean isLazy();
 }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
index 9133706..0f20820 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
@@ -73,6 +73,7 @@ import org.slf4j.LoggerFactory;
 
 import javax.el.ValueExpression;
 import javax.faces.application.Application;
+import javax.faces.component.NamingContainer;
 import javax.faces.component.UIColumn;
 import javax.faces.component.UIComponent;
 import javax.faces.component.UIData;
@@ -98,6 +99,7 @@ public class SheetRenderer extends RendererBase {
   private static final String SUFFIX_COLUMN_RENDERED = ComponentUtils.SUB_SEPARATOR + "rendered";
   private static final String SUFFIX_SCROLL_POSITION = ComponentUtils.SUB_SEPARATOR + "scrollPosition";
   private static final String SUFFIX_SELECTED = ComponentUtils.SUB_SEPARATOR + "selected";
+  private static final String SUFFIX_LAZY = NamingContainer.SEPARATOR_CHAR + "pageActionlazy";
   private static final String SUFFIX_PAGE_ACTION = "pageAction";
 
   @Override
@@ -195,6 +197,7 @@ public class SheetRenderer extends RendererBase {
           break;
         case toPage:
         case toRow:
+        case lazy:
           event = new PageActionEvent(component, action);
           final Integer target;
           final Object value;
@@ -280,6 +283,9 @@ public class SheetRenderer extends RendererBase {
     writer.writeAttribute(DataAttributes.BEHAVIOR_COMMANDS, JsonUtils.encode(commands), false);
     writer.writeAttribute(DataAttributes.SELECTION_MODE, sheet.getSelectable().name(), false);
     writer.writeAttribute(DataAttributes.FIRST, Integer.toString(sheet.getFirst()), false);
+    writer.writeAttribute(DataAttributes.ROWS, Integer.toString(sheet.getRows()), false);
+    writer.writeAttribute(DataAttributes.ROW_COUNT, Integer.toString(sheet.getRowCount()), false);
+    writer.writeAttribute(DataAttributes.LAZY, sheet.isLazy());
     final StringBuilder builder = new StringBuilder();
 
     final boolean autoLayout = sheet.isAutoLayout();
@@ -346,6 +352,10 @@ public class SheetRenderer extends RendererBase {
           sheetId + SUFFIX_SELECTED);
     }
 
+    if (sheet.isLazy()) {
+      encodeHiddenInput(writer,null, sheetId + SUFFIX_LAZY);
+    }
+
     StringBuilder expandedValue = null;
     if (sheet.isTreeModel()) {
       expandedValue = new StringBuilder(",");
@@ -661,11 +671,7 @@ public class SheetRenderer extends RendererBase {
       }
 
       writer.startElement(HtmlElements.TR);
-      if (rowRendered instanceof Boolean) {
-        // if rowRendered attribute is set we need the rowIndex on the client
-        writer.writeAttribute(DataAttributes.ROW_INDEX, rowIndex);
-      }
-
+      writer.writeAttribute(DataAttributes.ROW_INDEX, rowIndex);
       final boolean selected = selectedRows.contains(rowIndex);
       final String[] rowMarkups = (String[]) sheet.getAttributes().get("rowMarkup");
       Markup rowMarkup = Markup.NULL;
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java
index d845e5b..ec0086a 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java
@@ -24,6 +24,7 @@ import org.apache.myfaces.tobago.apt.annotation.BodyContentDescription;
 import org.apache.myfaces.tobago.apt.annotation.DynamicExpression;
 import org.apache.myfaces.tobago.apt.annotation.Facet;
 import org.apache.myfaces.tobago.apt.annotation.Markup;
+import org.apache.myfaces.tobago.apt.annotation.Preliminary;
 import org.apache.myfaces.tobago.apt.annotation.Tag;
 import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
 import org.apache.myfaces.tobago.apt.annotation.UIComponentTag;
@@ -260,6 +261,13 @@ public interface SheetTagDeclaration
       methodSignature = "javax.faces.event.ActionEvent")
   void setSortActionListener(String sortActionListener);
 
+  /**
+   * Preliminary feature: lazy loading by scrolling.
+   */
+  @Preliminary
+  @TagAttribute
+  @UIComponentTagAttribute(type = "boolean", defaultValue = "false")
+  void setLazy(String lazy);
 
   /**
    * Flag indicating if paging arrows are shown near direct links
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
index 7825257..fb70fd3 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
@@ -101,6 +101,12 @@ public enum DataAttributes implements MarkupLanguageAttributes {
   LAYOUT("data-tobago-layout"),
 
   /**
+   * Lazy loading in sheet.
+   * (Preliminary)
+   */
+  LAZY("data-tobago-lazy"),
+
+  /**
    * Defines the depth level of a tree node.
    */
   LEVEL("data-tobago-level"),
@@ -147,10 +153,22 @@ public enum DataAttributes implements MarkupLanguageAttributes {
   ROW_ACTION("data-tobago-row-action"),
 
   /*
-   * Holds the index of the row in a sheet, if the sheet has a rowRendered attribute.
+   * Holds the index of the row in a sheet.
    */
   ROW_INDEX("data-tobago-row-index"),
 
+  /**
+   * Number of rows to show/load for lazy loading in sheet.
+   * (Preliminary)
+   */
+  ROWS("data-tobago-rows"),
+
+  /**
+   * Number of all rows in sheet.
+   * (Preliminary)
+   */
+  ROW_COUNT("data-tobago-row-count"),
+
   SELECTION_MODE("data-tobago-selection-mode"),
 
   /**
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SheetController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SheetController.java
index 5bff77c..ce3968b8 100644
--- a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SheetController.java
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SheetController.java
@@ -82,12 +82,17 @@ public class SheetController implements Serializable {
   private void init() {
     solarList = astroData.findAll().collect(Collectors.toList());
 
+    int j = 1;
     hugeSolarList = new ArrayList<>();
-    for (int i = 1; i <= 12; i++) {
+    for (int i = 1;; i++) {
       for (final SolarObject solarObject : solarList) {
         final SolarObject solarObjectClone = new SolarObject(solarObject);
-        solarObjectClone.setName(solarObject.getName() + " (" + i + ". entry)");
         hugeSolarList.add(solarObjectClone);
+        solarObjectClone.setName("#" + j++ + " " +  solarObject.getName());
+
+        if (j > 10000) {
+          return;
+        }
       }
     }
   }
diff --git a/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/Demo.xml b/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/Demo.xml
index 1190e1f..7441472 100644
--- a/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/Demo.xml
+++ b/tobago-example/tobago-example-demo/src/main/resources/org/apache/myfaces/tobago/example/demo/Demo.xml
@@ -121,6 +121,7 @@
   <entry key="sheet_multi_header">Multi Header</entry>
   <entry key="sheet_tree">Column Tree</entry>
   <entry key="sheet_editable">Editable Sheet</entry>
+  <entry key="sheet_lazy">Lazy Loading</entry>
   <entry key="tree">Tree Control</entry>
   <entry key="tree_command_types">Command</entry>
   <entry key="tree_select">Select</entry>
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/40-test/3000-sheet/20-1000-entries/1000-entries.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/080-sheet/90-lazy/sheet_lazy.xhtml
similarity index 88%
rename from tobago-example/tobago-example-demo/src/main/webapp/content/40-test/3000-sheet/20-1000-entries/1000-entries.xhtml
rename to tobago-example/tobago-example-demo/src/main/webapp/content/20-component/080-sheet/90-lazy/sheet_lazy.xhtml
index 185ee4a..f9232fc 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/40-test/3000-sheet/20-1000-entries/1000-entries.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/080-sheet/90-lazy/sheet_lazy.xhtml
@@ -21,9 +21,11 @@
                 xmlns="http://www.w3.org/1999/xhtml"
                 xmlns:tc="http://myfaces.apache.org/tobago/component"
                 xmlns:ui="http://java.sun.com/jsf/facelets">
-  <ui:param name="title" value="#{demoBundle.sheet_types}"/>
+  <ui:param name="title" value="Lazy Loading Large Data"/>
   <tc:sheet value="#{sheetController.hugeSolarList}" id="sheet" var="luminary"
-            rows="10" markup="small" >
+            rows="100" markup="small" lazy="true"
+            showRowRange="none" showDirectLinks="none" showPageRange="none">
+    <tc:style maxHeight="500px"/>
     <tc:column label="Name">
       <tc:out value="#{luminary.name}" labelLayout="skip"/>
     </tc:column>
diff --git a/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/resources/tobago/standard/tobago-bootstrap/_version/js/tobago-sheet.js b/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/resources/tobago/standard/tobago-bootstrap/_version/js/tobago-sheet.js
index 5fe4529..1c73169 100644
--- a/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/resources/tobago/standard/tobago-bootstrap/_version/js/tobago-sheet.js
+++ b/tobago-theme/tobago-theme-standard/src/main/resources/META-INF/resources/tobago/standard/tobago-bootstrap/_version/js/tobago-sheet.js
@@ -51,6 +51,9 @@ Tobago.Sheet.init = function(elements) {
   sheets.each(function initSheets() {
     var sheet = jQuery(this);
     var id = sheet.attr("id");
+    if (sheet.data("tobago-lazy-initialized")) {
+      return;
+    }
     var commands = sheet.data("tobago-row-action");
     var click = commands ? commands.click : undefined;
     var dblclick = commands ? commands.dblclick : undefined;
@@ -90,24 +93,89 @@ Tobago.Sheet.prototype.reloadWithAction = function(source, action) {
     console.debug("reload sheet with action '" + action + "'"); // @DEV_ONLY
   var executeIds = this.id;
   var renderIds = this.id;
-//  if (this.behaviorCommands && this.behaviorCommands.reload) {
-//    if (this.behaviorCommands.reload.execute) {
-//      executeIds +=  " " + behaviorCommands.reload.execute;
-//    }
-//    if (this.behaviorCommands.reload.render) {
-//      renderIds +=  " " + this.behaviorCommands.reload.render;
-//    }
-//  }
+  var lazy = jQuery(Tobago.Utils.escapeClientId(this.id)).data("tobago-lazy");
+
   jsf.ajax.request(
       action,
       null,
       {
         "javax.faces.behavior.event": "reload",
         execute: executeIds,
-        render: renderIds
+        render: renderIds,
+        onevent: lazy ? Tobago.Sheet.lazyResponse : undefined,
+        onerror: lazy ? Tobago.Sheet.lazyError: undefined
       });
 };
 
+Tobago.Sheet.lazyResponse = function(event) {
+  if (event.status === "complete") {
+    var updates = event.responseXML.querySelectorAll("update");
+    for (var i = 0; i < updates.length; i++) {
+      var update = updates[i];
+      var id = update.getAttribute("id");
+      if (id.indexOf(":") > -1) { // is a JSF element id, but not a technical id from the framework
+        console.debug("[tobago-sheet][complete] Update after jsf.ajax complete: #" + id); // @DEV_ONLY
+
+        var sheet = document.getElementById(id);
+        sheet.id = id + "::lazy-temporary";
+
+        var page = Tobago.findPage();
+        page.get(0).insertAdjacentHTML("beforeend", "<div id='" + id + "'></div>");
+        var sheetLoader = document.getElementById(id);
+      }
+    }
+  } else if (event.status === "success") {
+    updates = event.responseXML.querySelectorAll("update");
+    for (i = 0; i < updates.length; i++) {
+      update = updates[i];
+      id = update.getAttribute("id");
+      if (id.indexOf(":") > -1) { // is a JSF element id, but not a technical id from the framework
+        console.debug("[tobago-sheet][success] Update after jsf.ajax complete: #" + id); // @DEV_ONLY
+
+        // sync the new rows into the sheet
+        sheetLoader = document.getElementById(id);
+        sheet = document.getElementById(id + "::lazy-temporary");
+        sheet.id = id;
+        var tbody = sheet.querySelector(".tobago-sheet-bodyTable > tbody");
+
+        var newRows = sheetLoader.querySelectorAll(".tobago-sheet-bodyTable > tbody > tr");
+        for (i = 0; i < newRows.length; i++) {
+          var newRow = newRows[i];
+          var rowIndex = Number(newRow.dataset.tobagoRowIndex);
+          var row = tbody.querySelector("tr[data-tobago-row-index='" + rowIndex + "']");
+          // replace the old row with the new row
+          row.insertAdjacentElement("afterend", newRow);
+          tbody.removeChild(row);
+        }
+
+        sheetLoader.parentElement.removeChild(sheetLoader);
+        jQuery(sheet).data("tobago-lazy-active", false);
+      }
+    }
+  }
+
+};
+
+Tobago.Sheet.lazyError = function(data) {
+  updates = event.responseXML.querySelectorAll("update");
+  for (i = 0; i < updates.length; i++) {
+    update = updates[i];
+    id = update.getAttribute("id");
+    if (id.indexOf(":") > -1) { // is a JSF element id, but not a technical id from the framework
+      var sheet = document.getElementById(id);
+      jQuery(sheet).data("tobago-lazy-active", false);
+      console.error("Sheet lazy loading error:"
+          + "\nError Description: " + data.description
+          + "\nError Name: " + data.errorName
+          + "\nError errorMessage: " + data.errorMessage
+          + "\nResponse Code: " + data.responseCode
+          + "\nResponse Text: " + data.responseText
+          + "\nStatus: " + data.status
+          + "\nType: " + data.type);
+    }
+  }
+};
+
 Tobago.Sheet.setup2 = function (sheets) {
 
   // synchronize column widths
@@ -426,7 +494,65 @@ Tobago.Sheet.setup2 = function (sheets) {
     sheet.find(".tobago-sheet-cell > input.tobago-sheet-columnSelector").click(function(event) {event.preventDefault()});
   });
 
-    // init reload
+  // lazy load by scrolling
+  jQuery(sheets).each(function () {
+    var $sheet = jQuery(this);
+    var lazy = $sheet.data("tobago-lazy");
+
+    function getTemplate(height, columns, rowIndex) {
+      var tr = document.createElement("tr");
+      tr.dataset.tobagoRowIndex = rowIndex;
+      tr.classList.add("tobago-sheet-row");
+      tr.setAttribute("dummy", "dummy");
+      tr.style.height = height + "px";
+      var td = document.createElement("td");
+      td.classList.add("tobago-sheet-cell");
+      td.colSpan = columns;
+      tr.appendChild(td);
+      return tr;
+    }
+
+    if (lazy) {
+      // prepare the sheet with some auto-created (empty) rows
+      var rows = $sheet.data("tobago-rows");
+      var rowCount = $sheet.data("tobago-row-count");
+      var $sheetBody = $sheet.find(".tobago-sheet-body");
+      var $tbody = $sheetBody.find("tbody");
+      var columns = $tbody.find("tr:first").find("td").length;
+      var height = $tbody.height() / rows;
+      var pointer = $tbody.get(0).rows[0]; // points current row
+      for (var i = 0; i < rowCount; i++) {
+        if (pointer) {
+          var rowIndex = Number(pointer.dataset.tobagoRowIndex);
+          if (i < rowIndex) {
+            var template = getTemplate(height, columns, i);
+            pointer.insertAdjacentElement("beforebegin", template);
+          } else if (i === rowIndex) {
+            pointer = pointer.nextElementSibling;
+          } else {
+            template = getTemplate(height, columns, i);
+            pointer.insertAdjacentElement("afterend", template);
+            pointer = template;
+          }
+        } else {
+          template = getTemplate(height, columns, i);
+          $tbody.get(0).insertAdjacentElement("beforeend", template);
+          pointer = template;
+        }
+      }
+
+      $sheetBody.bind("scroll", function () {
+        Tobago.Sheet.lazyCheck($sheet);
+      });
+
+      // initial
+      Tobago.Sheet.lazyCheck($sheet);
+
+      $sheet.data("tobago-lazy-initialized", true);
+    }
+  });
+
+  // init reload
   jQuery(sheets).filter("[data-tobago-reload]").each(function() {
     var sheet = jQuery(this);
     Tobago.Sheets.get(sheet.attr("id")).initReload();
@@ -453,6 +579,82 @@ Tobago.Sheet.setup2 = function (sheets) {
   });
 };
 
+/*
+  when an event occurs (initial load OR scroll event OR AJAX response)
+
+  then -> Tobago.Sheet.lazyCheck()
+          1. check, if the lazy reload is currently active
+             a) yes -> do nothing and exit
+             b) no  -> step 2.
+          2. check, if there are data need to load (depends on scroll position and already loaded data)
+             a) yes -> set lazy reload to active and make an AJAX request with Tobago.Sheet.reloadLazy()
+             b) no  -> do nothing and exit
+
+   AJAX response -> 1. update the rows in the sheet from the response
+                    2. go to the first part of this description
+*/
+
+Tobago.Sheet.lazyCheck = function($sheet) {
+  if ($sheet.data("tobago-lazy-active")) {
+    // nothing to do, because there is an active AJAX running
+    return;
+  }
+
+  var lastCheck = $sheet.data("tobago-lazy-last-check");
+  if (lastCheck && Date.now() - lastCheck < 100) {
+    // do nothing, because the last call was just a moment ago
+    return;
+  }
+
+  $sheet.data("tobago-lazy-last-check", Date.now());
+  var next = Tobago.Sheet.nextLazyLoad($sheet);
+  // console.info("next %o", next); // @DEV_ONLY
+  if (next) {
+    $sheet.data("tobago-lazy-active", true);
+    var id = $sheet.attr("id");
+    var input = document.getElementById(id + ":pageActionlazy");
+    input.value = next;
+    Tobago.Sheets.get(id).reloadWithAction(input, input.id);
+  }
+};
+
+Tobago.Sheet.nextLazyLoad = function($sheet) {
+  // find first tr in current visible area
+  var $sheetBody = $sheet.find(".tobago-sheet-body");
+  var rows = $sheet.data("tobago-rows");
+  var $tbody = $sheetBody.find("table tbody");
+  var min = 0;
+  var trElements = $tbody.prop("rows");
+  var max = trElements.length;
+  // binary search
+  while (min < max) {
+    var i = Math.floor((max - min) / 2) + min;
+    // console.log("min i max -> %d %d %d", min, i, max); // @DEV_ONLY
+    if (Tobago.Sheet.isRowAboveVisibleArea($sheetBody, trElements[i])) {
+      min = i + 1;
+    } else {
+      max = i;
+    }
+  }
+  for (i = min; i < min + rows && i < trElements.length; i++) {
+    if (Tobago.Sheet.isRowDummy($sheetBody, trElements[i])) {
+      return i + 1;
+    }
+  }
+
+  return null;
+};
+
+Tobago.Sheet.isRowAboveVisibleArea = function($sheetBody, tr) {
+  var viewStart = $sheetBody.prop("scrollTop");
+  var trEnd = tr.offsetTop + tr.clientHeight;
+  return trEnd < viewStart;
+};
+
+Tobago.Sheet.isRowDummy = function($sheetBody, tr) {
+  return tr.hasAttribute("dummy");
+};
+
 Tobago.Sheet.hideInputOrSubmit = function(input) {
   var output = input.siblings(".tobago-sheet-pagingOutput");
   var changed = output.html() !== input.val();
@@ -574,11 +776,7 @@ Tobago.Sheet.getRows = function($sheet) {
 };
 
 Tobago.Sheet.isRowSelected = function(sheet, row) {
-  var rowIndex = +row.data("tobago-row-index");
-  if (!rowIndex) {
-    rowIndex = row.index() + sheet.data("tobago-first");
-  }
-  return Tobago.Sheet.isSelected(sheet, rowIndex);
+  return Tobago.Sheet.isSelected(sheet, row.data("tobago-row-index"));
 };
 
 Tobago.Sheet.isSelected = function(sheet, rowIndex) {
@@ -638,12 +836,7 @@ Tobago.Sheet.selectRange = function($sheet, $rows, first, last, selectDeselected
 };
 
 Tobago.Sheet.getDataIndex = function(sheet, row) {
-  var rowIndex = row.data("tobago-row-index");
-  if (rowIndex) {
-    return +rowIndex;
-  } else {
-    return row.index() + sheet.data("tobago-first");
-  }
+  return row.data("tobago-row-index");
 };
 
 /**
@@ -685,38 +878,3 @@ Tobago.Sheet.isInputElement = function($element) {
     });
  };
 })(jQuery);
-
-/*
- var header = $(".tobago-sheet-headerTable");
- var body = $(".tobago-sheet-bodyTable");
-
- header;
- body;
-
- header.find("col").each(function(){
-
- console.info($(this).attr("width"));
- $(this).attr("width", 100);
- ;
- })
-
- header.find("col").each(function(){
-
- console.info($(this).attr("width"));
- ;
- })
-
-
- body.find("col").each(function(){
-
- console.info($(this).attr("width"));
- $(this).attr("width", 100);
- ;
- })
-
- body.find("col").each(function(){
-
- console.info($(this).attr("width"));
- ;
- })
- */