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 2023/11/23 14:15:24 UTC
(myfaces-tobago) 01/02: fix(sheet): lazy loading + sorting
This is an automated email from the ASF dual-hosted git repository.
hnoeth pushed a commit to branch tobago-5.x
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git
commit fa57371ac9cdf0db08771cc6379778ab93eadbdb
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Thu Nov 16 14:38:39 2023 +0100
fix(sheet): lazy loading + sorting
* fix duplicated IDs
* fix sorting for a sheet with lazy loading
* fix scroll position if scrollbar jumped
* fix scroll position if loading previous rows
* fix scroll position if attribute "first" is used
* add test
Issue: TOBAGO-2261
Issue: TOBAGO-2262
Issue: TOBAGO-2264
---
.../tobago/internal/component/AbstractUISheet.java | 10 ++
.../internal/renderkit/renderer/SheetRenderer.java | 6 +-
.../3000-sheet/80-lazy-sort/Lazy_Sort.test.js | 175 +++++++++++++++++++++
.../3000-sheet/80-lazy-sort/Lazy_Sort.xhtml | 46 ++++++
.../src/main/ts/tobago-sheet.ts | 103 ++++++++----
5 files changed, 307 insertions(+), 33 deletions(-)
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 608790e68a..336b8d6048 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
@@ -80,6 +80,7 @@ public abstract class AbstractUISheet extends AbstractUIData
public static final String SORTER_ID = "sorter";
public static final String NOT_SORTABLE_COL_MESSAGE_ID = "org.apache.myfaces.tobago.UISheet.SORTING_COL";
public static final String NOT_SORTABLE_MESSAGE_ID = "org.apache.myfaces.tobago.UISheet.SORTING";
+ private static final String LAZY_UPDATE = "isLazyUpdate";
private SheetState state;
private transient MeasureList columnLayout;
@@ -541,6 +542,15 @@ public abstract class AbstractUISheet extends AbstractUIData
getState().setFirst(first);
}
+ public boolean getLazyUpdate() {
+ Object lazyUpdate = getStateHelper().get(LAZY_UPDATE);
+ return lazyUpdate != null && (boolean) lazyUpdate;
+ }
+
+ public void setLazyUpdate(boolean lazyUpdate) {
+ getStateHelper().put(LAZY_UPDATE, lazyUpdate);
+ }
+
@Override
public boolean isRendersRowContainer() {
return true;
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 4b8aa11abb..c26913259b 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
@@ -67,7 +67,6 @@ import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
import org.apache.myfaces.tobago.renderkit.html.HtmlButtonTypes;
import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
-import org.apache.myfaces.tobago.util.AjaxUtils;
import org.apache.myfaces.tobago.util.ComponentUtils;
import org.apache.myfaces.tobago.util.ResourceUtils;
import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
@@ -137,6 +136,9 @@ public class SheetRenderer<T extends AbstractUISheet> extends RendererBase<T> {
ComponentUtils.setAttribute(component, Attributes.selectedListString, selectedRows);
}
+ final String sourceId = facesContext.getExternalContext().getRequestParameterMap().get("javax.faces.source");
+ component.setLazyUpdate(sourceId != null && sourceId.equals(clientId + SUFFIX_LAZY));
+
final String value
= facesContext.getExternalContext().getRequestParameterMap().get(clientId + SUFFIX_SCROLL_POSITION);
if (value != null) {
@@ -279,7 +281,7 @@ public class SheetRenderer<T extends AbstractUISheet> extends RendererBase<T> {
writer.writeAttribute(CustomAttributes.ROWS, component.getRows());
writer.writeAttribute(CustomAttributes.ROW_COUNT, Integer.toString(component.getRowCount()), false);
writer.writeAttribute(CustomAttributes.LAZY, component.isLazy());
- writer.writeAttribute(CustomAttributes.LAZY_UPDATE, component.isLazy() && AjaxUtils.isAjaxRequest(facesContext));
+ writer.writeAttribute(CustomAttributes.LAZY_UPDATE, component.getLazyUpdate());
final boolean autoLayout = component.isAutoLayout();
if (!autoLayout) {
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/3000-sheet/80-lazy-sort/Lazy_Sort.test.js b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/3000-sheet/80-lazy-sort/Lazy_Sort.test.js
new file mode 100644
index 0000000000..7d967d5a54
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/3000-sheet/80-lazy-sort/Lazy_Sort.test.js
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {elementByIdFn, querySelectorAllFn, querySelectorFn} from "/script/tobago-test.js";
+import {JasmineTestTool} from "/tobago/test/tobago-test-tool.js";
+
+it("initial load", function (done) {
+ const submit = elementByIdFn("page:mainForm:submit");
+ const sheet = elementByIdFn("page:mainForm:sheet");
+
+ let dataTobagoFirst;
+
+ const test = new JasmineTestTool(done);
+ test.do(() => dataTobagoFirst = Number(sheet().getAttribute("data-tobago-first")));
+ test.setup(() => isLoaded(dataTobagoFirst - 20) && isLoaded(dataTobagoFirst), null, "click", submit);
+ test.do(() => expect(isLoaded(dataTobagoFirst - 21)).toBeFalse());
+ test.do(() => expect(isLoaded(dataTobagoFirst - 20, dataTobagoFirst + 19)).toBeTrue());
+ test.do(() => expect(isLoaded(dataTobagoFirst + 20)).toBeFalse());
+ test.start();
+});
+
+it("focus row index 5 (min row index is 0) and scroll up", function (done) {
+ let timestamp;
+
+ const test = new JasmineTestTool(done);
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.do(() => focusRowIndex(5));
+ test.wait(() => isLoaded(5));
+ test.do(() => expect(isLoaded(4)).toBeFalse());
+ test.do(() => expect(isLoaded(5, 24)).toBeTrue());
+ test.do(() => expect(isLoaded(25)).toBeFalse());
+
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.do(() => focusRowIndex(4));
+ test.wait(() => isLoaded(4));
+ test.do(() => expect(isLoaded(0, 24)).toBeTrue());
+ test.do(() => expect(isLoaded(25)).toBeFalse());
+ test.start();
+});
+
+it("focus row index 9970 (max row index is 9999) and scroll down", function (done) {
+ let timestamp;
+
+ const test = new JasmineTestTool(done);
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.do(() => focusRowIndex(9970));
+ test.wait(() => isLoaded(9970));
+ test.do(() => expect(isLoaded(9969)).toBeFalse());
+ test.do(() => expect(isLoaded(9970, 9989)).toBeTrue());
+ test.do(() => expect(isLoaded(9990)).toBeFalse());
+
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.do(() => focusRowIndex(9971));
+ test.wait(() => isLoaded(9990));
+ test.do(() => expect(isLoaded(9969)).toBeFalse());
+ test.do(() => expect(isLoaded(9970, 9999)).toBeTrue());
+ test.start();
+});
+
+it("focus row index 500 and go up", function (done) {
+ let timestamp;
+
+ const test = new JasmineTestTool(done);
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.do(() => focusRowIndex(500));
+ test.wait(() => isLoaded(500));
+ test.do(() => expect(isLoaded(499)).toBeFalse());
+ test.do(() => expect(isLoaded(500, 519)).toBeTrue());
+ test.do(() => expect(isLoaded(520)).toBeFalse());
+
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.do(() => focusRowIndex(499));
+ test.wait(() => isLoaded(499));
+ test.do(() => expect(isLoaded(479)).toBeFalse());
+ test.do(() => expect(isLoaded(480, 519)).toBeTrue());
+ test.do(() => expect(isLoaded(520)).toBeFalse());
+ test.start();
+});
+
+it("focus row index 1000 and go down", function (done) {
+ let timestamp;
+
+ const test = new JasmineTestTool(done);
+ test.do(() => focusRowIndex(1000));
+ test.wait(() => isLoaded(1000));
+ test.do(() => expect(isLoaded(999)).toBeFalse());
+ test.do(() => expect(isLoaded(1000, 1019)).toBeTrue());
+ test.do(() => expect(isLoaded(1020)).toBeFalse());
+
+ test.do(() => focusRowIndex(1001));
+ test.wait(() => isLoaded(1001));
+ test.do(() => expect(isLoaded(999)).toBeFalse());
+ test.do(() => expect(isLoaded(1000, 1039)).toBeTrue());
+ test.do(() => expect(isLoaded(1040)).toBeFalse());
+ test.start();
+});
+
+it("focus row index 1500 and sort name asc, focus row index 2000 and sort desc", function (done) {
+ const nameColumn = elementByIdFn("page:mainForm:sheet:nameColumn_sorter");
+ const nameOutputs = querySelectorAllFn("tr tobago-out[id$=':name']");
+
+ let timestamp;
+
+ const test = new JasmineTestTool(done);
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.do(() => focusRowIndex(1500));
+ test.wait(() => isLoaded(1500));
+ test.do(() => expect(isLoaded(1499)).toBeFalse());
+ test.do(() => expect(isLoaded(1500, 1519)).toBeTrue());
+ test.do(() => expect(isLoaded(1520)).toBeFalse());
+
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.event("click", nameColumn, () => nameOutputs().length === 40);
+ test.do(() => expect(nameOutputs().length).toEqual(40));
+ test.do(() => expect(nameOutputs()[0].id).not.toEqual("page:mainForm:sheet:0:name"));
+
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.do(() => focusRowIndex(2000));
+ test.wait(() => isLoaded(2000));
+ test.do(() => expect(isLoaded(1999)).toBeFalse());
+ test.do(() => expect(isLoaded(2000, 2019)).toBeTrue());
+ test.do(() => expect(isLoaded(2020)).toBeFalse());
+
+ test.do(() => timestamp = Date.now());
+ test.wait(() => Date.now() - timestamp > 100);
+ test.event("click", nameColumn, () => nameOutputs().length === 40);
+ test.do(() => expect(nameOutputs().length).toEqual(40));
+ test.do(() => expect(isLoaded(1979)).toBeFalse());
+ test.do(() => expect(isLoaded(1980, 2019)).toBeTrue());
+ test.do(() => expect(isLoaded(2020)).toBeFalse());
+ test.start();
+});
+
+function isLoaded(startRow, endRow) {
+ if (endRow === undefined) {
+ endRow = startRow;
+ }
+ for (let i = startRow; i <= endRow; i++) {
+ if (row(i).getAttribute("dummy") != null) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function row(rowIndex) {
+ return querySelectorFn("tr[row-index='" + rowIndex + "']")();
+}
+
+function focusRowIndex(rowIndex) {
+ querySelectorFn("#page\\:mainForm\\:sheet .tobago-body")().scrollTop = row(rowIndex).offsetTop;
+}
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/3000-sheet/80-lazy-sort/Lazy_Sort.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/3000-sheet/80-lazy-sort/Lazy_Sort.xhtml
new file mode 100644
index 0000000000..a65e4660e1
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/3000-sheet/80-lazy-sort/Lazy_Sort.xhtml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<ui:composition template="/main.xhtml"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tc="http://myfaces.apache.org/tobago/component"
+ xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
+ <tc:button id="submit" label="submit"/>
+
+ <tc:sheet value="#{sheetController.hugeSolarList}" id="sheet" var="luminary"
+ rows="20" markup="small" lazy="true" columns="auto" first="80"
+ showRowRange="none" showDirectLinks="none" showPageRange="none">
+ <tc:style maxHeight="500px"/>
+ <tc:column id="nameColumn" label="Name" sortable="true">
+ <tc:out id="name" value="#{luminary.name}" labelLayout="skip"/>
+ </tc:column>
+ <tc:column label="Orbit" sortable="true">
+ <tc:out value="#{luminary.orbit}" labelLayout="skip"/>
+ </tc:column>
+ <tc:column label="Period (Days)">
+ <tc:out value="#{luminary.period}" labelLayout="skip"/>
+ </tc:column>
+ <tc:column label="Discoverer">
+ <tc:out value="#{luminary.discoverer}" labelLayout="skip"/>
+ </tc:column>
+ <tc:column label="Year">
+ <tc:out value="#{luminary.discoverYear}" labelLayout="skip"/>
+ </tc:column>
+ </tc:sheet>
+</ui:composition>
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-sheet.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-sheet.ts
index 73753dc080..d4433b6413 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-sheet.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-sheet.ts
@@ -197,7 +197,7 @@ export class Sheet extends HTMLElement {
row.addEventListener("click", this.clickOnRow.bind(this));
}
}
- if (selectionMode === "multi" && this.getSelectorAllCheckbox()) {
+ if (selectionMode === "multi" && this.getSelectorAllCheckbox()) {
const selectedSet = new Set<number>(JSON.parse(this.getHiddenSelected().value));
this.calculateSelectorAllChecked(selectedSet);
}
@@ -219,6 +219,7 @@ export class Sheet extends HTMLElement {
const sheetBody = this.sheetBody;
const tableBody = this.tableBody;
const columns = tableBody.rows[0].cells.length;
+ const firstShownRow = tableBody.rows[0];
let current: HTMLTableRowElement = tableBody.rows[0]; // current row in this algorithm, begin with first
// the algorithm goes straight through all rows, not selectors, because of performance
for (let i = 0; i < rowCount; i++) {
@@ -240,6 +241,7 @@ export class Sheet extends HTMLElement {
}
}
+ this.sheetBody.scrollTop = firstShownRow.offsetTop;
sheetBody.addEventListener("scroll", this.lazyCheck.bind(this));
// initial
@@ -270,6 +272,7 @@ export class Sheet extends HTMLElement {
});
}
}
+
// attribute getter + setter ---------------------------------------------------------- //
get lazyActive(): boolean {
@@ -300,6 +303,22 @@ export class Sheet extends HTMLElement {
return this.hasAttribute("lazy-update");
}
+ get lazyUpdateUpperRows(): boolean {
+ return this.getAttribute("lazy-update-upper-rows") === "true";
+ }
+
+ set lazyUpdateUpperRows(value: boolean) {
+ this.setAttribute("lazy-update-upper-rows", String(value));
+ }
+
+ get lazyUpdatePreviousBodyScrollHeight(): number {
+ return Number(this.getAttribute("lazy-update-previous-body-scroll-height"));
+ }
+
+ set lazyUpdatePreviousBodyScrollHeight(value: number) {
+ this.setAttribute("lazy-update-previous-body-scroll-height", String(value));
+ }
+
get rows(): number {
return parseInt(this.getAttribute("rows"));
}
@@ -316,6 +335,24 @@ export class Sheet extends HTMLElement {
return this.querySelector(".tobago-body tbody");
}
+ get firstVisibleRowIndex(): number {
+ const rowElements = this.tableBody.rows;
+ let min = 0;
+ let max = rowElements.length;
+ let rowIndex: number;
+ while (min < max) {
+ rowIndex = Math.round((max - min) / 2) + min;
+ if (rowElements[rowIndex].offsetTop > this.sheetBody.scrollTop) {
+ rowIndex--;
+ max = rowIndex;
+ } else {
+ min = rowIndex;
+ }
+ }
+
+ return rowIndex;
+ }
+
// -------------------------------------------------------------------------------------- //
/*
@@ -351,52 +388,48 @@ export class Sheet extends HTMLElement {
this.lastCheckMillis = Date.now();
const next = this.nextLazyLoad();
// console.info("next %o", next); // @DEV_ONLY
- if (next) {
+ if (next !== null) {
this.lazyActive = true;
const rootNode = this.getRootNode() as ShadowRoot | Document;
const input = rootNode.getElementById(this.id + ":pageActionlazy") as HTMLInputElement;
- input.value = String(next);
+ input.value = String(1 + next); //input.value rowIndex starts at 1 (not 0)
+ this.lazyUpdatePreviousBodyScrollHeight = this.sheetBody.scrollHeight;
this.reloadWithAction(input);
}
}
nextLazyLoad(): number {
- // find first tr in current visible area
const rows = this.rows;
const rowElements = this.tableBody.rows;
- let min = 0;
- let max = rowElements.length;
- // binary search
- let i: number;
- while (min < max) {
- i = Math.floor((max - min) / 2) + min;
- // console.log("min i max -> %d %d %d", min, i, max); // @DEV_ONLY
- if (this.isRowAboveVisibleArea(rowElements[i])) {
- min = i + 1;
- } else {
- max = i;
+ const firstVisibleRowIndex = this.firstVisibleRowIndex;
+
+ for (let rowIndex = firstVisibleRowIndex + 1; rowIndex < firstVisibleRowIndex + rows; rowIndex++) {
+ if (rowIndex >= 0 && rowIndex < rowElements.length && rowElements[rowIndex].hasAttribute("dummy")) {
+ this.lazyUpdateUpperRows = false;
+ if (rowElements[firstVisibleRowIndex].hasAttribute("dummy")) {
+ return firstVisibleRowIndex;
+ } else if (rowIndex + rows - 1 < rowElements.length) {
+ return rowIndex;
+ } else {
+ return rowElements.length - rows;
+ }
}
}
- for (i = min; i < min + rows && i < rowElements.length; i++) {
- if (this.isRowDummy(rowElements[i])) {
- return i + 1;
+
+ for (let rowIndex = firstVisibleRowIndex; rowIndex > firstVisibleRowIndex - rows; rowIndex--) {
+ if (rowIndex >= 0 && rowIndex < rowElements.length && rowElements[rowIndex].hasAttribute("dummy")) {
+ this.lazyUpdateUpperRows = true;
+ if (rowIndex - rows + 1 >= 0) {
+ return rowIndex - rows + 1;
+ } else {
+ return 0;
+ }
}
}
return null;
}
- isRowAboveVisibleArea(tr: HTMLTableRowElement): boolean {
- const sheetBody = this.sheetBody;
- const viewStart = sheetBody.scrollTop;
- const trEnd = tr.offsetTop + tr.clientHeight;
- return trEnd < viewStart;
- }
-
- isRowDummy(tr: HTMLTableRowElement): boolean {
- return tr.hasAttribute("dummy");
- }
-
lazyResponse(event: EventData): void {
let updates: NodeListOf<Element>;
if (event.status === "complete") {
@@ -404,7 +437,7 @@ export class Sheet extends HTMLElement {
for (let i = 0; i < updates.length; i++) {
const update = updates[i];
const id = update.getAttribute("id");
- if (id.indexOf(":") > -1) { // is a JSF element id, but not a technical id from the framework
+ if (this.id === id) { // 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
const sheet = document.getElementById(id);
@@ -420,7 +453,7 @@ export class Sheet extends HTMLElement {
for (let i = 0; i < updates.length; i++) {
const update = updates[i];
const id = update.getAttribute("id");
- if (id.indexOf(":") > -1) { // is a JSF element id, but not a technical id from the framework
+ if (this.id === `${id}::lazy-temporary`) { // 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
@@ -443,6 +476,14 @@ export class Sheet extends HTMLElement {
this.lazyActive = false;
}
}
+
+ const scrollTop: number = JSON.parse(this.getHiddenScrollPosition().value)[1];
+ if (this.lazyUpdateUpperRows) {
+ const additionalHeight: number = this.sheetBody.scrollHeight - this.lazyUpdatePreviousBodyScrollHeight;
+ this.sheetBody.scrollTop = scrollTop + additionalHeight;
+ } else {
+ this.sheetBody.scrollTop = scrollTop;
+ }
}
}