UNPKG

@fastkit/vue-page

Version:

Middleware for more convenient control of routing in Vue applications.

1,028 lines (1,027 loc) 30.6 kB
import { EV } from "@fastkit/ev"; import { Fragment, Transition, computed, createVNode, defineComponent, h, inject, nextTick, onBeforeUnmount, provide, reactive, ref } from "vue"; import { ClientOnly, NumberishPropOption, extractRouteMatchedItems, getRouteMatchedComponents, getRouteQuery, isComponentCustomOptions, resolveNumberish, withCtx } from "@fastkit/vue-utils"; import { RouterLink, RouterView, stringifyQuery, useLink, useRouter } from "vue-router"; import { IN_WINDOW, isObject, isPromise } from "@fastkit/helpers"; import { Cookies } from "@fastkit/cookies"; import { TinyLogger, createTinyError } from "@fastkit/tiny-logger"; //#region src/composables/router/suspense-route-bucket.ts var SuspenseRouteBucket = class extends EV { _sources = ref([]); _total = ref(0); _progress; get sources() { return this._sources.value; } get total() { return this._total.value; } get progress() { return this._progress.value; } constructor() { super(); this._progress = computed(() => { const { total, sources } = this; return { total, resolved: total - sources.length }; }); this.push = this.push.bind(this); this.remove = this.remove.bind(this); } push(source) { if (!source) return; const { sources } = this; if (!sources.includes(source)) { sources.push(source); this._total.value++; const payload = { bucket: this, source }; this.emit("push", payload); if (sources.length === 1) this.emit("start", payload); } } remove(source) { if (!source) return; const { sources } = this; const index = sources.indexOf(source); if (index !== -1) { sources.splice(index, 1); const payload = { bucket: this, source }; this.emit("remove", payload); if (sources.length === 0) { this._total.value = 0; this.emit("finish", payload); } } } ensureReady() { return new Promise((resolve) => { const { total } = this; if (total === 0) return resolve(); this.once("finish", () => { resolve(); }); }); } }; let _suspenseRouteBucket; function getSuspenseRouteBucket() { if (!_suspenseRouteBucket) _suspenseRouteBucket = new SuspenseRouteBucket(); return _suspenseRouteBucket; } //#endregion //#region src/composables/router/scroll-behavior.ts var ScrollBehaviorMessenger = class { static fns = []; static trigger() { this.fns.forEach((fn) => fn(false)); this.fns = []; } static reserveNextPosition(position) { return new Promise((resolve) => { this.fns.push((canceled) => { resolve(canceled ? void 0 : position); }); }); } static release() { this.fns.forEach((fn) => fn(true)); this.fns = []; } }; function resolveRawRouterScrollBehavior(source) { if (source === "top") source = (to, from, savedPosition) => savedPosition || { top: 0, left: 0 }; return source; } function extractMatchedBehaviors(route) { const components = getRouteMatchedComponents(route); const behaviors = []; components.forEach(({ scrollBehavior }) => { if (scrollBehavior != null) behaviors.push(resolveRawRouterScrollBehavior(scrollBehavior)); }); return behaviors; } function findLastMatchedBehavior(route) { const behaviors = extractMatchedBehaviors(route); return behaviors[behaviors.length - 1]; } function createScrollBehavior() { const scrollBehavior = (to, from, savedPosition) => { let position = false; const isRouteChanged = to !== from; if (savedPosition) position = savedPosition; else if (isRouteChanged) position = (findLastMatchedBehavior(to) || resolveRawRouterScrollBehavior("top"))(to, from, savedPosition); else if (to.path === from.path && to.hash !== from.hash) {} return getSuspenseRouteBucket().ensureReady().then(async () => { if (to.hash) { let { hash } = to; if (typeof window.CSS !== "undefined" && typeof window.CSS.escape !== "undefined") hash = `#${window.CSS.escape(hash.substr(1))}`; try { if (document.querySelector(hash)) position = { el: hash }; } catch (e) { console.warn("Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape)."); } } return ScrollBehaviorMessenger.reserveNextPosition(position); }); }; return scrollBehavior; } //#endregion //#region src/utils.ts function parseWatchQueryOption(route, opts) { if (opts === true) opts = Object.keys(route.query); if (!opts || !opts.length) return ""; const query = {}; opts.forEach((row) => { query[row] = route.query[row]; }); const queryStr = stringifyQuery(query); let key = "&"; if (queryStr) key += `?${queryStr}`; return key; } function extractWatchQueryOption(source) { if (!isObject(source)) return; if (source.watchQuery) return source.watchQuery; if (isObject(source.type) && source.type.watchQuery) return source.type.watchQuery; } function generateWatchQueryKeySuffix(routeProps) { const option = extractWatchQueryOption(routeProps.Component); return parseWatchQueryOption(routeProps.route, option); } const interpolatePath = (route, match) => match.path.replace(/(:\w+)\([^)]+\)/g, "$1").replace(/(:\w+)[?+*]/g, "$1").replace(/:\w+/g, (r) => route.params[r.slice(1)]?.toString() || ""); const generateRouteKey = (routeProps, override) => { const matchedRoute = routeProps.route.matched.find((m) => routeProps.Component && m.components?.default === routeProps.Component.type); if (!matchedRoute) return ""; const source = override ?? matchedRoute?.meta.key ?? interpolatePath(routeProps.route, matchedRoute); return typeof source === "function" ? source(routeProps.route) : source; }; const generateRouteKeyWithWatchQuery = (routeProps, override) => { const routeKey = generateRouteKey(routeProps, override); if (!routeKey) return routeKey; return `${routeKey}${generateWatchQueryKeySuffix(routeProps)}`; }; function routeKeyWithWatchQueryByRouteItem(route, item) { return `${interpolatePath(route, item.route)}${generateWatchQueryKeySuffix({ Component: item.Component, route })}`; } let __forcePrefetchStates = {}; function getForcePrefetchStates(pageKey) { return __forcePrefetchStates[pageKey]; } function consumeForcePrefetchStates(pageKey) { const value = getForcePrefetchStates(pageKey); setForcePrefetchStates(pageKey, false); return value; } function setForcePrefetchStates(pageKey, value) { if (IN_WINDOW) return; if (value) __forcePrefetchStates[pageKey] = value; else delete __forcePrefetchStates[pageKey]; } //#endregion //#region src/logger.ts const name$1 = "vue-page"; new TinyLogger(name$1); const VuePageError = createTinyError(name$1); //#endregion //#region src/injections.ts const VuePageControlInjectionKey = Symbol("VuePageControl"); function useVuePageControl(opts = {}) { const pageControl = inject(VuePageControlInjectionKey, null); if (!pageControl) throw new VuePageError(`missing provided VuePageControl.`); const { onStart, onFinish, onError } = opts; if (onStart || onFinish) { onStart && pageControl.on("start", onStart); onFinish && pageControl.on("finish", onFinish); onError && pageControl.on("error", onError); onBeforeUnmount(() => { onStart && pageControl.off("start", onStart); onFinish && pageControl.off("finish", onFinish); onError && pageControl.off("error", onError); }); } return pageControl; } //#endregion //#region src/composables/page-error.ts const name = "VuePageControlError"; function isVuePageControlError(source) { return source instanceof VuePageControlError; } var VuePageControlError = class extends Error { name = name; statusCode; constructor(rawSettings = {}) { let settings; if (rawSettings instanceof Error) if (isVuePageControlError(rawSettings)) settings = { statusCode: rawSettings.statusCode, message: rawSettings.message, stack: rawSettings.stack }; else settings = { message: rawSettings.message, stack: rawSettings.stack }; else settings = typeof rawSettings === "string" ? { message: rawSettings } : rawSettings && typeof rawSettings === "object" ? { statusCode: rawSettings.statusCode, message: rawSettings.message, stack: rawSettings.stack } : {}; const { statusCode = 500, message = "An error has occurred.", stack } = settings; super(message); this.statusCode = statusCode; if (stack) this.stack = stack; } toJSON() { return { name: this.name, statusCode: this.statusCode, message: this.message, stack: this.stack }; } }; //#endregion //#region src/components/VErrorPage/VErrorPage.tsx const VErrorPage = defineComponent({ name: "VErrorPage", setup() { const control = useVuePageControl(); return () => { const pageError = control.pageError || new VuePageControlError(); return createVNode("div", { "class": "v-error-page", "style": { padding: "32px", fontSize: "16px", lineHeight: "1.5" } }, [ createVNode("h1", { "class": "v-error-page__status-code", "style": { fontSize: "3em", margin: "16px 0" } }, [pageError.statusCode]), createVNode("p", { "class": "v-error-page__message", "style": { margin: "1em 0" } }, [pageError.message]), createVNode("pre", { "class": "v-error-page__stack", "style": { whiteSpace: "pre-wrap" } }, [createVNode("code", { "style": { fontFamily: "monospace, monospace", fontSize: "0.75em" } }, [pageError.stack])]) ]); }; } }); //#endregion //#region src/composables/page-control.ts function extractPrefetch(Component) { if (!isComponentCustomOptions(Component)) return; let { prefetch, middleware = [] } = Component; if (!Array.isArray(middleware)) middleware = [middleware]; const { prefetchHandler } = Component; if (prefetch && typeof prefetch === "object") prefetch = prefetch.prefetch; if (typeof prefetch === "function" || middleware.length) return { prefetch, prefetchHandler, middleware }; } function updateWatchQueryOption(Component, queries) { if (!isComponentCustomOptions(Component)) return; if (Component.watchQuery === true) return; Component.watchQuery = Component.watchQuery || []; const { watchQuery } = Component; for (const query of queries) if (!watchQuery.includes(query)) watchQuery.push(query); } function extractRouteMatchedItemsWithPrefetch(control, to, from) { const extracted = []; const fromMatched = extractRouteMatchedItems(from); const fromItems = {}; fromMatched.forEach((_item) => { const fromKey = routeKeyWithWatchQueryByRouteItem(from, _item); fromItems[fromKey] = _item; }); const matched = extractRouteMatchedItems(to); matched.forEach((_item) => { const prefetchSettings = extractPrefetch(_item.Component); if (!prefetchSettings) return; const { prefetchHandler, middleware } = prefetchSettings; let { prefetch } = prefetchSettings; const pageKey = routeKeyWithWatchQueryByRouteItem(to, _item); const fromItem = fromItems[pageKey]; let forcePrefetch; if (prefetchHandler) forcePrefetch = prefetchHandler({ control, to, from, matched: { from: fromItem, to: _item }, pageKey }); setForcePrefetchStates(pageKey, forcePrefetch || false); if (!forcePrefetch && fromItem) prefetch = void 0; function updateQueries(queries) { updateWatchQueryOption(_item.Component, queries); } extracted.push({ ..._item, prefetch, updateQueries, middleware }); }); return { extracted, matched }; } function createMiddleware(source) { return source; } const DEFAULT_REDIRECT_STATUS = 302; const STATE_BUCKET_SYMBOL = "pgc-state"; const QUERY_BUCKET_SYMBOL = "pgc-query"; function resolveRawVuePageControlRedirectSpec(source) { if (typeof source === "string") source = { path: source }; const { statusCode = DEFAULT_REDIRECT_STATUS, query, hash } = source; const { path } = source; const { name, params } = source; return { statusCode, query, hash, path, name, params }; } const ERROR_STATE_NAME = "__vpe__"; var VuePageControl = class extends EV { app; router; cookies; RouterLink; useLink; _from = ref(null); _to = ref(null); _initialState; _beforeRoute = ref(null); _route; _stopFn; _prefetchQueues = ref([]); _initialStateConsumed = false; _runningQueues; _preftechProgress; _suspenseComponents = ref([]); _err = ref(null); _ErrorComponent; request; response; _serverRedirect; _writeResponse; isClient; middleware; _redirectSpec; _providedMap = /* @__PURE__ */ new Map(); _transitioning = ref(false); _serverRedirected = ref(false); get isServer() { return !this.isClient; } get serverRedirected() { return this._serverRedirected.value; } get from() { return this._from.value; } get to() { return this._to.value; } get initialState() { return this._initialState; } get beforeRoute() { return this._beforeRoute.value; } get route() { return this._route.value; } get prefetchQueues() { return this._prefetchQueues.value.map((g) => g()); } get initialStateConsumed() { return this._initialStateConsumed; } get runningQueues() { return this._runningQueues.value; } get preftechProgress() { return this._preftechProgress.value; } get suspenseComponents() { return this._suspenseComponents.value.map((g) => g()); } get pageError() { const _err = this._err.value; return _err && _err(); } get ErrorComponent() { return this._ErrorComponent; } get transitioning() { return this._transitioning.value; } constructor(settings) { super(); const { app, router, initialState = {}, initialRoute, ErrorComponent, request, response, serverRedirect, writeResponse, middleware = [] } = settings; this.app = app; this.router = router; this.request = request; this.response = response; this.isClient = typeof window !== "undefined"; this.RouterLink = settings.RouterLink || RouterLink; this.useLink = settings.useLink || useLink; const cookiesContext = this.isClient ? document : { req: request, res: response }; this.cookies = new Cookies(cookiesContext, { bucket: reactive({}) }); this.middleware = middleware; this._serverRedirect = serverRedirect; this._writeResponse = writeResponse; this._ErrorComponent = ErrorComponent || VErrorPage; initialState[STATE_BUCKET_SYMBOL] = initialState[STATE_BUCKET_SYMBOL] || {}; initialState[QUERY_BUCKET_SYMBOL] = initialState[QUERY_BUCKET_SYMBOL] || {}; initialState[STATE_BUCKET_SYMBOL] = reactive(initialState[STATE_BUCKET_SYMBOL]); this._initialState = initialState; this._route = ref(initialRoute); this._runningQueues = computed(() => this.prefetchQueues.filter((q) => q.running)); this._preftechProgress = computed(() => { const { prefetchQueues, runningQueues } = this; const total = prefetchQueues.length; const resolved = total - runningQueues.length; return { total, resolved, per: total === 0 ? 0 : resolved / total, finished: resolved >= total }; }); [ "_handleBeforeResolve", "_handleAfterEach", "setInitialState", "injectData", "onSuspensePending", "redirect" ].forEach((fn) => { const _fn = this[fn]; this[fn] = _fn.bind(this); }); if (IN_WINDOW) { const initialError = this.consumeInitialState(ERROR_STATE_NAME); if (initialError) this._setPageError(initialError); } this._init(); } /** @private */ _getQueryBucket(provideKey) { const obj = this.initialState[QUERY_BUCKET_SYMBOL]; let bucket = obj[provideKey]; if (!bucket) { bucket = []; obj[provideKey] = bucket; } return bucket; } /** @private */ _pushQueryToBucket(provideKey, query) { const bucket = this._getQueryBucket(provideKey); if (!bucket.includes(query)) bucket.push(query); } async initState(key, initializer) { const bucket = this.initialState[STATE_BUCKET_SYMBOL]; let data = bucket[key]; if (data === void 0) data = typeof initializer === "function" ? await initializer() : initializer; bucket[key] = reactive(data); this.app.provide(key, data); return data; } useState(key, defaultValue) { let data = this.initialState[STATE_BUCKET_SYMBOL][key]; if (data === void 0) data = typeof defaultValue === "function" ? defaultValue() : defaultValue; return data; } provide(key, value) { this.app.provide(key, value); this._providedMap.set(key, value); } inject(key, defaultValue, treatDefaultAsFactory) { let value = this._providedMap.get(key); if (value === void 0 && defaultValue !== void 0) value = treatDefaultAsFactory && typeof defaultValue === "function" ? defaultValue() : defaultValue; return value; } addMiddleware(middleware) { this.middleware.push(middleware); } writeResponse(params) { const { _writeResponse } = this; if (!_writeResponse) throw new Error("missing provided writeResponse function."); return _writeResponse(params); } onSuspensePending(Component) { if (!this.suspenseComponents.includes(Component)) this._suspenseComponents.value.push(() => Component); } onSuspenseResolved(Component) { const index = this.suspenseComponents.indexOf(Component); if (index !== -1) this._suspenseComponents.value.splice(index, 1); } setInitialState(key, data) { if (this.initialState) this.initialState[key] = data; } consumeInitialState(key) { const { initialState } = this; if (!initialState) return; const v = initialState[key]; delete initialState[key]; return v; } injectData(key, defaultData, allowNull) { const queue = this.prefetchQueues.find(({ _provideKey }) => _provideKey === key); const data = queue && queue.resolvedData; const result = data === void 0 ? defaultData : data; if (!allowNull && result == null) throw new Error(`missing provided data. "${key}"`); return result; } _stop() { if (this._stopFn) { this._stopFn(); delete this._stopFn; } } _init() { this._stop(); const fns = []; fns.push(this.router.beforeResolve(this._handleBeforeResolve), this.router.afterEach(this._handleAfterEach)); this._stopFn = () => { fns.forEach((fn) => fn()); delete this._stopFn; }; } async _handleAfterEach(to, from) { delete this._redirectSpec; this._transitioning.value = false; const { router } = this; this._beforeRoute.value = router.resolve(from); this._route.value = router.resolve(to); this.emit("finish", this.preftechProgress); } _closePrefetchs() { this.prefetchQueues.forEach((queue) => queue.close()); this._prefetchQueues.value = []; } redirect(redirectSpec) { this._redirectSpec = resolveRawVuePageControlRedirectSpec(redirectSpec); if (!this.transitioning) this._triggerRedirect(); } _triggerRedirect() { const { _redirectSpec } = this; if (!_redirectSpec) return; let { path, query, hash } = _redirectSpec; const { statusCode, name, params } = _redirectSpec; if (name) { const tmp = this.router.resolve({ name, params, query, hash }); path = tmp.path; query = tmp.query; hash = tmp.hash; } const queryStr = query ? stringifyQuery(query) : query; const queryAppends = queryStr ? path.includes("?") ? `&${queryStr}` : `?${queryStr}` : ""; let fullPath = `${path}${queryAppends}`; if (/^\//.test(fullPath)) { const { base } = this.router.options.history; fullPath = `${base}/${fullPath.replace(/^\//, "")}`; } if (this.isClient) { if (/(^[.]{1,2}\/)|(^\/(?!\/))/.test(fullPath)) this.router.replace({ path, query, hash }); else window.location.replace(fullPath); return false; } if (!this._serverRedirect) throw new Error("required server redirect function."); this._serverRedirected.value = true; this._serverRedirect(fullPath, statusCode); } async _handleBeforeResolve(to, from) { this._to.value = to; this._from.value = from; this._closePrefetchs(); delete this._redirectSpec; if (IN_WINDOW && !this.initialStateConsumed && this.pageError) { this._initialStateConsumed = true; return; } try { const { extracted, matched } = extractRouteMatchedItemsWithPrefetch(this, to, from); if (!matched.length) throw new VuePageControlError({ statusCode: 404, message: "This page could not be found" }); this._transitioning.value = true; const allMiddleware = [...this.middleware]; extracted.forEach(({ middleware }) => { allMiddleware.push(...middleware); }); for (const middlewareFn of allMiddleware) { await middlewareFn(this); if (this._redirectSpec) break; } if (this._redirectSpec) return this._triggerRedirect(); const requiredItems = extracted.filter((e) => !!e.prefetch); if (!requiredItems.length) { this._initialStateConsumed = true; this._deletePageError(); return; } const queueSetups = []; let queues = requiredItems.map((item) => { const queue = new VuePagePrefetchQueue(this, item, to); if (queue._setupPromise) queueSetups.push(queue._setupPromise); return queue; }); this._prefetchQueues.value = queues.map((q) => () => q); this.emit("start", this.preftechProgress); await Promise.all(queueSetups); if (this._redirectSpec) return this._triggerRedirect(); queues.forEach((queue) => { const { item, provideKey } = queue; if (provideKey) item.updateQueries(this._getQueryBucket(provideKey)); }); const providePromises = []; queues = queues.filter(({ _providePromise }) => { if (!_providePromise) return false; providePromises.push(_providePromise); return true; }); this._prefetchQueues.value = queues.map((q) => () => q); await Promise.all(providePromises); if (this._redirectSpec) return this._triggerRedirect(); this._initialStateConsumed = true; this._deletePageError(); } catch (_err) { this._setPageError(_err); } } _deletePageError() { this._err.value = null; } _setPageError(_err, withEmit = true) { const err = new VuePageControlError(_err); this._err.value = () => err; if (!IN_WINDOW) { this.setInitialState(ERROR_STATE_NAME, err.toJSON()); this._writeStates(err.statusCode); } withEmit && this.emit("error", err); return err; } _writeStates(status) { if (IN_WINDOW || !this.response) return; if (this.response.headersSent) return; this.response.statusCode = status; this.writeResponse({ status }); } }; var VuePagePrefetchQueue = class extends EV { control; item; route; _resolved = ref(false); _canceled = ref(false); _running; _resolvedData; /** @private */ _providePromise; /** @private */ _provideKey; /** @private */ _setupPromise; get resolved() { return this._resolved.value; } get canceled() { return this._canceled.value; } get provideKey() { return this._provideKey; } get resolvedData() { return this._resolvedData; } get params() { return this.route.params; } get query() { return this.route.query; } get running() { return this._running.value; } constructor(control, item, to) { super(); this.control = control; this.item = item; this.route = to; [ "close", "cancel", "provide", "injectOtherQueue", "getQuery" ].forEach((fn) => { const _fn = this[fn]; this[fn] = _fn.bind(this); }); this._running = computed(() => !this.resolved && !this.canceled); const prefetchFn = typeof item.prefetch === "function" ? item.prefetch : item.prefetch.prefetch; this._setupPromise = prefetchFn(this); } getQuery(key, type = String, defaultValue) { const { provideKey } = this; if (!provideKey) throw new Error("missing provideKey"); this.control._pushQueryToBucket(provideKey, key); return getRouteQuery(this.query, key, type, defaultValue); } close() { this._canceled.value = true; this.emit("close", void 0); this.offAll(); delete this._providePromise; delete this._setupPromise; delete this._resolvedData; delete this.item; delete this.control; } cancel() { return this.close(); } provide(key, data) { this._provideKey = key; let promise; if (IN_WINDOW && !this.control.initialStateConsumed) { const consumed = this.control.consumeInitialState(key); promise = Promise.resolve(consumed); } else if (typeof data !== "function") promise = Promise.resolve(data); else { const payload = data(this); promise = isPromise(payload) ? payload : Promise.resolve(payload); } this._providePromise = promise.then((v) => { this._resolved.value = true; this._resolvedData = v; if (!IN_WINDOW) this.control.setInitialState(key, v); return v; }); return this._providePromise; } injectOtherQueue(key, defaultData) { const queue = this.control.prefetchQueues.find(({ _provideKey }) => _provideKey === key); if (!queue || !queue._providePromise) return Promise.resolve(defaultData); return queue._providePromise.then((value) => { return value === void 0 ? defaultData : value; }); } }; function createPrefetch(key, data) { const prefetch = function prefetch(queue) { return queue.provide(key, data); }; function inject(defaultValue, allowNull) { return useVuePageControl().injectData(key, defaultValue, allowNull); } return { key, prefetch, inject }; } //#endregion //#region src/composables/plugin.ts function installVuePageControl(settings) { const pageControl = new VuePageControl(settings); settings.app.provide(VuePageControlInjectionKey, pageControl); settings.app.config.globalProperties.$vpc = pageControl; return pageControl; } //#endregion //#region src/composables/state.ts function useState(key, defaultValue) { return inject(key, defaultValue, true); } function provideState(key, value) { return provide(key, value); } //#endregion //#region src/components/VPage.tsx const DEFAULT_TRANSITION = "page"; const VPage = defineComponent({ name: "VPage", props: { pageKey: { type: [Function, String], default: null } }, setup(props) { const currentComponent = ref(null); const transition = computed(() => { if (!currentComponent.value) return DEFAULT_TRANSITION; return DEFAULT_TRANSITION; }); /** * @TODO */ return () => createVNode(RouterView, null, { default: withCtx((routeProps) => { const { Component } = routeProps; let key = generateRouteKeyWithWatchQuery(routeProps, props.pageKey); if (consumeForcePrefetchStates(key)) key = `${key}:${Date.now()}`; return [createVNode(Transition, { "name": transition.value, "mode": "out-in", "onEnter": (ev) => { ScrollBehaviorMessenger.trigger(); }, "onEnterCancelled": () => { ScrollBehaviorMessenger.release(); } }, { default: () => [Component ? h(Component, { key }) : void 0] })]; }) }); } }); //#endregion //#region src/components/VPageProgress/VPageProgress.tsx const VPageProgress = defineComponent({ name: "VPageProgress", props: { color: String, failedColor: String, height: NumberishPropOption, throttle: NumberishPropOption, duration: NumberishPropOption, continuous: Boolean, rtl: Boolean }, setup(props) { useVuePageControl({ onStart: () => { start(); }, onFinish: () => { finish(); }, onError: () => { canSucceed.value = false; } }); let _timer = null; let _throttle = null; let _cut; const show = ref(false); const percent = ref(0); const skipTimerCount = ref(0); const canSucceed = ref(true); const throttle = computed(() => resolveNumberish(props.throttle, 200)); const duration = computed(() => resolveNumberish(props.duration, 3e3)); const reversed = ref(props.rtl); const continuous = computed(() => props.continuous); const color = computed(() => props.color || "black"); const failedColor = computed(() => props.failedColor || "red"); const height = computed(() => resolveNumberish(props.height, 2)); const width = computed(() => `${percent.value}%`); const styles = computed(() => ({ position: "fixed", top: 0, left: props.rtl ? "auto" : 0, right: 0, zIndex: 999999, width: width.value, height: `${height.value}px`, background: canSucceed.value ? color.value : failedColor.value, opacity: 1, transition: `width 0.1s, opacity 0.4s` })); function clear() { _timer !== null && clearInterval(_timer); _throttle !== null && clearTimeout(_throttle); _timer = null; } function increase(num) { percent.value = Math.min(100, Math.floor(percent.value + num)); } function decrease(num) { percent.value = Math.max(0, Math.floor(percent.value - num)); } function finish() { percent.value = reversed.value ? 0 : 100; hide(); } function hide() { clear(); window.setTimeout(() => { show.value = false; nextTick(() => { percent.value = 0; reversed.value = false; }); }, 500); } function start() { clear(); percent.value = 0; reversed.value = false; skipTimerCount.value = 0; canSucceed.value = true; if (throttle.value) _throttle = window.setTimeout(() => startTimer(), throttle.value); else startTimer(); } function startTimer() { if (!show.value) show.value = true; _timer = window.setInterval(() => { if (typeof _cut === "undefined") _cut = 1e4 / Math.floor(duration.value); if (skipTimerCount.value > 0) { skipTimerCount.value--; return; } if (reversed.value) decrease(_cut); else increase(_cut); if (continuous.value && (percent.value >= 100 || percent.value <= 0)) { skipTimerCount.value = 1; reversed.value = !reversed.value; } }, 100); } return () => { if (!show.value) return; return createVNode("div", { "class": "v-page-progress", "style": styles.value }, null); }; } }); //#endregion //#region src/components/VPageLink.ts const VPageLink = defineComponent({ name: "VPageLink", setup(_props, ctx) { const vpc = useVuePageControl(); return () => h(vpc.RouterLink, ctx.attrs, ctx.slots); } }); //#endregion //#region src/components/VPageRoot/VPageRoot.tsx const VPageRoot = defineComponent({ name: "VPageRoot", setup(props, ctx) { const control = useVuePageControl(); useRouter(); return () => { if (control.serverRedirected) return []; const { pageError } = control; return createVNode(Fragment, null, [pageError ? h(control.ErrorComponent, { key: "error" }) : ctx.slots.default?.(), createVNode(ClientOnly, null, { default: () => [createVNode(VPageProgress, null, null)] })]); }; } }); //#endregion export { ScrollBehaviorMessenger, VErrorPage, VPage, VPageLink, VPageProgress, VPageRoot, VuePageControl, VuePageControlError, VuePageControlInjectionKey, VuePagePrefetchQueue, createMiddleware, createPrefetch, createScrollBehavior, extractRouteMatchedItemsWithPrefetch, getSuspenseRouteBucket, installVuePageControl, isVuePageControlError, provideState, useState, useVuePageControl }; //# sourceMappingURL=vue-page.mjs.map