import { createElement } from "../../lib/skeleton/index.js"; import { animate, slideYIn } from "../../lib/animate.js"; import rxjs, { effect } from "../../lib/rx.js"; import { loadCSS } from "../../helpers/loader.js"; import { qs } from "../../lib/dom.js"; import { ApplicationError } from "../../lib/error.js"; import { createLoader } from "../../components/loader.js"; import ctrlError from "../ctrl_error.js"; import { createThing } from "./thing.js"; import { handleError, getFiles } from "./ctrl_filesystem_state.js"; import { ls } from "./model_files.js"; export default async function(render) { const $page = createElement(`

`); render($page); // feature: virtual scrolling const removeLoader = createLoader($page); const path = location.pathname.replace(new RegExp("^/files"), ""); effect(rxjs.of(path).pipe( ls(), removeLoader, rxjs.mergeMap(({ files, path }) => { // STEP1: setup the list of files const FILE_HEIGHT = 160; const BLOCK_SIZE = Math.ceil(document.body.clientHeight / FILE_HEIGHT) + 1; // const BLOCK_SIZE = 6; const COLUMN_PER_ROW = 4; const VIRTUAL_SCROLL_MINIMUM_TRIGGER = 50; let size = files.length; if (size > VIRTUAL_SCROLL_MINIMUM_TRIGGER) { size = Math.min(files.length, BLOCK_SIZE * COLUMN_PER_ROW); } const $list = qs($page, ".list"); const $fs = document.createDocumentFragment(); for (let i = 0; i < size; i++) { const file = files[i]; $fs.appendChild(createThing({ name: file.name, type: file.type, link: createLink(file, path), })); } 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 $listAfter = qs($page, ".ifscroll-after"); const height = (Math.ceil(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) => { if (size < 0 || size > height) throw new ApplicationError( "INTERNAL ERROR", `assertion on size failed: size[${size}] height[${height}]` ); $listBefore.style.height = `${size}px`; $listAfter.style.height = `${height - size}px`; }; setHeight(0); const top = ($node) => $node.getBoundingClientRect().top; return rxjs.of({ files, path, currentState: 0, $list, setHeight, FILE_HEIGHT, BLOCK_SIZE, COLUMN_PER_ROW, MARGIN: 35, // TODO: top($list) - top($list.closest(".scroll-y")); }); }), rxjs.mergeMap(({ files, path, BLOCK_SIZE, COLUMN_PER_ROW, FILE_HEIGHT, MARGIN, currentState, height, setHeight, $list, }) => rxjs.fromEvent($page.closest(".scroll-y"), "scroll", { passive: true }).pipe( rxjs.map((e) => { // 0-------------0-----------1-----------2-----------3 .... // [padding] $block1 $block2 $block3 .... const nextState = Math.floor((e.target.scrollTop - MARGIN) / FILE_HEIGHT); return Math.max(nextState, 0); }), rxjs.distinctUntilChanged(), rxjs.debounce(() => new rxjs.Observable((observer) => { const id = requestAnimationFrame(() => observer.next()); return () => cancelAnimationFrame(id); })), rxjs.tap((nextState) => { // STEP1: calculate the virtual scroll paramameters let diff = nextState - currentState; const diffSgn = Math.sign(diff); 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; if (diffSgn > 0) { // => scroll down fileStart += BLOCK_SIZE * COLUMN_PER_ROW; fileStart -= Math.min(diff, BLOCK_SIZE) * 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 return; } else if (fileEnd > files.length) { // occur when files.length isn't a multiple of COLUMN_PER_ROW and // we've scrolled to the bottom of the list already nextState = Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE; fileEnd = files.length - 1; for (let i=0; i 0) { // scroll down $list.appendChild($fs); for (let i = 0; i < n; i++) $list.firstChild.remove(); } else { // scroll up $list.insertBefore($fs, $list.firstChild); for (let i = 0; i < n; i++) $list.lastChild.remove(); } setHeight(nextState * FILE_HEIGHT); currentState = nextState; }), )), rxjs.catchError(ctrlError()), )); } export function init() { return Promise.all([ loadCSS(import.meta.url, "./ctrl_filesystem.css"), loadCSS(import.meta.url, "./thing.css"), ]); } function createLink(file, path) { if (file.type === "file") { return "/view" + path + file.name; } return "/files" + path + file.name + "/"; }