import { createElement } from "../../lib/skeleton/index.js"; import rxjs, { effect } from "../../lib/rx.js"; import { qs, qsa, safe } from "../../lib/dom.js"; import ajax from "../../lib/ajax.js"; import { loadCSS } from "../../helpers/loader.js"; import t from "../../locales/index.js"; import { createLoader } from "../../components/loader.js"; import { get as getPlugin } from "../../model/plugin.js"; import ctrlDownloader, { init as initDownloader } from "./application_downloader.js"; import { renderMenubar, buttonDownload } from "./component_menubar.js"; import { transition } from "./common.js"; const MAX_ROWS = 200; class ITable { contructor() {} getHeader() { throw new Error("NOT_IMPLEMENTED"); } getBody() { throw new Error("NOT_IMPLEMENTED"); } } export default async function(render, { mime, getDownloadUrl, getFilename, hasMenubar = true, acl$ = rxjs.EMPTY }) { const $page = createElement(`
`); render($page); const $menubar = renderMenubar( qs($page, "component-menubar"), buttonDownload(getFilename(), getDownloadUrl()), ); const $dom = { thead: qs($page, ".thead"), tbody: qs($page, ".tbody"), }; const removeLoader = createLoader(qs($page, ".component_table_container")); const padding = 10; const STATE = { header: {}, body: [], rows: [], }; // feature: initial render const init$ = ajax({ url: getDownloadUrl(), responseType: "arraybuffer" }).pipe( rxjs.mergeMap(async({ response }) => { const loader = getPlugin(mime); if (!loader) throw new TypeError(`unsupported mimetype "${mime}"`); const [, url] = loader; const module = await import(url); let table = new (await module.default(ITable, { $menubar }))(response); if (typeof table.then === "function") table = await table; STATE.header = table.getHeader(); STATE.body = table.getBody(); STATE.rows = STATE.body; }), removeLoader, rxjs.tap(() => { buildHead(STATE, $dom, padding); buildRows(STATE.rows.slice(0, MAX_ROWS), STATE.header, $dom.tbody, padding, true, false); }), rxjs.catchError((err) => rxjs.from(initDownloader()).pipe( rxjs.tap(() => ctrlDownloader(render, { acl$, getFilename, getDownloadUrl })), rxjs.tap(() => console.log("cannot open file", err)), rxjs.mergeMap(() => rxjs.EMPTY), )), rxjs.share(), ); effect(init$); // feature: search const $search = createElement(``); effect(init$.pipe( rxjs.tap(() => $menubar.add($search)), rxjs.mergeMap(() => rxjs.fromEvent($search, "keyup").pipe(rxjs.debounce((e) => { if (!e.target.value) return rxjs.of(null); return rxjs.timer(300); }))), rxjs.tap((e) => { const terms = e.target.value.toLowerCase().trim().split(" "); $dom.tbody.scrollTo(0, 0); if (terms === "") STATE.rows = STATE.body; else STATE.rows = STATE.body.filter((row) => { const line = Object.values(row).join("").toLowerCase(); for (let i=0; i $dom.thead.scrollTo($dom.tbody.scrollLeft, 0)) )); effect(rxjs.fromEvent($dom.thead, "scroll").pipe( rxjs.tap(() => $dom.tbody.scrollTo($dom.thead.scrollLeft, $dom.tbody.scrollTop)) )); // feature: infinite scroll effect(rxjs.fromEvent($dom.tbody, "scroll").pipe( rxjs.mergeMap(async(e) => { const scrollBottom = e.target.scrollHeight - (e.target.scrollTop + e.target.clientHeight); if (scrollBottom > 0) return; else if (STATE.rows.length <= MAX_ROWS) return; else if (STATE.rows.length <= $dom.tbody.children.length) return; const current = $dom.tbody.children.length; const newRows = STATE.rows.slice(current, current + 10); buildRows(newRows, STATE.header, $dom.tbody, padding, false, false); }), )); // feature: make the last column to always fit the viewport effect(rxjs.merge( init$, init$.pipe( rxjs.mergeMap(() => rxjs.fromEvent(window, "resize")), rxjs.debounce((e) => e["debounce"] === false ? rxjs.of(null) : rxjs.timer(100)), ), ).pipe( rxjs.tap(() => resizeLastColumnIfNeeded({ $target: $dom.thead, $childs: qs($dom.thead, ".tr"), padding, })), rxjs.tap(() => qsa($dom.tbody, ".tr").forEach(($tr) => resizeLastColumnIfNeeded({ $target: $dom.tbody, $childs: $tr, padding, }))), )); } export function init($root) { const priors = ($root && [ $root.classList.add("component_page_viewerpage"), loadCSS(import.meta.url, "./component_menubar.css"), loadCSS(import.meta.url, "../ctrl_viewerpage.css"), ]); return Promise.all([ loadCSS(import.meta.url, "./application_table.css"), ...priors, ]); } async function buildRows(rows, legends, $tbody, padding, isInit, withClear) { if (withClear) $tbody.innerHTML = ""; for (let i=0; i`); legends.forEach(({ name, size }, i) => { const $col = createElement(`
`); $col.setAttribute("data-column", name); $col.setAttribute("title", obj[name]); if (obj[name]) $col.textContent = obj[name]; else $col.appendChild(createElement(`-`)); $tr.appendChild($col); }); $tbody.appendChild($tr); } $tbody.style.opacity = "0"; if (rows.length === 0) $tbody.appendChild(createElement(`

${t("Empty")}

`)); if (!isInit) { const e = new Event("resize"); e["debounce"] = false; window.dispatchEvent(e); await new Promise(requestAnimationFrame); } $tbody.style.opacity = "1"; if (isInit) transition($tbody.parentElement); } function buildHead(STATE, $dom, padding) { const $tr = createElement(`
`); STATE.header.forEach(({ name, size }, i) => { const $th = createElement(`
`); $th.setAttribute("title", name); $th.textContent = name; $th.appendChild(createElement(``)); let ascending = null; qs($th, "img").onclick = (e) => { ascending = !ascending; STATE.rows = sortBy(STATE.rows, ascending, name); qsa(e.target.closest(".tr"), "img").forEach(($img) => { $img.style.transform = "rotate(0deg)"; }); if (ascending) e.target.style.transform = "rotate(180deg)"; $dom.tbody.scrollTo($dom.tbody.scrollLeft, 0); buildRows(STATE.rows.slice(0, MAX_ROWS), STATE.header, $dom.tbody, padding, false, true); }; $tr.appendChild($th); }); $dom.thead.appendChild($tr); } function styleCell(l, name, padding) { const maxSize = 40; const charSize = 7; let sizeInChar = Math.min(l, maxSize); if (name.length >= sizeInChar) sizeInChar = Math.min(name.length + 1, maxSize); return `width: ${sizeInChar*charSize+padding*2}px;`; } function withCenter(className, fieldLength, isLast) { if (fieldLength > 4 || isLast) return className; return `${className} center`; } function resizeLastColumnIfNeeded({ $target, $childs, padding = 0 }) { const fullWidth = $target.clientWidth; let currWidth = 0; $childs.childNodes.forEach(($node) => currWidth += $node.clientWidth); if (currWidth < fullWidth && $childs.lastChild !== null) { const lastWidth = ($childs.lastChild.clientWidth - padding * 2) + fullWidth - currWidth; $childs.lastChild.setAttribute("style", `width: ${lastWidth}px`); } } function sortBy(rows, ascending, key) { const o = ascending ? 1 : -1; return rows.sort((a, b) => { let diff = a[key] - b[key]; if (isNaN(diff)) { if (a[key] === b[key]) diff = 0; else if (a[key] < b[key]) diff = -1; else diff = 1; } return o*diff; }); }