UNPKG

@fastkit/vue-page

Version:

Middleware for more convenient control of routing in Vue applications.

1,158 lines (1,152 loc) 33.6 kB
import { EV } from '@fastkit/ev'; import { defineComponent, createVNode, ref, computed, Transition, h, Fragment, inject, onBeforeUnmount, nextTick, reactive, provide } from 'vue'; import { withCtx, NumberishPropOption, resolveNumberish, ClientOnly, extractRouteMatchedItems, getRouteQuery, isComponentCustomOptions, getRouteMatchedComponents } from '@fastkit/vue-utils'; import { RouterView, useRouter, stringifyQuery, RouterLink, useLink } from 'vue-router'; import { IN_WINDOW, isObject, isPromise } from '@fastkit/helpers'; import { Cookies } from '@fastkit/cookies'; import { TinyLogger, createTinyError } from '@fastkit/tiny-logger'; 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(); }); }); } }; var _suspenseRouteBucket; function getSuspenseRouteBucket() { if (!_suspenseRouteBucket) { _suspenseRouteBucket = new SuspenseRouteBucket(); } return _suspenseRouteBucket; } 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) { const matchedBehavior = findLastMatchedBehavior(to) || resolveRawRouterScrollBehavior("top"); position = matchedBehavior(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 { const el = document.querySelector(hash); if (el) { 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; } 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); } var interpolatePath = (route, match) => match.path.replace(/(:\w+)\([^)]+\)/g, "$1").replace(/(:\w+)[?+*]/g, "$1").replace(/:\w+/g, (r) => route.params[r.slice(1)]?.toString() || ""); var 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; }; var generateRouteKeyWithWatchQuery = (routeProps, override) => { const routeKey = generateRouteKey(routeProps, override); if (!routeKey) return routeKey; return `${routeKey}${generateWatchQueryKeySuffix(routeProps)}`; }; function routeKeyWithWatchQueryByRouteItem(route, item) { const routeKey = interpolatePath(route, item.route); const routeProps = { Component: item.Component, route }; return `${routeKey}${generateWatchQueryKeySuffix(routeProps)}`; } var __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]; } } var name = "vue-page"; new TinyLogger(name); var VuePageError = createTinyError(name); // src/injections.ts var 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; } // src/composables/page-error.ts var name2 = "VuePageControlError"; function isVuePageControlError(source) { return source instanceof VuePageControlError; } var VuePageControlError = class extends Error { name = name2; 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 = // eslint-disable-next-line no-nested-ternary 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 }; } }; var 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])])]); }; } }); // src/composables/page-control.ts function extractPrefetch(Component2) { if (!isComponentCustomOptions(Component2)) return; let { prefetch, middleware = [] } = Component2; if (!Array.isArray(middleware)) { middleware = [middleware]; } const { prefetchHandler } = Component2; if (prefetch && typeof prefetch === "object") { prefetch = prefetch.prefetch; } if (typeof prefetch === "function" || middleware.length) { return { prefetch, prefetchHandler, middleware }; } } function updateWatchQueryOption(Component2, queries) { if (!isComponentCustomOptions(Component2)) return; if (Component2.watchQuery === true) return; Component2.watchQuery = Component2.watchQuery || []; const { watchQuery } = Component2; 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) { const ctx = { control, to, from, matched: { from: fromItem, to: _item }, pageKey }; forcePrefetch = prefetchHandler(ctx); } 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; } var DEFAULT_REDIRECT_STATUS = 302; var STATE_BUCKET_SYMBOL = "pgc-state"; var 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: name3, params } = source; return { statusCode, query, hash, path, name: name3, params }; } var 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; const per = total === 0 ? 0 : resolved / total; return { total, resolved, per, 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; } // eslint-disable-line prettier/prettier useState(key, defaultValue) { const bucket = this.initialState[STATE_BUCKET_SYMBOL]; let data = bucket[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); } // eslint-disable-line prettier/prettier 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(Component2) { if (!this.suspenseComponents.includes(Component2)) { this._suspenseComponents.value.push(() => Component2); } } onSuspenseResolved(Component2) { const index = this.suspenseComponents.indexOf(Component2); 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: name3, params } = _redirectSpec; if (name3) { const tmp = this.router.resolve({ name: name3, 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) { const isInSite = /(^[.]{1,2}\/)|(^\/(?!\/))/.test(fullPath); if (isInSite) { 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); } // eslint-disable-next-line no-dupe-class-members 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) => { const v = value === void 0 ? defaultData : value; return v; }); } }; function createPrefetch(key, data) { const prefetch = function prefetch2(queue) { return queue.provide(key, data); }; function inject3(defaultValue, allowNull) { const pageControl = useVuePageControl(); return pageControl.injectData(key, defaultValue, allowNull); } return { key, prefetch, inject: inject3 }; } // 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; } function useState(key, defaultValue) { return inject(key, defaultValue, true); } function provideState(key, value) { return provide(key, value); } var DEFAULT_TRANSITION = "page"; var VPage = defineComponent({ name: "VPage", props: { pageKey: { type: [Function, String], default: null } }, setup(props) { const currentComponent = ref(null); const transition = computed(() => { const vnode = currentComponent.value; if (!vnode) return DEFAULT_TRANSITION; return DEFAULT_TRANSITION; }); return () => createVNode(RouterView, null, { default: withCtx((routeProps) => { const { Component: Component2 } = 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: () => [Component2 ? h(Component2, { key }) : void 0] })]; }) }); } }); var 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 ${duration.value}ms`, 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); }; } }); var VPageLink = defineComponent({ name: "VPageLink", setup(_props, ctx) { const vpc = useVuePageControl(); return () => h(vpc.RouterLink, ctx.attrs, ctx.slots); } }); var VPageRoot = defineComponent({ name: "VPageRoot", setup(props, ctx) { const control = useVuePageControl(); useRouter(); return () => { if (control.serverRedirected) { return []; } const { pageError } = control; const content = pageError ? h(control.ErrorComponent, { key: "error" }) : ctx.slots.default?.(); return createVNode(Fragment, null, [content, createVNode(ClientOnly, null, { default: () => [createVNode(VPageProgress, null, null)] })]); }; } }); 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 //# sourceMappingURL=vue-page.mjs.map