From 589387f051f87a087d4eade6719e0b56224ab358 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Tue, 18 Oct 2022 01:05:19 +0530 Subject: [PATCH] feat: add page load progress bar --- docs/guide/theme-introduction.md | 5 +- docs/guide/using-vue.md | 5 +- .../theme-default/components/VPNavBar.vue | 4 +- src/client/theme-default/index.ts | 27 +- src/client/theme-default/nprogress/index.css | 31 ++ src/client/theme-default/nprogress/index.d.ts | 45 ++ src/client/theme-default/nprogress/index.js | 505 ++++++++++++++++++ .../styles/lib-override/nprogress.css | 12 - theme.d.ts | 2 + 9 files changed, 616 insertions(+), 20 deletions(-) create mode 100644 src/client/theme-default/nprogress/index.css create mode 100644 src/client/theme-default/nprogress/index.d.ts create mode 100644 src/client/theme-default/nprogress/index.js delete mode 100644 src/client/theme-default/styles/lib-override/nprogress.css diff --git a/docs/guide/theme-introduction.md b/docs/guide/theme-introduction.md index 4d125a2f..906009de 100644 --- a/docs/guide/theme-introduction.md +++ b/docs/guide/theme-introduction.md @@ -109,9 +109,10 @@ import DefaultTheme from 'vitepress/theme' export default { ...DefaultTheme, - enhanceApp({ app }) { + enhanceApp(ctx) { + DefaultTheme.enhanceApp(ctx) // register global components - app.component('MyGlobalComponent', /* ... */) + ctx.app.component('MyGlobalComponent', /* ... */) } } ``` diff --git a/docs/guide/using-vue.md b/docs/guide/using-vue.md index 0ae94833..16a4d237 100644 --- a/docs/guide/using-vue.md +++ b/docs/guide/using-vue.md @@ -113,8 +113,9 @@ import DefaultTheme from 'vitepress/theme' export default { ...DefaultTheme, - enhanceApp({ app }) { - app.component('VueClickAwayExample', VueClickAwayExample) + enhanceApp(ctx) { + DefaultTheme.enhanceApp(ctx) + ctx.app.component('VueClickAwayExample', VueClickAwayExample) } } ``` diff --git a/src/client/theme-default/components/VPNavBar.vue b/src/client/theme-default/components/VPNavBar.vue index abfbba49..a14ac73c 100644 --- a/src/client/theme-default/components/VPNavBar.vue +++ b/src/client/theme-default/components/VPNavBar.vue @@ -70,8 +70,8 @@ const { hasSidebar } = useSidebar() } .VPNavBar.has-sidebar .content { - margin-right: -32px; - padding-right: 32px; + margin-right: -100vw; + padding-right: 100vw; -webkit-backdrop-filter: saturate(50%) blur(8px); backdrop-filter: saturate(50%) blur(8px); background: rgba(255, 255, 255, 0.7); diff --git a/src/client/theme-default/index.ts b/src/client/theme-default/index.ts index 7b792128..cbc70f32 100644 --- a/src/client/theme-default/index.ts +++ b/src/client/theme-default/index.ts @@ -6,8 +6,10 @@ import './styles/components/custom-block.css' import './styles/components/vp-code.css' import './styles/components/vp-doc.css' import './styles/components/vp-sponsor.css' +import './nprogress/index.css' -import { Theme } from 'vitepress' +import { inBrowser, Theme } from 'vitepress' +import nprogress from './nprogress/index.js' import Layout from './Layout.vue' import NotFound from './NotFound.vue' @@ -22,7 +24,28 @@ export { default as VPTeamMembers } from './components/VPTeamMembers.vue' const theme: Theme = { Layout, - NotFound + NotFound, + enhanceApp: ({ router }) => { + if (inBrowser) { + let timeoutId: NodeJS.Timeout + let called = false + router.onBeforeRouteChange = () => { + timeoutId = setTimeout(() => { + nprogress.start() + called = true + }, 500) + } + router.onAfterRouteChanged = () => { + if (timeoutId) { + clearTimeout(timeoutId) + } + if (called) { + nprogress.done(true) + called = false + } + } + } + } } export default theme diff --git a/src/client/theme-default/nprogress/index.css b/src/client/theme-default/nprogress/index.css new file mode 100644 index 00000000..0003245d --- /dev/null +++ b/src/client/theme-default/nprogress/index.css @@ -0,0 +1,31 @@ +/* Make clicks pass-through */ +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: var(--vp-c-brand); + + position: fixed; + z-index: 1031; + top: 0; + left: 0; + + width: 100%; + height: 2px; +} + +/* Fancy blur effect */ +#nprogress .peg { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px var(--vp-c-brand), 0 0 5px var(--vp-c-brand); + opacity: 1; + + -webkit-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} diff --git a/src/client/theme-default/nprogress/index.d.ts b/src/client/theme-default/nprogress/index.d.ts new file mode 100644 index 00000000..e196b228 --- /dev/null +++ b/src/client/theme-default/nprogress/index.d.ts @@ -0,0 +1,45 @@ +// Type definitions for NProgress 0.2 +// Project: https://github.com/rstacruz/nprogress +// Definitions by: Judah Gabriel Himango , Ovyerus +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.1 + +declare namespace nProgress { + interface NProgressOptions { + minimum: number + template: string + easing: string + speed: number + trickle: boolean + trickleSpeed: number + showSpinner: boolean + parent: string + positionUsing: string + barSelector: string + spinnerSelector: string + } + + interface NProgress { + version: string + settings: NProgressOptions + status: number | null + + configure(options: Partial): NProgress + set(number: number): NProgress + isStarted(): boolean + start(): NProgress + done(force?: boolean): NProgress + inc(amount?: number): NProgress + trickle(): NProgress + + /* Internal */ + + render(fromStart?: boolean): HTMLDivElement + remove(): void + isRendered(): boolean + getPositioningCSS(): 'translate3d' | 'translate' | 'margin' + } +} + +declare const nProgress: nProgress.NProgress +export default nProgress diff --git a/src/client/theme-default/nprogress/index.js b/src/client/theme-default/nprogress/index.js new file mode 100644 index 00000000..28f262fe --- /dev/null +++ b/src/client/theme-default/nprogress/index.js @@ -0,0 +1,505 @@ +/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT */ + +var NProgress = {} + +NProgress.version = '0.2.0' + +var Settings = (NProgress.settings = { + minimum: 0.08, + easing: 'linear', + positionUsing: '', + speed: 200, + trickle: true, + trickleSpeed: 200, + showSpinner: true, + barSelector: '[role="bar"]', + spinnerSelector: '[role="spinner"]', + parent: 'body', + template: + '
' +}) + +/** + * Updates configuration. + * + * NProgress.configure({ + * minimum: 0.1 + * }); + */ +NProgress.configure = function (options) { + var key, value + for (key in options) { + value = options[key] + if (value !== undefined && options.hasOwnProperty(key)) + Settings[key] = value + } + + return this +} + +/** + * Last number. + */ + +NProgress.status = null + +/** + * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`. + * + * NProgress.set(0.4); + * NProgress.set(1.0); + */ + +NProgress.set = function (n) { + var started = NProgress.isStarted() + + n = clamp(n, Settings.minimum, 1) + NProgress.status = n === 1 ? null : n + + var progress = NProgress.render(!started), + bar = progress.querySelector(Settings.barSelector), + speed = Settings.speed, + ease = Settings.easing + + progress.offsetWidth /* Repaint */ + + queue(function (next) { + // Set positionUsing if it hasn't already been set + if (Settings.positionUsing === '') + Settings.positionUsing = NProgress.getPositioningCSS() + + // Add transition + css(bar, barPositionCSS(n, speed, ease)) + + if (n === 1) { + // Fade out + css(progress, { + transition: 'none', + opacity: 1 + }) + progress.offsetWidth /* Repaint */ + + setTimeout(function () { + css(progress, { + transition: 'all ' + speed + 'ms linear', + opacity: 0 + }) + setTimeout(function () { + NProgress.remove() + next() + }, speed) + }, speed) + } else { + setTimeout(next, speed) + } + }) + + return this +} + +NProgress.isStarted = function () { + return typeof NProgress.status === 'number' +} + +/** + * Shows the progress bar. + * This is the same as setting the status to 0%, except that it doesn't go backwards. + * + * NProgress.start(); + * + */ +NProgress.start = function () { + if (!NProgress.status) NProgress.set(0) + + var work = function () { + setTimeout(function () { + if (!NProgress.status) return + NProgress.trickle() + work() + }, Settings.trickleSpeed) + } + + if (Settings.trickle) work() + + return this +} + +/** + * Hides the progress bar. + * This is the *sort of* the same as setting the status to 100%, with the + * difference being `done()` makes some placebo effect of some realistic motion. + * + * NProgress.done(); + * + * If `true` is passed, it will show the progress bar even if its hidden. + * + * NProgress.done(true); + */ + +NProgress.done = function (force) { + if (!force && !NProgress.status) return this + + return NProgress.inc(0.3 + 0.5 * Math.random()).set(1) +} + +/** + * Increments by a random amount. + */ + +NProgress.inc = function (amount) { + var n = NProgress.status + + if (!n) { + return NProgress.start() + } else if (n > 1) { + return + } else { + if (typeof amount !== 'number') { + if (n >= 0 && n < 0.2) { + amount = 0.1 + } else if (n >= 0.2 && n < 0.5) { + amount = 0.04 + } else if (n >= 0.5 && n < 0.8) { + amount = 0.02 + } else if (n >= 0.8 && n < 0.99) { + amount = 0.005 + } else { + amount = 0 + } + } + + n = clamp(n + amount, 0, 0.994) + return NProgress.set(n) + } +} + +NProgress.trickle = function () { + return NProgress.inc() +} + +/** + * Waits for all supplied jQuery promises and + * increases the progress as the promises resolve. + * + * @param $promise jQUery Promise + */ +;(function () { + var initial = 0, + current = 0 + + NProgress.promise = function ($promise) { + if (!$promise || $promise.state() === 'resolved') { + return this + } + + if (current === 0) { + NProgress.start() + } + + initial++ + current++ + + $promise.always(function () { + current-- + if (current === 0) { + initial = 0 + NProgress.done() + } else { + NProgress.set((initial - current) / initial) + } + }) + + return this + } +})() + +/** + * (Internal) renders the progress bar markup based on the `template` + * setting. + */ + +NProgress.render = function (fromStart) { + if (NProgress.isRendered()) return document.getElementById('nprogress') + + addClass(document.documentElement, 'nprogress-busy') + + var progress = document.createElement('div') + progress.id = 'nprogress' + progress.innerHTML = Settings.template + + var bar = progress.querySelector(Settings.barSelector), + perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0), + parent = isDOM(Settings.parent) + ? Settings.parent + : document.querySelector(Settings.parent), + spinner + + css(bar, { + transition: 'all 0 linear', + transform: 'translate3d(' + perc + '%,0,0)' + }) + + if (!Settings.showSpinner) { + spinner = progress.querySelector(Settings.spinnerSelector) + spinner && removeElement(spinner) + } + + if (parent != document.body) { + addClass(parent, 'nprogress-custom-parent') + } + + parent.appendChild(progress) + return progress +} + +/** + * Removes the element. Opposite of render(). + */ + +NProgress.remove = function () { + removeClass(document.documentElement, 'nprogress-busy') + var parent = isDOM(Settings.parent) + ? Settings.parent + : document.querySelector(Settings.parent) + removeClass(parent, 'nprogress-custom-parent') + var progress = document.getElementById('nprogress') + progress && removeElement(progress) +} + +/** + * Checks if the progress bar is rendered. + */ + +NProgress.isRendered = function () { + return !!document.getElementById('nprogress') +} + +/** + * Determine which positioning CSS rule to use. + */ + +NProgress.getPositioningCSS = function () { + // Sniff on document.body.style + var bodyStyle = document.body.style + + // Sniff prefixes + var vendorPrefix = + 'WebkitTransform' in bodyStyle + ? 'Webkit' + : 'MozTransform' in bodyStyle + ? 'Moz' + : 'msTransform' in bodyStyle + ? 'ms' + : 'OTransform' in bodyStyle + ? 'O' + : '' + + if (vendorPrefix + 'Perspective' in bodyStyle) { + // Modern browsers with 3D support, e.g. Webkit, IE10 + return 'translate3d' + } else if (vendorPrefix + 'Transform' in bodyStyle) { + // Browsers without 3D support, e.g. IE9 + return 'translate' + } else { + // Browsers without translate() support, e.g. IE7-8 + return 'margin' + } +} + +/** + * Helpers + */ + +function isDOM(obj) { + if (typeof HTMLElement === 'object') { + return obj instanceof HTMLElement + } + return ( + obj && + typeof obj === 'object' && + obj.nodeType === 1 && + typeof obj.nodeName === 'string' + ) +} + +function clamp(n, min, max) { + if (n < min) return min + if (n > max) return max + return n +} + +/** + * (Internal) converts a percentage (`0..1`) to a bar translateX + * percentage (`-100%..0%`). + */ + +function toBarPerc(n) { + return (-1 + n) * 100 +} + +/** + * (Internal) returns the correct CSS for changing the bar's + * position given an n percentage, and speed and ease from Settings + */ + +function barPositionCSS(n, speed, ease) { + var barCSS + + if (Settings.positionUsing === 'translate3d') { + barCSS = { transform: 'translate3d(' + toBarPerc(n) + '%,0,0)' } + } else if (Settings.positionUsing === 'translate') { + barCSS = { transform: 'translate(' + toBarPerc(n) + '%,0)' } + } else { + barCSS = { 'margin-left': toBarPerc(n) + '%' } + } + + barCSS.transition = 'all ' + speed + 'ms ' + ease + + return barCSS +} + +/** + * (Internal) Queues a function to be executed. + */ + +var queue = (function () { + var pending = [] + + function next() { + var fn = pending.shift() + if (fn) { + fn(next) + } + } + + return function (fn) { + pending.push(fn) + if (pending.length == 1) next() + } +})() + +/** + * (Internal) Applies css properties to an element, similar to the jQuery + * css method. + * + * While this helper does assist with vendor prefixed property names, it + * does not perform any manipulation of values prior to setting styles. + */ + +var css = (function () { + var cssPrefixes = ['Webkit', 'O', 'Moz', 'ms'], + cssProps = {} + + function camelCase(string) { + return string + .replace(/^-ms-/, 'ms-') + .replace(/-([\da-z])/gi, function (match, letter) { + return letter.toUpperCase() + }) + } + + function getVendorProp(name) { + var style = document.body.style + if (name in style) return name + + var i = cssPrefixes.length, + capName = name.charAt(0).toUpperCase() + name.slice(1), + vendorName + while (i--) { + vendorName = cssPrefixes[i] + capName + if (vendorName in style) return vendorName + } + + return name + } + + function getStyleProp(name) { + name = camelCase(name) + return cssProps[name] || (cssProps[name] = getVendorProp(name)) + } + + function applyCss(element, prop, value) { + prop = getStyleProp(prop) + element.style[prop] = value + } + + return function (element, properties) { + var args = arguments, + prop, + value + + if (args.length == 2) { + for (prop in properties) { + value = properties[prop] + if (value !== undefined && properties.hasOwnProperty(prop)) + applyCss(element, prop, value) + } + } else { + applyCss(element, args[1], args[2]) + } + } +})() + +/** + * (Internal) Determines if an element or space separated list of class names contains a class name. + */ + +function hasClass(element, name) { + var list = typeof element == 'string' ? element : classList(element) + return list.indexOf(' ' + name + ' ') >= 0 +} + +/** + * (Internal) Adds a class to an element. + */ + +function addClass(element, name) { + var oldList = classList(element), + newList = oldList + name + + if (hasClass(oldList, name)) return + + // Trim the opening space. + element.className = newList.substring(1) +} + +/** + * (Internal) Removes a class from an element. + */ + +function removeClass(element, name) { + var oldList = classList(element), + newList + + if (!hasClass(element, name)) return + + // Replace the class name. + newList = oldList.replace(' ' + name + ' ', ' ') + + // Trim the opening and closing spaces. + element.className = newList.substring(1, newList.length - 1) +} + +/** + * (Internal) Gets a space separated list of the class names on the element. + * The list is wrapped with a single space on each end to facilitate finding + * matches within the list. + */ + +function classList(element) { + return (' ' + ((element && element.className) || '') + ' ').replace( + /\s+/gi, + ' ' + ) +} + +/** + * (Internal) Removes an element from the DOM. + */ + +function removeElement(element) { + element && element.parentNode && element.parentNode.removeChild(element) +} + +export default NProgress diff --git a/src/client/theme-default/styles/lib-override/nprogress.css b/src/client/theme-default/styles/lib-override/nprogress.css deleted file mode 100644 index 477703ce..00000000 --- a/src/client/theme-default/styles/lib-override/nprogress.css +++ /dev/null @@ -1,12 +0,0 @@ -#nprogress .bar { - background: var(--vp-c-brand); -} - -#nprogress .spinner-icon { - border-top-color: var(--vp-c-brand); - border-left-color: var(--vp-c-brand); -} - -#nprogress .peg { - box-shadow: 0 0 10px var(--vp-c-brand), 0 0 5px var(--vp-c-brand); -} diff --git a/theme.d.ts b/theme.d.ts index 553f02a0..1848e45b 100644 --- a/theme.d.ts +++ b/theme.d.ts @@ -1,5 +1,6 @@ // so that users can do `import DefaultTheme from 'vitepress/theme'` import type { DefineComponent } from 'vue' +import { EnhanceAppContext } from './dist/client/index.js' // TODO: add props for these export const VPHomeHero: DefineComponent @@ -14,6 +15,7 @@ export const VPTeamMembers: DefineComponent declare const theme: { Layout: DefineComponent NotFound: DefineComponent + enhanceApp: (ctx: EnhanceAppContext) => void } export default theme