mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-11-04 05:27:04 +08:00 
			
		
		
		
	fix (scroll): virtual scroll debugging
This commit is contained in:
		@ -7,7 +7,7 @@ import { ApplicationError } from "../../lib/error.js";
 | 
				
			|||||||
import { toggle as toggleLoader } from "../../components/loader.js";
 | 
					import { toggle as toggleLoader } from "../../components/loader.js";
 | 
				
			||||||
import ctrlError from "../ctrl_error.js";
 | 
					import ctrlError from "../ctrl_error.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { createThing, allocateMemory, css } from "./thing.js";
 | 
					import { createThing, css } from "./thing.js";
 | 
				
			||||||
import { handleError, getFiles } from "./ctrl_filesystem_state.js";
 | 
					import { handleError, getFiles } from "./ctrl_filesystem_state.js";
 | 
				
			||||||
import { ls } from "./model_files.js";
 | 
					import { ls } from "./model_files.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -37,12 +37,17 @@ export default async function(render) {
 | 
				
			|||||||
            files: new Array(400).fill(1),
 | 
					            files: new Array(400).fill(1),
 | 
				
			||||||
        }), 1000))),
 | 
					        }), 1000))),
 | 
				
			||||||
        toggleLoader($page, false),
 | 
					        toggleLoader($page, false),
 | 
				
			||||||
        rxjs.mergeMap(({ files }) => {
 | 
					        rxjs.mergeMap(({ files }) => { // STEP1: setup the list of files
 | 
				
			||||||
            const BLOCK_SIZE = 8;
 | 
					 | 
				
			||||||
            const COLUMN_PER_ROW = 2;
 | 
					 | 
				
			||||||
            const FILE_HEIGHT = 160;
 | 
					            const FILE_HEIGHT = 160;
 | 
				
			||||||
            const size = Math.min(files.length, BLOCK_SIZE * COLUMN_PER_ROW);
 | 
					            const BLOCK_SIZE = Math.ceil(document.body.clientHeight / FILE_HEIGHT) + 1;
 | 
				
			||||||
            allocateMemory(BLOCK_SIZE * COLUMN_PER_ROW);
 | 
					            // const BLOCK_SIZE = 7;
 | 
				
			||||||
 | 
					            const COLUMN_PER_ROW = 2;
 | 
				
			||||||
 | 
					            const VIRTUAL_SCROLL_MINIMUM_TRIGGER = 10;
 | 
				
			||||||
 | 
					            let size = files.length;
 | 
				
			||||||
 | 
					            if (size > VIRTUAL_SCROLL_MINIMUM_TRIGGER) {
 | 
				
			||||||
 | 
					                size = BLOCK_SIZE * COLUMN_PER_ROW;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const $list = qs($page, ".list");
 | 
				
			||||||
            const $fs = document.createDocumentFragment();
 | 
					            const $fs = document.createDocumentFragment();
 | 
				
			||||||
            for (let i = 0; i < size; i++) {
 | 
					            for (let i = 0; i < size; i++) {
 | 
				
			||||||
                $fs.appendChild(createThing({
 | 
					                $fs.appendChild(createThing({
 | 
				
			||||||
@ -51,43 +56,56 @@ export default async function(render) {
 | 
				
			|||||||
                    link: "/view/test.txt",
 | 
					                    link: "/view/test.txt",
 | 
				
			||||||
                }));
 | 
					                }));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            const $list = qs($page, ".list");
 | 
					            animate($list, { time: 200, keyframes: slideYIn(5) });
 | 
				
			||||||
 | 
					            $list.appendChild($fs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /////////////////////////////////////////
 | 
				
			||||||
 | 
					            // CASE 1: virtual scroll isn't enabled
 | 
				
			||||||
 | 
					            if (files.length <= VIRTUAL_SCROLL_MINIMUM_TRIGGER) {
 | 
				
			||||||
 | 
					                return rxjs.EMPTY;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            /////////////////////////////////////////
 | 
				
			||||||
 | 
					            // CASE 2: with virtual scroll
 | 
				
			||||||
            const $listBefore = qs($page, ".ifscroll-before");
 | 
					            const $listBefore = qs($page, ".ifscroll-before");
 | 
				
			||||||
            const $listAfter = qs($page, ".ifscroll-after");
 | 
					            const $listAfter = qs($page, ".ifscroll-after");
 | 
				
			||||||
 | 
					            const height = (Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE) * FILE_HEIGHT;
 | 
				
			||||||
            const height = (Math.floor(files.length / COLUMN_PER_ROW) - BLOCK_SIZE) * FILE_HEIGHT;
 | 
					            if (height > 33554400) {
 | 
				
			||||||
 | 
					                console.log(`maximum CSS height reached, requested height ${height} is too large`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            const setHeight = (size) => {
 | 
					            const setHeight = (size) => {
 | 
				
			||||||
 | 
					                if (size < 0 || size > height) throw new ApplicationError(
 | 
				
			||||||
 | 
					                    "INTERNAL ERROR",
 | 
				
			||||||
 | 
					                    `assertion on size failed: size[${size}] height[${height}]`
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
                $listBefore.style.height = `${size}px`;
 | 
					                $listBefore.style.height = `${size}px`;
 | 
				
			||||||
                $listAfter.style.height = `${height - size}px`;
 | 
					                $listAfter.style.height = `${height - size}px`;
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
            setHeight(0);
 | 
					            setHeight(0);
 | 
				
			||||||
            animate($list, { time: 200, keyframes: slideYIn(5) });
 | 
					            const top = ($node) => $node.getBoundingClientRect().top;
 | 
				
			||||||
            $list.appendChild($fs);
 | 
					 | 
				
			||||||
            if (files.length === size) return rxjs.EMPTY;
 | 
					 | 
				
			||||||
            return rxjs.of({
 | 
					            return rxjs.of({
 | 
				
			||||||
                files,
 | 
					                files,
 | 
				
			||||||
                currentState: 0,
 | 
					                currentState: 0,
 | 
				
			||||||
                BLOCK_SIZE, COLUMN_PER_ROW, FILE_HEIGHT,
 | 
					 | 
				
			||||||
                setHeight,
 | 
					 | 
				
			||||||
                $list,
 | 
					                $list,
 | 
				
			||||||
 | 
					                setHeight,
 | 
				
			||||||
 | 
					                FILE_HEIGHT, BLOCK_SIZE, COLUMN_PER_ROW,
 | 
				
			||||||
 | 
					                MARGIN: 35, // TODO: top($list) - top($list.closest(".scroll-y"));
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        rxjs.mergeMap(({
 | 
					        rxjs.mergeMap(({
 | 
				
			||||||
            files,
 | 
					            files,
 | 
				
			||||||
            BLOCK_SIZE, COLUMN_PER_ROW, FILE_HEIGHT,
 | 
					            BLOCK_SIZE, COLUMN_PER_ROW, FILE_HEIGHT,
 | 
				
			||||||
 | 
					            MARGIN,
 | 
				
			||||||
            currentState,
 | 
					            currentState,
 | 
				
			||||||
            height, setHeight,
 | 
					            height, setHeight,
 | 
				
			||||||
            $list,
 | 
					            $list,
 | 
				
			||||||
        }) => rxjs.fromEvent(
 | 
					        }) => rxjs.fromEvent($page.closest(".scroll-y"), "scroll", { passive: true }).pipe(
 | 
				
			||||||
            $page.parentElement.parentElement.parentElement,
 | 
					            rxjs.map((e) => {
 | 
				
			||||||
            "scroll", { passive: true },
 | 
					                // 0-------------0-----------1-----------2-----------3 ....
 | 
				
			||||||
        ).pipe(
 | 
					                //    [padding]     $block1     $block2     $block3    ....
 | 
				
			||||||
            rxjs.map((e) => Math.min(
 | 
					                const nextState = Math.floor((e.target.scrollTop - MARGIN) / FILE_HEIGHT);
 | 
				
			||||||
                Math.ceil(Math.max(0, e.target.scrollTop) / FILE_HEIGHT),
 | 
					                return Math.max(nextState, 0);
 | 
				
			||||||
                // cap state value when BLOCK_SIZE is larger than minimum value. This is to
 | 
					            }),
 | 
				
			||||||
                // prevent issues when scrolling fast to the bottom (aka diff > 1)
 | 
					 | 
				
			||||||
                Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE,
 | 
					 | 
				
			||||||
            )),
 | 
					 | 
				
			||||||
            rxjs.distinctUntilChanged(),
 | 
					            rxjs.distinctUntilChanged(),
 | 
				
			||||||
            rxjs.debounce(() => new rxjs.Observable((observer) => {
 | 
					            rxjs.debounce(() => new rxjs.Observable((observer) => {
 | 
				
			||||||
                const id = requestAnimationFrame(() => observer.next());
 | 
					                const id = requestAnimationFrame(() => observer.next());
 | 
				
			||||||
@ -97,31 +115,32 @@ export default async function(render) {
 | 
				
			|||||||
                // STEP1: calculate the virtual scroll paramameters
 | 
					                // STEP1: calculate the virtual scroll paramameters
 | 
				
			||||||
                let diff = nextState - currentState;
 | 
					                let diff = nextState - currentState;
 | 
				
			||||||
                const diffSgn = Math.sign(diff);
 | 
					                const diffSgn = Math.sign(diff);
 | 
				
			||||||
                if (Math.abs(diff) > BLOCK_SIZE) diff = diffSgn * BLOCK_SIZE; // fast scroll
 | 
					                if (Math.abs(diff) > BLOCK_SIZE) { // diff is bound by BLOCK_SIZE
 | 
				
			||||||
 | 
					                    // we can't be moving more than what is on the screen
 | 
				
			||||||
 | 
					                    diff = diffSgn * BLOCK_SIZE;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                let fileStart = nextState * COLUMN_PER_ROW;
 | 
					                let fileStart = nextState * COLUMN_PER_ROW;
 | 
				
			||||||
                if (diffSgn > 0) { // => scroll down
 | 
					                if (diffSgn > 0) { // => scroll down
 | 
				
			||||||
                    // eg: files[15] BLOCK_SIZE=1 COLUMN_PER_ROW=1
 | 
					 | 
				
			||||||
                    // -----------[currentState:0]--------------------------[nextState:5]---------
 | 
					 | 
				
			||||||
                    // -----------[fileStart=5+1=6]
 | 
					 | 
				
			||||||
                    fileStart += BLOCK_SIZE * COLUMN_PER_ROW;
 | 
					                    fileStart += BLOCK_SIZE * COLUMN_PER_ROW;
 | 
				
			||||||
                    // -----------[fileStart=6-min(5,1)=5]
 | 
					 | 
				
			||||||
                    fileStart -= Math.min(diff, BLOCK_SIZE) * COLUMN_PER_ROW;
 | 
					                    fileStart -= Math.min(diff, BLOCK_SIZE) * COLUMN_PER_ROW;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                let fileEnd = fileStart + diffSgn * diff * COLUMN_PER_ROW;
 | 
					                let fileEnd = fileStart + diffSgn * diff * COLUMN_PER_ROW;
 | 
				
			||||||
 | 
					                if (fileStart >= files.length) { // occur when BLOCK_SIZE is larger than its absolute minimum
 | 
				
			||||||
                if (fileStart >= files.length) throw new ApplicationError(
 | 
					                    return;
 | 
				
			||||||
                    "INTERNAL_ERROR",
 | 
					                }
 | 
				
			||||||
                    `assert failed in virtual scroll range[${fileStart}:${fileEnd}] length[${files.length}]`,
 | 
					                else if (fileEnd > files.length) {
 | 
				
			||||||
                ); else if (fileEnd > files.length) {
 | 
					 | 
				
			||||||
                    // occur when files.length isn't a multiple of COLUMN_PER_ROW and
 | 
					                    // occur when files.length isn't a multiple of COLUMN_PER_ROW and
 | 
				
			||||||
                    // we've scrolled to the bottom of the list
 | 
					                    // we've scrolled to the bottom of the list already
 | 
				
			||||||
                    nextState = Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE - 1;
 | 
					                    nextState = Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE;
 | 
				
			||||||
                    fileEnd = files.length;
 | 
					                    fileEnd = files.length - 1;
 | 
				
			||||||
                    do {
 | 
					                    for (let i=0; i<COLUMN_PER_ROW; i++) {
 | 
				
			||||||
                        // add some padding to fileEnd to balance the list to the
 | 
					                        // add some padding to fileEnd to balance the list to the
 | 
				
			||||||
                        // nearest COLUMN_PER_ROW
 | 
					                        // nearest COLUMN_PER_ROW
 | 
				
			||||||
                        fileEnd += 1;
 | 
					                        fileEnd += 1;
 | 
				
			||||||
                    } while (fileEnd % COLUMN_PER_ROW !== 0);
 | 
					                        if (fileEnd % COLUMN_PER_ROW === 0) {
 | 
				
			||||||
 | 
					                            break
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // STEP2: create the new elements
 | 
					                // STEP2: create the new elements
 | 
				
			||||||
@ -139,8 +158,6 @@ export default async function(render) {
 | 
				
			|||||||
                    }));
 | 
					                    }));
 | 
				
			||||||
                    n += 1;
 | 
					                    n += 1;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                // console.log(`n[${n}] state[${currentState} -> ${nextState}] files[${fileStart}:${fileEnd}]`)
 | 
					 | 
				
			||||||
                if (n === 0) return;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // STEP3: update the DOM
 | 
					                // STEP3: update the DOM
 | 
				
			||||||
                if (diffSgn > 0) { // scroll down
 | 
					                if (diffSgn > 0) { // scroll down
 | 
				
			||||||
@ -156,12 +173,4 @@ export default async function(render) {
 | 
				
			|||||||
        )),
 | 
					        )),
 | 
				
			||||||
        rxjs.catchError(ctrlError()),
 | 
					        rxjs.catchError(ctrlError()),
 | 
				
			||||||
    ));
 | 
					    ));
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // feature2: fs in "search" mode
 | 
					 | 
				
			||||||
    // TODO
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function isInViewport(element) {
 | 
					 | 
				
			||||||
    const rect = element.getBoundingClientRect();
 | 
					 | 
				
			||||||
    return rect.bottom > 0;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -37,28 +37,17 @@ export function createThing({
 | 
				
			|||||||
    link = "",
 | 
					    link = "",
 | 
				
			||||||
    // permissions = {}
 | 
					    // permissions = {}
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
    const $thing = $get();
 | 
					    const $thing = $tmpl.cloneNode(true);
 | 
				
			||||||
    if ($thing instanceof HTMLElement) {
 | 
					    if (!$thing instanceof window.HTMLElement) throw new Error("assertion failed: $thing must be an HTMLELement");
 | 
				
			||||||
        const $label = $thing.querySelector(".component_filename .file-details > span");
 | 
					    const $label = $thing.querySelector(".component_filename .file-details > span");
 | 
				
			||||||
        if ($label instanceof HTMLElement) $label.textContent = name;
 | 
					    if (!$label instanceof window.HTMLElement) throw new Error("assertion failed: $label must be an HTMLELement");
 | 
				
			||||||
        $thing?.querySelector("a")?.setAttribute("href", link);
 | 
					
 | 
				
			||||||
 | 
					    $label.textContent = name;
 | 
				
			||||||
 | 
					    $thing.querySelector(".component_checkbox").onclick = (e) => {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					        console.log("CLICK");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    $thing.querySelector("a").setAttribute("href", link);
 | 
				
			||||||
    return $thing;
 | 
					    return $thing;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
function $get() {
 | 
					 | 
				
			||||||
    // the very first implementation was:
 | 
					 | 
				
			||||||
    return $tmpl.cloneNode(true);
 | 
					 | 
				
			||||||
    // the major issue was cloneNode is slow and would often make us miss an animationFrame. A much more
 | 
					 | 
				
			||||||
    // efficient approach is to use a ring buffer of node we reuse as we scroll around
 | 
					 | 
				
			||||||
    if (bufferIdx >= $tmplBuffer.length) bufferIdx = 0;
 | 
					 | 
				
			||||||
    const $node = $tmplBuffer[bufferIdx];
 | 
					 | 
				
			||||||
    bufferIdx += 1;
 | 
					 | 
				
			||||||
    // console.log($node);
 | 
					 | 
				
			||||||
    return $node;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
let $tmplBuffer = [];
 | 
					 | 
				
			||||||
let bufferIdx = 0;
 | 
					 | 
				
			||||||
export function allocateMemory(size) {
 | 
					 | 
				
			||||||
    $tmplBuffer = Array.apply(null, {length: size}).map(() => $tmpl.cloneNode(true))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user