307 lines · 13.3 KB · JavaScript
RawDownload
//  listView.js////  Created by David Back on 27 Aug 2018//  Copyright 2018 High Fidelity, Inc.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html const SCROLL_ROWS = 2; // number of rows used as scrolling buffer, each time we pass this number of rows we scrollconst FIRST_ROW_INDEX = 2; // the first elRow element's index in the child nodes of the table body function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunction, updateRowFunction, clearRowFunction,                  preRefreshFunction, postRefreshFunction, preResizeFunction, WINDOW_NONVARIABLE_HEIGHT) {    this.elTableBody = elTableBody;    this.elTableScroll = elTableScroll;    this.elTableHeaderRow = elTableHeaderRow;        this.elTopBuffer = null;    this.elBottomBuffer = null;        this.createRowFunction = createRowFunction;    this.updateRowFunction = updateRowFunction;    this.clearRowFunction = clearRowFunction;    this.preRefreshFunction = preRefreshFunction;    this.postRefreshFunction = postRefreshFunction;    this.preResizeFunction = preResizeFunction;        // the list of row elements created in the table up to max viewable height plus SCROLL_ROWS rows for scrolling buffer    this.elRows = [];    // the list of all row item data to show in the scrolling table, passed to updateRowFunction to set to each row    this.itemData = [];    // the current index within the itemData list that is set to the top most elRow element    this.rowOffset = 0;    // height of the elRow elements    this.rowHeight = 0;    // the previous elTableScroll.scrollTop value when the elRows were last shifted for scrolling    this.lastRowShiftScrollTop = 0;        this.initialize();}    ListView.prototype = {    getNumRows: function() {        return this.elRows.length;    },        getScrollHeight: function() {        return this.rowHeight * SCROLL_ROWS;    },        getFirstVisibleRowIndex: function() {        return this.rowOffset;    },        getLastVisibleRowIndex: function() {        return this.getFirstVisibleRowIndex() + entityList.getNumRows() - 1;    },        resetToTop: function() {        this.rowOffset = 0;        this.lastRowShiftScrollTop = 0;        this.refreshBuffers();        this.elTableScroll.scrollTop = 0;    },     clear: function() {        for (let i = 0; i < this.getNumRows(); i++) {            let elRow = this.elTableBody.childNodes[i + FIRST_ROW_INDEX];            this.clearRowFunction(elRow);            elRow.style.display = "none"; // hide cleared rows        }    },     scroll: function() {        let scrollTop = this.elTableScroll.scrollTop;               let scrollHeight = this.getScrollHeight();        let nextRowChangeScrollTop = this.lastRowShiftScrollTop + scrollHeight;        let totalItems = this.itemData.length;        let numRows = this.getNumRows();         // if the top of the scroll area has past the amount of scroll row space since the last point of scrolling and there        // are still more rows to scroll to then trigger a scroll down by the min of the scroll row space or number of        // remaining rows below        // if the top of the scroll area has gone back above the last point of scrolling then trigger a scroll up by min of        // the scroll row space or number of rows above        if (scrollTop >= nextRowChangeScrollTop && numRows + this.rowOffset < totalItems) {            let numScrolls = Math.ceil((scrollTop - nextRowChangeScrollTop) / scrollHeight);            let numScrollRows = numScrolls * SCROLL_ROWS;            if (numScrollRows + this.rowOffset + numRows > totalItems) {                numScrollRows = totalItems - this.rowOffset - numRows;            }            this.scrollRows(numScrollRows);        } else if (scrollTop < this.lastRowShiftScrollTop) {            let numScrolls = Math.ceil((this.lastRowShiftScrollTop - scrollTop) / scrollHeight);            let numScrollRows = numScrolls * SCROLL_ROWS;            if (this.rowOffset - numScrollRows < 0) {                numScrollRows = this.rowOffset;            }            this.scrollRows(-numScrollRows);        }    },        scrollRows: function(numScrollRows) {        let numScrollRowsAbsolute = Math.abs(numScrollRows);        if (numScrollRowsAbsolute === 0) {            return;        }                let scrollDown = numScrollRows > 0;                 let prevTopHeight = parseInt(this.elTopBuffer.getAttribute("height"));        let prevBottomHeight =  parseInt(this.elBottomBuffer.getAttribute("height"));                // if the number of rows to scroll at once is greater than the total visible number of row elements,        // then just advance the rowOffset accordingly and allow the refresh below to update all rows        if (numScrollRowsAbsolute > this.getNumRows()) {            this.rowOffset += numScrollRows;        } else {            // for each row to scroll down, move the top row element to the bottom of the            // table before the bottom buffer and reset it's row data to the new item            // for each row to scroll up, move the bottom row element to the top of            // the table before the top row and reset it's row data to the new item            for (let i = 0; i < numScrollRowsAbsolute; i++) {                let topRow = this.elTableBody.childNodes[FIRST_ROW_INDEX];                let rowToMove = scrollDown ? topRow : this.elTableBody.childNodes[FIRST_ROW_INDEX + this.getNumRows() - 1];                let rowIndex = scrollDown ? this.getNumRows() + this.rowOffset : this.rowOffset - 1;                let moveRowBefore = scrollDown ? this.elBottomBuffer : topRow;                this.elTableBody.removeChild(rowToMove);                this.elTableBody.insertBefore(rowToMove, moveRowBefore);                this.updateRowFunction(rowToMove, this.itemData[rowIndex]);                this.rowOffset += scrollDown ? 1 : -1;            }        }                // add/remove the row space that was scrolled away to the top buffer height and last scroll point        // add/remove the row space that was scrolled away to the bottom buffer height              let scrolledSpace = this.rowHeight * numScrollRows;        let newTopHeight = prevTopHeight + scrolledSpace;        let newBottomHeight = prevBottomHeight - scrolledSpace;        this.elTopBuffer.setAttribute("height", newTopHeight);        this.elBottomBuffer.setAttribute("height", newBottomHeight);        this.lastRowShiftScrollTop += scrolledSpace;                // if scrolling more than the total number of visible rows at once then refresh all row data        if (numScrollRowsAbsolute > this.getNumRows()) {            this.refresh();        }    },     /**     * Scrolls firstRowIndex with least effort, also tries to make the window include the other selections in case lastRowIndex is set.     * In the case that firstRowIndex and lastRowIndex are already within the visible bounds then nothing will happen.     * @param {number} firstRowIndex - The row that will be scrolled to.     * @param {number} lastRowIndex - The last index of the bound.     */    scrollToRow: function (firstRowIndex, lastRowIndex) {        lastRowIndex = lastRowIndex ? lastRowIndex : firstRowIndex;        let boundingTop = firstRowIndex * this.rowHeight;        let boundingBottom = (lastRowIndex * this.rowHeight) + this.rowHeight;        if ((boundingBottom - boundingTop) > this.elTableScroll.clientHeight) {            boundingBottom = boundingTop + this.elTableScroll.clientHeight;        }         let currentVisibleAreaTop = this.elTableScroll.scrollTop;        let currentVisibleAreaBottom = currentVisibleAreaTop + this.elTableScroll.clientHeight;         if (boundingTop < currentVisibleAreaTop) {            this.elTableScroll.scrollTop = boundingTop;        } else if (boundingBottom > currentVisibleAreaBottom) {            this.elTableScroll.scrollTop = boundingBottom - (this.elTableScroll.clientHeight);        }    },        refresh: function() {        this.preRefreshFunction();        // block refreshing before rows are initialized        let numRows = this.getNumRows();        if (numRows === 0) {            return;        }                let prevScrollTop = this.elTableScroll.scrollTop;                // start with all row data cleared and initially set to invisible        this.clear();                // if we are at the bottom of the list adjust row offset to make sure all rows stay in view        this.refreshRowOffset();                // update all row data and set rows visible until max visible items reached        for (let i = 0; i < numRows; i++) {            let rowIndex = i + this.rowOffset;            if (rowIndex >= this.itemData.length) {                break;            }            let rowElementIndex = i + FIRST_ROW_INDEX;            let elRow = this.elTableBody.childNodes[rowElementIndex];            let itemData = this.itemData[rowIndex];            this.updateRowFunction(elRow, itemData);            elRow.style.display = ""; // make sure the row is visible        }                // update the top and bottom buffer heights to adjust for above changes        this.refreshBuffers();         // adjust the last row shift scroll point based on how much the current scroll point changed        let scrollTopDifference =  this.elTableScroll.scrollTop - prevScrollTop;        if (scrollTopDifference !== 0) {            this.lastRowShiftScrollTop += scrollTopDifference;            if (this.lastRowShiftScrollTop < 0) {                this.lastRowShiftScrollTop = 0;            }        }        this.postRefreshFunction();    },        refreshBuffers: function() {        // top buffer height is the number of hidden rows above the top row        let topHiddenRows = this.rowOffset;        let topBufferHeight = this.rowHeight * topHiddenRows;        this.elTopBuffer.setAttribute("height", topBufferHeight);                // bottom buffer height is the number of hidden rows below the bottom row (last scroll buffer row)        let bottomHiddenRows = this.itemData.length - this.getNumRows() - this.rowOffset;        let bottomBufferHeight = this.rowHeight * bottomHiddenRows;        if (bottomHiddenRows < 0) {            bottomBufferHeight = 0;        }        this.elBottomBuffer.setAttribute("height", bottomBufferHeight);    },        refreshRowOffset: function() {        // make sure the row offset isn't causing visible rows to pass the end of the item list and is clamped to 0        let numRows = this.getNumRows();        if (this.rowOffset + numRows > this.itemData.length) {            this.rowOffset = this.itemData.length - numRows;        }        if (this.rowOffset < 0) {            this.rowOffset = 0;        }    },     resize: function() {                if (!this.elTableBody || !this.elTableScroll) {            console.log("ListView.resize - no valid table body or table scroll element");            return;        }        this.preResizeFunction();        let prevScrollTop = this.elTableScroll.scrollTop;           // take up available window space        this.elTableScroll.style.height = window.innerHeight - WINDOW_NONVARIABLE_HEIGHT;        let viewableHeight = parseInt(this.elTableScroll.style.height) + 1;         // remove all existing row elements and clear row list        for (let i = 0; i < this.getNumRows(); i++) {            let elRow = this.elRows[i];            this.elTableBody.removeChild(elRow);        }        this.elRows = [];                // create new row elements inserted between the top and bottom buffers up until the max viewable scroll area        let usedHeight = 0;        while (usedHeight < viewableHeight) {            let newRow = this.createRowFunction();            this.elTableBody.insertBefore(newRow, this.elBottomBuffer);            this.rowHeight = newRow.offsetHeight;            usedHeight += this.rowHeight;                 this.elRows.push(newRow);        }                // add SCROLL_ROWS extras rows for scrolling buffer purposes        for (let i = 0; i < SCROLL_ROWS; i++) {            let scrollRow = this.createRowFunction();            this.elTableBody.insertBefore(scrollRow, this.elBottomBuffer);            this.elRows.push(scrollRow);        }                // restore the scroll point to the same scroll point from before above changes        this.elTableScroll.scrollTop = prevScrollTop;                this.refresh();    },        initialize: function() {        if (!this.elTableBody || !this.elTableScroll) {            console.log("ListView.initialize - no valid table body or table scroll element");            return;        }                this.elTopBuffer = document.createElement("tr");        this.elTableBody.appendChild(this.elTopBuffer);        this.elTopBuffer.setAttribute("height", 0);                this.elBottomBuffer = document.createElement("tr");        this.elTableBody.appendChild(this.elBottomBuffer);        this.elBottomBuffer.setAttribute("height", 0);         this.onScroll = this.scroll.bind(this);        this.elTableScroll.addEventListener("scroll", this.onScroll);        this.onResize = this.resize.bind(this);        window.addEventListener("resize", this.onResize);         // initialize all row elements        this.resize();    }};