@discoveryjs/discovery
Version:
Frontend framework for rapid data (JSON) analysis, shareable serverless reports and dashboards
114 lines (113 loc) • 3.97 kB
JavaScript
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
};
}
}