native-spa-route
Version:
provide SPA route's experience with native web component
651 lines (650 loc) • 22.5 kB
JavaScript
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