UNPKG

@discoveryjs/discovery

Version:

Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards

114 lines (113 loc) 3.97 kB
import { Dictionary } from "./dict.js"; import { Observer } from "./observer.js"; import { createElement } from "./utils/dom.js"; import { isRawViewConfig } from "./view.js"; const CONFIG = Symbol("config"); const BUILDIN_NOT_FOUND = { name: "not-found", render: (el, { name }) => { el.style.cssText = "color:#a00"; el.innerText = `Page \`${name}\` not found`; } }; export class PageRenderer extends Dictionary { #host; #view; #lastRenderPageEl; lastPage; lastPageRef; pageOverscrolled; setPageOverscroll; constructor(host, view) { super(); this.#host = host; this.#view = view; this.#lastRenderPageEl = null; this.lastPage = null; this.lastPageRef = null; this.pageOverscrolled = new Observer(false); this.setPageOverscroll = () => { }; if (typeof IntersectionObserver === "function") { const pageOverscrollTriggerEl = createElement("div", { style: "position:absolute" }); const root = host.dom?.content || null; let unsubscribe = () => { }; if (root !== null) { const overscrollObserver = new IntersectionObserver( (entries) => this.pageOverscrolled.set(!entries[entries.length - 1].isIntersecting), { root } ); this.setPageOverscroll = (newPageEl) => { overscrollObserver.unobserve(pageOverscrollTriggerEl); unsubscribe(); if (newPageEl) { newPageEl.prepend(pageOverscrollTriggerEl); overscrollObserver.observe(pageOverscrollTriggerEl); unsubscribe = this.pageOverscrolled.subscribeSync((overscrolled) => { newPageEl.classList.toggle("page_overscrolled", overscrolled); }); } }; } } } define(name, _render, _options) { const options = isRawViewConfig(_render) || typeof _render === "function" ? { ..._options, render: _render } : _render; const { render, ...optionsWithoutRender } = options; if (render === void 0) { throw new Error(`Page "${name}" requires a specified render option`); } return PageRenderer.define(this, name, Object.freeze({ name, render: typeof render === "function" ? render.bind(this.#view) : (el, data, context) => this.#view.render(el, render, data, context), options: Object.freeze(optionsWithoutRender), [CONFIG]: render })); } render(prevPageEl, name, data, context) { let page = this.get(name); if (!page) { page = this.get("not-found") || BUILDIN_NOT_FOUND; data = { name }; } const { reuseEl, init, keepScrollOffset = true } = page.options || {}; const pageRef = this.#host.pageRef; const pageChanged = this.lastPage !== name; const pageRefChanged = this.lastPageRef !== pageRef; const newPageEl = reuseEl && !pageChanged ? prevPageEl : createElement("article", `page page-${CSS.escape(name)}`); this.#lastRenderPageEl = newPageEl; this.lastPage = name; this.lastPageRef = pageRef; if (pageChanged && typeof init === "function") { init(newPageEl); } const renderState = new Promise(async (resolve, reject) => { try { await page.render(newPageEl, data, context); if (this.#lastRenderPageEl !== newPageEl) { reject(new Error("Aborted by new page render")); return; } if (newPageEl !== prevPageEl) { prevPageEl.replaceWith(newPageEl); this.setPageOverscroll(newPageEl); } const parentEl = newPageEl.parentNode; if (parentEl !== null && (pageChanged || pageRefChanged || !keepScrollOffset)) { parentEl.scrollTop = 0; } resolve(); } catch (e) { newPageEl.replaceChildren(); this.#view.render(newPageEl, "alert-danger", String(e) + " (see details in console)"); reject(e); } }); return { pageEl: newPageEl, config: page[CONFIG], renderState }; } }