UNPKG

native-spa-route

Version:

provide SPA route's experience with native web component

651 lines (650 loc) 22.5 kB
import w from "debug"; import { css as M, LitElement as $, nothing as f, html as u } from "lit"; import { property as h, state as l, query as L, customElement as k, queryAll as P } from "lit/decorators.js"; import { unsafeHTML as y } from "lit/directives/unsafe-html.js"; import { cache as O } from "lit/directives/cache.js"; import { createRef as j, ref as N } from "lit/directives/ref.js"; import { ifDefined as U } from "lit/directives/if-defined.js"; function B() { document.body.addEventListener("click", (t) => { const e = t.composedPath()[0]; if (e.tagName === "A") { const s = e.getAttribute("href"); if (typeof s == "string") { if (s.startsWith("http")) return; t.preventDefault(), history.pushState({}, e.getAttribute("title") ?? "", s); } } }); } function E(t, e) { window.history[e] = new Proxy(window.history[e], { apply: (s, o, r) => { var v, C; const a = r[0], d = r[1]; let c = r[2]; typeof (t == null ? void 0 : t.url_adapter) == "function" && (c = t.url_adapter(c)), (v = t == null ? void 0 : t.before) == null || v.call(t, e, a, d, c); const A = s.apply(o, [a, d, c]); return (C = t == null ? void 0 : t.after) == null || C.call(t, e, a, d, c), window.dispatchEvent( new CustomEvent(`history:${e}`, { detail: { type: e, data: a, unused: d, url: c } }) ), A; } }); } function b(t, e) { window.history[e] = new Proxy(window.history[e], { apply: (s, o) => { var a, d; (a = t == null ? void 0 : t.before) == null || a.call(t, e, void 0, "", void 0); const r = s.apply(o, []); return (d = t == null ? void 0 : t.after) == null || d.call(t, e, void 0, "", void 0), window.dispatchEvent( new CustomEvent(`history:${e}`, { detail: { type: e } }) ), r; } }); } function I(t, e) { window.history[e] = new Proxy(window.history[e], { apply: (s, o, r) => { var d, _; (d = t == null ? void 0 : t.before) == null || d.call(t, e, r[0], "", void 0); const a = s.apply(o, r); return (_ = t == null ? void 0 : t.after) == null || _.call(t, e, r[0], "", void 0), window.dispatchEvent( new CustomEvent(`history:${e}`, { detail: { type: e, delta: r[0] } }) ), a; } }); } let S = !1; function q(t = {}) { S || (S = !0, E(t, "pushState"), E(t, "replaceState"), b(t, "back"), b(t, "forward"), I(t, "go")); } function R(t) { window.addEventListener("history:pushState", t), window.addEventListener("history:replaceState", t), window.addEventListener("history:back", t), window.addEventListener("history:forward", t), window.addEventListener("history:go", t), window.addEventListener("popstate", t, !1); } function T(t) { window.removeEventListener("history:pushState", t), window.removeEventListener("history:replaceState", t), window.removeEventListener("history:back", t), window.removeEventListener("history:forward", t), window.removeEventListener("history:go", t), window.removeEventListener("popstate", t, !1); } function it(t, e) { location.pathname === t && history.pushState(null, "", e), R(() => { location.pathname === t && history.pushState(null, "", e); }); } function z(t, e = !1) { var o; if (!t.getAttribute("src") && !t.textContent) return !0; let s; if (e) { let r = Array.from(document.styleSheets).findIndex((a) => a.ownerNode === t); if (r < 0) return !1; s = document.styleSheets[r]; } else s = t.sheet; return !!((o = s == null ? void 0 : s.cssRules) != null && o.length); } function H(t) { return new Promise((e) => { setTimeout(() => { e(); }, t); }); } async function V(t, e) { return new Promise(async (s) => { let o = 0, r = 50, a = 5 * 60 * 1e3 / r; for (; t.isActive(); ) { if (o > a) return console.warn("css check over max time"), s(!1); if (o += 1, z(e, !1)) return s(!0); await H(r), await t.updateComplete; } s(!1); }); } var F = Object.defineProperty, G = Object.getOwnPropertyDescriptor, n = (t, e, s, o) => { for (var r = o > 1 ? void 0 : o ? G(e, s) : e, a = t.length - 1, d; a >= 0; a--) (d = t[a]) && (r = (o ? d(e, s, r) : d(r)) || r); return o && r && F(e, s, r), r; }; const W = w("native-spa-route"), Y = w("native-spa-route:cache"), x = /^\:/; function g(t, e) { let s = /^\//.test(t); if (e.parentElement && e.parentElement.tagName !== "BODY") if (e.parentElement.tagName === "NATIVE-ROUTE") { const r = e.parentElement.getAttribute("path") ?? ""; let a = `${s ? "" : "/"}${t}`; return r === "" || r === "/" ? a : g(`${r}${a}`, e.parentElement); } else return g(t, e.parentElement); let o = t; return s || (o = `/${t}`), o !== "/" && (o = o.replace(/\/$/, "")), o; } let i = class extends $ { constructor() { super(...arguments), this.path = "", this.exact = !1, this.element = "", this.lazy = !1, this.appendDirection = "after", this.customRender = !1, this.renderAfterReady = !1, this.disableShadow = !1, this.shadowCSSUrl = "", this.cssUrl = "", this.cssParseMode = "inline", this.cssDelayRender = 32, this.drop = !1, this.cssOnly = !1, this.cacheVaildTime = 10 * 60 * 1e3, this.virtualNode = !1, this.groupMatchMode = !1, this._style_tag_ref = j(), this.active = !1, this.fullpath = "", this.moduleReady = "nothing", this.cssReady = "nothing", this.cssContent = "", this.isExactMatch = !1, this.isModuleRenderInstanceDestroied = !1, this._route_match_check_path = "", this._route_match_check_grouped_path = [], this.route_change_callback = () => { let t = this.fullpath, e = this._route_match_check_grouped_path; if (this.isExactMatch = window.location.pathname === t, this.isExactMatch) { this.active = !0; return; } if (this.exact) { this.active = window.location.pathname === t; return; } const o = (window.location.pathname ?? "/").split("/"), r = e; if (r.length > o.length) { this.active = !1; return; } let a = !0; for (let d = 0; d < r.length && !(d >= o.length); d++) { const _ = o[d], c = r[d]; if (c instanceof RegExp) { if (!c.test(_)) { a = !1; break; } } else if (_ !== c) { a = !1; break; } } this.active = a; }; } log(...t) { return W(`[${this.path}]`, ...t); } cachelog(...t) { return Y(`[${this.path}]`, ...t); } checkIsExactMatch() { return this.isExactMatch; } getFullPath() { return this.fullpath; } isCssExsit() { const t = this.shadowCSSUrl || this.cssUrl; return Array.isArray(t) ? t.length > 0 : t !== ""; } isActive() { return this.active; } isRenderError() { return !!(this.errorRender && (this.moduleReady === "rejected" || (this.shadowCSSUrl || this.cssUrl) && this.cssReady === "rejected")); } isRenderLoading() { if (this.renderAfterReady) { if (this.moduleReady !== "fulfilled") return !!this.loadingElement; if (this.isCssExsit() && this.cssReady !== "fulfilled") return !!this.loadingElement; } return !1; } isRenderElement() { return this.renderAfterReady ? !(this.isCssExsit() && this.cssReady !== "fulfilled" || this.moduleReady !== "fulfilled") : !0; } renderErrorContent() { return this.errorRender ? this.moduleReady === "rejected" ? u`${y( this.errorRender(new Error("module load rejected")) )}` : this.isCssExsit() && this.cssReady === "rejected" ? u`${y( this.errorRender(new Error("css load rejected")) )}` : f : f; } renderLoading() { return u` <div part="loading-container" class="loading-container"> ${y(this.loadingElement)} </div> `; } renderElement() { return u`${y(this.element)}`; } render() { const t = this.active ? this.isRenderError() ? this.renderErrorContent() : u` ${this.appendDirection === "before" ? u`<slot></slot>` : f} <style ${N(this._style_tag_ref)} src=${U(this._css_base64)}> ${this.cssParseMode === "base64" ? "" : this.cssContent} </style> ${this.isRenderLoading() ? this.renderLoading() : this.customRender ? u`<div part="custom-render-container" class="custom-render-container" ></div>` : this.isRenderElement() ? this.renderElement() : f} ${this.appendDirection !== "before" ? u`<slot></slot>` : f} ` : f; return this.drop ? t : u`${O(t)}`; } setCSSContent(t, e = !1) { e ? this.cssContent += t : this.cssContent = t; } async _load_CSS() { if (!(!this.isCssExsit() || this.cssReady === "fulfilled")) { if (this.cssContent) this.cssReady = "pending", this.setCSSContent(this.cssContent, !1); else { const t = this.shadowCSSUrl || this.cssUrl; if (t) { this.cssReady = "pending"; try { if (Array.isArray(t)) { const e = await Promise.all( t.map((s) => fetch(s).then((o) => o.text())) ); this.setCSSContent(e.join(""), !1); } else { const e = await fetch(t).then((s) => s.text()); this.setCSSContent(e, !1); } } catch (e) { console.error(e), this.cssReady = "rejected"; return; } } } if (await this.updateComplete, this.renderAfterReady) { let t = this._style_tag_ref.value; if (!t) { let s; this.disableShadow ? s = this.querySelector("style") : s = this.renderRoot.querySelector("style"), (s == null ? void 0 : s.tagName) === "STYLE" && (t = s); } if (!t) { console.error("cannot found style tag, make css load rejected"), this.cssReady = "rejected"; return; } await V(this, t) ? this.cssReady = "fulfilled" : this.cssReady = "rejected"; } else this.cssReady = "fulfilled"; } } async _load_URL_module() { const t = this.url; if (typeof t == "string" && this.moduleReady !== "fulfilled") { this.moduleReady = "pending"; const e = Date.now(); this._url_module = import( /* @vite-ignore */ /* webpackIgnore: true */ t ).then((s) => { if (this.log("module load fulfilled"), !this.drop && !s.destroy && console.error( `cannot found module [${this._url_module}] \`destroy\` method` ), this.lockLoadingTime) { const r = Date.now() - e; r < this.lockLoadingTime ? setTimeout(() => { this.moduleReady = "fulfilled"; }, this.lockLoadingTime - r) : this.moduleReady = "fulfilled"; } else this.moduleReady = "fulfilled"; return s; }).catch((s) => { console.error(s), this.log("module load rejected"), this.moduleReady = "rejected"; }); } } async loadAssets() { this.log("start load assets"), this._load_CSS(), this._load_URL_module(); } _could_set_cache_invalid_timer() { return this.customRender && !this.drop && this.moduleReady === "fulfilled"; } _call_module_destroy() { var t; this.log("call module destroy"), this.moduleReady === "fulfilled" ? (this.cacheDestroyTimer && (this.log("clear destroy timer first"), clearTimeout(this.cacheDestroyTimer), this.cacheDestroyTimer = void 0), (t = this._url_module) == null || t.then(({ destroy: e }) => { if (typeof e == "function") try { this.cachelog("call module destory in module: ", this), e(), this.drop || (this.isModuleRenderInstanceDestroied = !0); } catch (s) { console.warn("module destory failed"), console.warn(s); } else this.cachelog("not found destroy function"); })) : console.warn( "module not fullfilled, cannot call route component destroy" ); } _set_cache_invalid_timer() { if (this.log("call cache invaild timer"), !this._could_set_cache_invalid_timer()) return this.cachelog("cannot set cache clear timer"); this.cacheDestroyTimer && (this.cachelog("clear cache timer"), clearTimeout(this.cacheDestroyTimer), this.cacheDestroyTimer = void 0), this.active || (this.cachelog("start set cache clear timer"), this.cacheDestroyTimer = setTimeout(() => { this._call_module_destroy(); }, this.cacheVaildTime)); } async _render_url_module() { var e; if (this.shadowCSSUrl && this.cssUrl && this.cssReady !== "fulfilled" && this.cssReady !== "rejected" || this.moduleReady !== "fulfilled" && this.moduleReady !== "rejected") return; this.log("all source load end, continue logic"); let t; if (this.moduleReady === "rejected") if (this.errorRender) t = this.errorRender; else throw new Error( "No component render found, and error render not found" ); if (this.moduleReady === "fulfilled") { let s = await this._url_module; this.log("module: ", s), this.log("this.customRender: ", this.customRender), typeof this.customRender == "string" ? t = s[this.customRender] : this.customRender && (t = s.render); } if (this.log("render function: ", t), t) { await this.updateComplete; let s = this.disableShadow ? this.querySelector("div.custom-render-container") : this.cacheCustomRenderDom; if (!s) { console.warn("not found custom render dom"); return; } this.drop ? t(s) : this.isModuleRenderInstanceDestroied ? (this.log("cache invalid, call `render` function"), this.isModuleRenderInstanceDestroied = !1, t(s)) : (e = s == null ? void 0 : s.children) != null && e.length ? this.log( "custom render target has children, will not duplicately call render" ) : (this.log("custom render target has no children, call `render`"), t(s)); return; } } willUpdate(t) { this.cssParseMode === "base64" && t.has("cssContent") && (this._css_base64 = Buffer.from(this.cssContent, "base64url").toString()); } async updated(t) { t.has("active") && (this.dispatchEvent( new CustomEvent("route:active_status_change", { detail: { current: this.active, previous: t.get("active") }, bubbles: !0, composed: !0 }) ), this.dispatchEvent( new CustomEvent(`route:${this.active ? "active" : "un_active"}`, { detail: {}, bubbles: !0, composed: !0 }) ), this.url && (this.active ? (this.cacheDestroyTimer && (this.cachelog("current module active, clear timer", this), clearTimeout(this.cacheDestroyTimer), this.cacheDestroyTimer = void 0), this.log("attach route active"), this.lazy && (!this._url_module || this.moduleReady !== "fulfilled" || this.isCssExsit() && this.cssReady !== "fulfilled") ? (this.log("in lazy mode, some source not loaded, call loadAssets"), this.loadAssets()) : this._render_url_module()) : this.drop ? this._call_module_destroy() : this._set_cache_invalid_timer())), t.has("moduleReady") && t.get("moduleReady") === "pending" && this._render_url_module(), t.has("cssReady") && t.get("cssReady") === "pending" && this._render_url_module(), t.has("path") && (this.fullpath = g(this.path, this)), t.has("isExactMatch") && this.dispatchEvent( new CustomEvent("route:exact_match_change", { detail: this.isExactMatch, bubbles: !0, composed: !0 }) ); } _parse_route_match_check_group() { let t = this.fullpath, e; const s = (o) => o === "/" ? [""] : o.split("/"); return this.groupMatchMode ? (e = t.split(this.path), t.endsWith(this.path) && (e = e.slice(0, -1)), e = e.map((o) => o.replace(/\/$/, "")), e = [ ...s(e[0]), this.path, ...e.slice(1) ]) : e = s(t), e.map( (o) => x.test(o) ? new RegExp(o.replace(x, "")) : o ); } connectedCallback() { super.connectedCallback(), this.fullpath = g(this.path, this), this._route_match_check_path = this.fullpath, this._route_match_check_grouped_path = this._parse_route_match_check_group(), !this.lazy && this.url && this.loadAssets(), this.cssOnly && (this.moduleReady = "fulfilled", this._url_module = Promise.resolve({ render() { }, destroy() { } })), this.route_change_callback(), R(this.route_change_callback); } disconnectedCallback() { super.disconnectedCallback(), T(this.route_change_callback); } createRenderRoot() { return this.disableShadow ? this : super.createRenderRoot(); } }; i.styles = M` .loading-container { display: flex; align-items: center; justify-content: space-around; width: 100%; height: 100%; } `; n([ h({ type: String }) ], i.prototype, "path", 2); n([ h({ type: Boolean }) ], i.prototype, "exact", 2); n([ h({ type: String }) ], i.prototype, "element", 2); n([ h({ type: String }) ], i.prototype, "url", 2); n([ h({ type: Boolean }) ], i.prototype, "lazy", 2); n([ h({ type: String, attribute: "append-direction" }) ], i.prototype, "appendDirection", 2); n([ h({ attribute: "custom-render", converter: { fromAttribute: (t) => t === "" ? !0 : t, toAttribute: (t) => typeof t != "string" ? t ? "" : "false" : t } }) ], i.prototype, "customRender", 2); n([ h({ type: Boolean, attribute: "render-after-ready" }) ], i.prototype, "renderAfterReady", 2); n([ h({ type: Boolean, attribute: "disable-shadow" }) ], i.prototype, "disableShadow", 2); n([ h({ type: String }) ], i.prototype, "shadowCSSUrl", 2); n([ h({ type: String }) ], i.prototype, "loadingElement", 2); n([ h({ type: Number }) ], i.prototype, "lockLoadingTime", 2); n([ h({ attribute: "css-url", converter: { fromAttribute(t) { return t ? /\[.*\]/.test(t) ? JSON.parse(t) : t : ""; }, toAttribute(t) { return Array.isArray(t) ? JSON.stringify(t) : t; } } }) ], i.prototype, "cssUrl", 2); n([ h({ type: String }) ], i.prototype, "cssParseMode", 2); n([ h({ type: Number }) ], i.prototype, "cssDelayRender", 2); n([ h({ type: Function }) ], i.prototype, "errorRender", 2); n([ h({ type: Boolean }) ], i.prototype, "drop", 2); n([ h({ type: Boolean }) ], i.prototype, "cssOnly", 2); n([ h({ type: Number }) ], i.prototype, "customCSSBlockRenderTime", 2); n([ h({ type: Number }) ], i.prototype, "cacheVaildTime", 2); n([ h({ type: Boolean }) ], i.prototype, "virtualNode", 2); n([ h({ type: Boolean }) ], i.prototype, "groupMatchMode", 2); n([ l() ], i.prototype, "active", 2); n([ l({ hasChanged: () => !1 }) ], i.prototype, "fullpath", 2); n([ l({ hasChanged: () => !1 }) ], i.prototype, "_url_module", 2); n([ l() ], i.prototype, "moduleReady", 2); n([ l() ], i.prototype, "cssReady", 2); n([ l() ], i.prototype, "cssContent", 2); n([ l({ hasChanged: () => !1 }) ], i.prototype, "_css_base64", 2); n([ l() ], i.prototype, "isExactMatch", 2); n([ l({ hasChanged: () => !1 }) ], i.prototype, "isModuleRenderInstanceDestroied", 2); n([ l({ hasChanged: () => !1 }) ], i.prototype, "cacheDestroyTimer", 2); n([ L("div.custom-render-container", !1) ], i.prototype, "cacheCustomRenderDom", 2); i = n([ k("native-route") ], i); var J = Object.defineProperty, X = Object.getOwnPropertyDescriptor, m = (t, e, s, o) => { for (var r = o > 1 ? void 0 : o ? X(e, s) : e, a = t.length - 1, d; a >= 0; a--) (d = t[a]) && (r = (o ? d(e, s, r) : d(r)) || r); return o && r && J(e, s, r), r; }; const K = w("native-spa-route"); let p = class extends $ { constructor() { super(...arguments), this.rootPath = "/", this.autoCatchHrefClick = !0, this.disableShadow = !1, this.status = "common", this.active = !1, this._route_change_callback = async () => { this.active = location.pathname.startsWith(this.rootPath), this.active ? await this._is_match_route() ? this.status = "common" : this.status = "404" : this.status = "common"; }; } // constructor() { // super(); // } /** * get current container's status */ getStatus() { return this.status; } /** * if disabled shadow dom, this is the only way to add 404 status content */ set404Content(t) { this.contentOf404 = t; } _render_404() { if (!this.contentOf404) return f; let t = typeof this.contentOf404; return t === "string" ? u`${y(this.contentOf404)}` : t === "function" ? u`${y(this.contentOf404() ?? "")}` : f; } _render_with_light_dom() { return this.active && this.status === "404" ? this._render_404() : f; } _render_with_shadow_dom() { return this.active && this.status === "404" ? u`<slot name="404">${this._render_404()}</slot>` : u`<slot></slot>`; } render() { return this.disableShadow ? this._render_with_light_dom() : this._render_with_shadow_dom(); } async _is_match_route() { const t = this.querySelectorAll("native-route"); await Promise.all(Array.from(t).map((e) => e.updateComplete)); for (const e of t) if (!e.virtualNode && e.isActive()) return K("this route is active: ", e), !0; return !1; } updated(t) { } createRenderRoot() { return this.disableShadow ? this : super.createRenderRoot(); } connectedCallback() { super.connectedCallback(), this._route_change_callback(), R(this._route_change_callback); } disconnectedCallback() { super.disconnectedCallback(), T(this._route_change_callback); } }; m([ h({ type: String, attribute: "root-path", reflect: !0 }) ], p.prototype, "rootPath", 2); m([ h({ type: Boolean }) ], p.prototype, "autoCatchHrefClick", 2); m([ h({ type: Boolean }) ], p.prototype, "disableShadow", 2); m([ P("native-route") ], p.prototype, "allRouteNode", 2); m([ l() ], p.prototype, "statusRenderMap", 2); m([ l() ], p.prototype, "status", 2); m([ l() ], p.prototype, "contentOf404", 2); m([ l() ], p.prototype, "active", 2); p = m([ k("native-route-container") ], p); let D = !1; const nt = "0.2.12"; function at() { D || (D = !0, B(), q()); } export { i as Route, p as RouteContainer, B as hook_a_link, q as hook_history_change, R as hook_route_change, at as preload, it as redirect, T as unhook_route_change, nt as version }; //# sourceMappingURL=main.es.js.map