mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-28 08:42:47 -04:00
Added support for instant navigation progress indicator
This commit is contained in:
@@ -39,6 +39,7 @@ export type Flag =
|
||||
| "navigation.expand" /* Automatic expansion */
|
||||
| "navigation.indexes" /* Section pages */
|
||||
| "navigation.instant" /* Instant navigation */
|
||||
| "navigation.instant.progress" /* Instant navigation progress */
|
||||
| "navigation.sections" /* Section navigation */
|
||||
| "navigation.tabs" /* Tabs navigation */
|
||||
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */
|
||||
|
||||
@@ -21,17 +21,24 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
EMPTY,
|
||||
Observable,
|
||||
catchError,
|
||||
from,
|
||||
Subject,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
throwError
|
||||
switchMap
|
||||
} from "rxjs"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
progress$?: Subject<number> // Progress subject
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -48,16 +55,46 @@ import {
|
||||
* @returns Response observable
|
||||
*/
|
||||
export function request(
|
||||
url: URL | string, options: RequestInit = { credentials: "same-origin" }
|
||||
): Observable<Response> {
|
||||
return from(fetch(`${url}`, options))
|
||||
.pipe(
|
||||
catchError(() => EMPTY),
|
||||
switchMap(res => res.status !== 200
|
||||
? throwError(() => new Error(res.statusText))
|
||||
: of(res)
|
||||
)
|
||||
)
|
||||
url: URL | string, options?: Options
|
||||
): Observable<Blob> {
|
||||
return new Observable<Blob>(observer => {
|
||||
const req = new XMLHttpRequest()
|
||||
req.open("GET", `${url}`)
|
||||
req.responseType = "blob"
|
||||
|
||||
// Handle response
|
||||
req.addEventListener("load", () => {
|
||||
if (req.status >= 200 && req.status < 300) {
|
||||
observer.next(req.response)
|
||||
observer.complete()
|
||||
} else {
|
||||
observer.error(new Error(req.statusText))
|
||||
}
|
||||
})
|
||||
|
||||
// Handle network errors
|
||||
req.addEventListener("error", () => {
|
||||
observer.error(new Error("Network Error"))
|
||||
})
|
||||
|
||||
// Handle aborted requests
|
||||
req.addEventListener("abort", () => {
|
||||
observer.error(new Error("Request aborted"))
|
||||
})
|
||||
|
||||
// Handle download progress
|
||||
if (typeof options?.progress$ !== "undefined") {
|
||||
req.addEventListener("progress", event => {
|
||||
options.progress$!.next((event.loaded / event.total) * 100)
|
||||
})
|
||||
|
||||
// Immediately set progress to 5% to indicate that we're loading
|
||||
options.progress$.next(5)
|
||||
}
|
||||
|
||||
// Send request
|
||||
req.send()
|
||||
})
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@@ -73,11 +110,12 @@ export function request(
|
||||
* @returns Data observable
|
||||
*/
|
||||
export function requestJSON<T>(
|
||||
url: URL | string, options?: RequestInit
|
||||
url: URL | string, options?: Options
|
||||
): Observable<T> {
|
||||
return request(url, options)
|
||||
.pipe(
|
||||
switchMap(res => res.json()),
|
||||
switchMap(res => res.text()),
|
||||
map(body => JSON.parse(body) as T),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
@@ -91,7 +129,7 @@ export function requestJSON<T>(
|
||||
* @returns Data observable
|
||||
*/
|
||||
export function requestXML(
|
||||
url: URL | string, options?: RequestInit
|
||||
url: URL | string, options?: Options
|
||||
): Observable<Document> {
|
||||
const dom = new DOMParser()
|
||||
return request(url, options)
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
mountHeader,
|
||||
mountHeaderTitle,
|
||||
mountPalette,
|
||||
mountProgress,
|
||||
mountSearch,
|
||||
mountSearchHiglight,
|
||||
mountSidebar,
|
||||
@@ -143,9 +144,12 @@ const index$ = document.forms.namedItem("search")
|
||||
const alert$ = new Subject<string>()
|
||||
setupClipboardJS({ alert$ })
|
||||
|
||||
/* Set up progress indicator */
|
||||
const progress$ = new Subject<number>()
|
||||
|
||||
/* Set up instant navigation, if enabled */
|
||||
if (feature("navigation.instant"))
|
||||
setupInstantNavigation({ location$, viewport$ })
|
||||
setupInstantNavigation({ location$, viewport$, progress$ })
|
||||
.subscribe(document$)
|
||||
|
||||
/* Set up version selector */
|
||||
@@ -227,6 +231,10 @@ const control$ = merge(
|
||||
...getComponentElements("palette")
|
||||
.map(el => mountPalette(el)),
|
||||
|
||||
/* Progress bar */
|
||||
...getComponentElements("progress")
|
||||
.map(el => mountProgress(el, { progress$ })),
|
||||
|
||||
/* Search */
|
||||
...getComponentElements("search")
|
||||
.map(el => mountSearch(el, { index$, keyboard$ })),
|
||||
@@ -304,4 +312,5 @@ window.tablet$ = tablet$ /* Media tablet observable */
|
||||
window.screen$ = screen$ /* Media screen observable */
|
||||
window.print$ = print$ /* Media print observable */
|
||||
window.alert$ = alert$ /* Alert subject */
|
||||
window.progress$ = progress$ /* Progress indicator subject */
|
||||
window.component$ = component$ /* Component observable */
|
||||
|
||||
@@ -41,6 +41,7 @@ export type ComponentType =
|
||||
| "main" /* Main area */
|
||||
| "outdated" /* Version warning */
|
||||
| "palette" /* Color palette */
|
||||
| "progress" /* Progress indicator */
|
||||
| "search" /* Search */
|
||||
| "search-query" /* Search input */
|
||||
| "search-result" /* Search results */
|
||||
@@ -86,6 +87,7 @@ interface ComponentTypeMap {
|
||||
"main": HTMLElement /* Main area */
|
||||
"outdated": HTMLElement /* Version warning */
|
||||
"palette": HTMLElement /* Color palette */
|
||||
"progress": HTMLElement /* Progress indicator */
|
||||
"search": HTMLElement /* Search */
|
||||
"search-query": HTMLInputElement /* Search input */
|
||||
"search-result": HTMLElement /* Search results */
|
||||
|
||||
@@ -28,6 +28,7 @@ export * from "./dialog"
|
||||
export * from "./header"
|
||||
export * from "./main"
|
||||
export * from "./palette"
|
||||
export * from "./progress"
|
||||
export * from "./search"
|
||||
export * from "./sidebar"
|
||||
export * from "./source"
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
defer,
|
||||
finalize,
|
||||
map,
|
||||
tap
|
||||
} from "rxjs"
|
||||
|
||||
import { Component } from "../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Progress indicator
|
||||
*/
|
||||
export interface Progress {
|
||||
value: number // Progress value
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
progress$: Subject<number> // Progress subject
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount progress indicator
|
||||
*
|
||||
* @param el - Progress indicator element
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Progress indicator component observable
|
||||
*/
|
||||
export function mountProgress(
|
||||
el: HTMLElement, { progress$ }: MountOptions
|
||||
): Observable<Component<Progress>> {
|
||||
|
||||
// Mount component on subscription
|
||||
return defer(() => {
|
||||
const push$ = new Subject<Progress>()
|
||||
push$.subscribe(({ value }) => {
|
||||
el.style.setProperty("--md-progress-value", `${value}`)
|
||||
})
|
||||
|
||||
// Create and return component
|
||||
return progress$
|
||||
.pipe(
|
||||
tap(value => push$.next({ value })),
|
||||
finalize(() => push$.complete()),
|
||||
map(value => ({ ref: el, value }))
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -67,8 +67,9 @@ import { fetchSitemap } from "../sitemap"
|
||||
* Setup options
|
||||
*/
|
||||
interface SetupOptions {
|
||||
location$: Subject<URL> /* Location subject */
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
location$: Subject<URL> // Location subject
|
||||
viewport$: Observable<Viewport> // Viewport observable
|
||||
progress$: Subject<number> // Progress suject
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -138,7 +139,7 @@ function lookup(head: HTMLHeadElement): Map<string, HTMLElement> {
|
||||
* @returns Document observable
|
||||
*/
|
||||
export function setupInstantNavigation(
|
||||
{ location$, viewport$ }: SetupOptions
|
||||
{ location$, viewport$, progress$ }: SetupOptions
|
||||
): Observable<Document> {
|
||||
const config = configuration()
|
||||
if (location.protocol === "file:")
|
||||
@@ -260,7 +261,7 @@ export function setupInstantNavigation(
|
||||
startWith(getLocation()),
|
||||
distinctUntilKeyChanged("pathname"),
|
||||
skip(1),
|
||||
switchMap(url => request(url)
|
||||
switchMap(url => request(url, { progress$ })
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
setLocation(url, true)
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
@import "main/components/nav";
|
||||
@import "main/components/pagination";
|
||||
@import "main/components/post";
|
||||
@import "main/components/progress";
|
||||
@import "main/components/search";
|
||||
@import "main/components/select";
|
||||
@import "main/components/sidebar";
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
////
|
||||
/// Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
///
|
||||
/// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
/// copy of this software and associated documentation files (the "Software"),
|
||||
/// to deal in the Software without restriction, including without limitation
|
||||
/// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
/// and/or sell copies of the Software, and to permit persons to whom the
|
||||
/// Software is furnished to do so, subject to the following conditions:
|
||||
///
|
||||
/// The above copyright notice and this permission notice shall be included in
|
||||
/// all copies or substantial portions of the Software.
|
||||
///
|
||||
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
|
||||
/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
/// DEALINGS
|
||||
////
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rules
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Progress variables
|
||||
:root {
|
||||
--md-progress-value: 0;
|
||||
--md-progress-delay: 400ms;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Progress indicator
|
||||
.md-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
width: 100%;
|
||||
height: px2rem(1.5px);
|
||||
background: var(--md-primary-bg-color);
|
||||
opacity:
|
||||
min(
|
||||
clamp(0, var(--md-progress-value), 1),
|
||||
clamp(0, 100 - var(--md-progress-value), 1)
|
||||
);
|
||||
transition:
|
||||
transform 500ms cubic-bezier(0.19, 1, 0.22, 1),
|
||||
opacity 250ms var(--md-progress-delay);
|
||||
transform: scaleX(calc(var(--md-progress-value) * 1%));
|
||||
transform-origin: left;
|
||||
}
|
||||
@@ -26,10 +26,8 @@
|
||||
|
||||
// Tasklist variables
|
||||
:root {
|
||||
--md-tasklist-icon:
|
||||
svg-load("octicons/check-circle-fill-24.svg");
|
||||
--md-tasklist-icon--checked:
|
||||
svg-load("octicons/check-circle-fill-24.svg");
|
||||
--md-tasklist-icon: svg-load("octicons/check-circle-fill-24.svg");
|
||||
--md-tasklist-icon--checked: svg-load("octicons/check-circle-fill-24.svg");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -379,6 +379,11 @@
|
||||
<div class="md-dialog__inner md-typeset"></div>
|
||||
</div>
|
||||
|
||||
<!-- Progress indicator -->
|
||||
{% if "navigation.instant.progress" in features %}
|
||||
{% include "partials/progress.html" %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Consent -->
|
||||
{% if config.extra.consent %}
|
||||
<div class="md-consent" data-md-component="consent" id="__consent" hidden>
|
||||
|
||||
24
src/templates/partials/progress.html
Normal file
24
src/templates/partials/progress.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
Copyright (c) 2016-2023 Martin Donath <martin.donath@squidfunk.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
-->
|
||||
|
||||
<!-- Progress indicator -->
|
||||
<div class="md-progress" data-md-component="progress" role="progressbar"></div>
|
||||
Reference in New Issue
Block a user