Added support for instant navigation progress indicator

This commit is contained in:
squidfunk 2023-10-01 09:42:02 +02:00
parent 015685f421
commit f8fd537b97
No known key found for this signature in database
GPG Key ID: 5ED40BC4F9C436DF
28 changed files with 323 additions and 93 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,5 +23,5 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{ 'assets/javascripts/custom.a4bbca43.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/custom.9458f965.min.js' | url }}"></script>
{% endblock %} {% endblock %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -44,7 +44,7 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block styles %} {% block styles %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.1dbc8ddf.min.css' | url }}"> <link rel="stylesheet" href="{{ 'assets/stylesheets/main.79e020e9.min.css' | url }}">
{% if config.theme.palette %} {% if config.theme.palette %}
{% set palette = config.theme.palette %} {% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.a5377069.min.css' | url }}"> <link rel="stylesheet" href="{{ 'assets/stylesheets/palette.a5377069.min.css' | url }}">
@ -206,6 +206,9 @@
<div class="md-dialog" data-md-component="dialog"> <div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div> <div class="md-dialog__inner md-typeset"></div>
</div> </div>
{% if "navigation.instant.progress" in features %}
{% include "partials/progress.html" %}
{% endif %}
{% if config.extra.consent %} {% if config.extra.consent %}
<div class="md-consent" data-md-component="consent" id="__consent" hidden> <div class="md-consent" data-md-component="consent" id="__consent" hidden>
<div class="md-consent__overlay"></div> <div class="md-consent__overlay"></div>
@ -250,7 +253,7 @@
</script> </script>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ 'assets/javascripts/bundle.697ed5af.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/bundle.6eac0284.min.js' | url }}"></script>
{% for script in config.extra_javascript %} {% for script in config.extra_javascript %}
{{ script | script_tag }} {{ script | script_tag }}
{% endfor %} {% endfor %}

View File

@ -0,0 +1,4 @@
{#-
This file was automatically generated - do not edit
-#}
<div class="md-progress" data-md-component="progress" role="progressbar"></div>

View File

@ -51,6 +51,8 @@ theme:
- navigation.footer - navigation.footer
- navigation.indexes - navigation.indexes
# - navigation.instant # - navigation.instant
# - navigation.instant.prefetch
# - navigation.instant.progress
# - navigation.prune # - navigation.prune
- navigation.sections - navigation.sections
- navigation.tabs - navigation.tabs
@ -253,7 +255,7 @@ nav:
- Insiders: - Insiders:
- insiders/index.md - insiders/index.md
- Getting started: insiders/getting-started.md - Getting started: insiders/getting-started.md
- Changelog: - Changelog:
- insiders/changelog/index.md - insiders/changelog/index.md
- How to upgrade: insiders/upgrade.md - How to upgrade: insiders/upgrade.md
- Blog: - Blog:

View File

@ -39,6 +39,7 @@ export type Flag =
| "navigation.expand" /* Automatic expansion */ | "navigation.expand" /* Automatic expansion */
| "navigation.indexes" /* Section pages */ | "navigation.indexes" /* Section pages */
| "navigation.instant" /* Instant navigation */ | "navigation.instant" /* Instant navigation */
| "navigation.instant.progress" /* Instant navigation progress */
| "navigation.sections" /* Section navigation */ | "navigation.sections" /* Section navigation */
| "navigation.tabs" /* Tabs navigation */ | "navigation.tabs" /* Tabs navigation */
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */ | "navigation.tabs.sticky" /* Tabs navigation (sticky) */

View File

@ -21,17 +21,24 @@
*/ */
import { import {
EMPTY,
Observable, Observable,
catchError, Subject,
from,
map, map,
of,
shareReplay, shareReplay,
switchMap, switchMap
throwError
} from "rxjs" } from "rxjs"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
progress$?: Subject<number> // Progress subject
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -48,16 +55,46 @@ import {
* @returns Response observable * @returns Response observable
*/ */
export function request( export function request(
url: URL | string, options: RequestInit = { credentials: "same-origin" } url: URL | string, options?: Options
): Observable<Response> { ): Observable<Blob> {
return from(fetch(`${url}`, options)) return new Observable<Blob>(observer => {
.pipe( const req = new XMLHttpRequest()
catchError(() => EMPTY), req.open("GET", `${url}`)
switchMap(res => res.status !== 200 req.responseType = "blob"
? throwError(() => new Error(res.statusText))
: of(res) // 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 * @returns Data observable
*/ */
export function requestJSON<T>( export function requestJSON<T>(
url: URL | string, options?: RequestInit url: URL | string, options?: Options
): Observable<T> { ): Observable<T> {
return request(url, options) return request(url, options)
.pipe( .pipe(
switchMap(res => res.json()), switchMap(res => res.text()),
map(body => JSON.parse(body) as T),
shareReplay(1) shareReplay(1)
) )
} }
@ -91,7 +129,7 @@ export function requestJSON<T>(
* @returns Data observable * @returns Data observable
*/ */
export function requestXML( export function requestXML(
url: URL | string, options?: RequestInit url: URL | string, options?: Options
): Observable<Document> { ): Observable<Document> {
const dom = new DOMParser() const dom = new DOMParser()
return request(url, options) return request(url, options)

View File

@ -65,6 +65,7 @@ import {
mountHeader, mountHeader,
mountHeaderTitle, mountHeaderTitle,
mountPalette, mountPalette,
mountProgress,
mountSearch, mountSearch,
mountSearchHiglight, mountSearchHiglight,
mountSidebar, mountSidebar,
@ -143,9 +144,12 @@ const index$ = document.forms.namedItem("search")
const alert$ = new Subject<string>() const alert$ = new Subject<string>()
setupClipboardJS({ alert$ }) setupClipboardJS({ alert$ })
/* Set up progress indicator */
const progress$ = new Subject<number>()
/* Set up instant navigation, if enabled */ /* Set up instant navigation, if enabled */
if (feature("navigation.instant")) if (feature("navigation.instant"))
setupInstantNavigation({ location$, viewport$ }) setupInstantNavigation({ location$, viewport$, progress$ })
.subscribe(document$) .subscribe(document$)
/* Set up version selector */ /* Set up version selector */
@ -227,6 +231,10 @@ const control$ = merge(
...getComponentElements("palette") ...getComponentElements("palette")
.map(el => mountPalette(el)), .map(el => mountPalette(el)),
/* Progress bar */
...getComponentElements("progress")
.map(el => mountProgress(el, { progress$ })),
/* Search */ /* Search */
...getComponentElements("search") ...getComponentElements("search")
.map(el => mountSearch(el, { index$, keyboard$ })), .map(el => mountSearch(el, { index$, keyboard$ })),
@ -304,4 +312,5 @@ window.tablet$ = tablet$ /* Media tablet observable */
window.screen$ = screen$ /* Media screen observable */ window.screen$ = screen$ /* Media screen observable */
window.print$ = print$ /* Media print observable */ window.print$ = print$ /* Media print observable */
window.alert$ = alert$ /* Alert subject */ window.alert$ = alert$ /* Alert subject */
window.progress$ = progress$ /* Progress indicator subject */
window.component$ = component$ /* Component observable */ window.component$ = component$ /* Component observable */

View File

@ -41,6 +41,7 @@ export type ComponentType =
| "main" /* Main area */ | "main" /* Main area */
| "outdated" /* Version warning */ | "outdated" /* Version warning */
| "palette" /* Color palette */ | "palette" /* Color palette */
| "progress" /* Progress indicator */
| "search" /* Search */ | "search" /* Search */
| "search-query" /* Search input */ | "search-query" /* Search input */
| "search-result" /* Search results */ | "search-result" /* Search results */
@ -86,6 +87,7 @@ interface ComponentTypeMap {
"main": HTMLElement /* Main area */ "main": HTMLElement /* Main area */
"outdated": HTMLElement /* Version warning */ "outdated": HTMLElement /* Version warning */
"palette": HTMLElement /* Color palette */ "palette": HTMLElement /* Color palette */
"progress": HTMLElement /* Progress indicator */
"search": HTMLElement /* Search */ "search": HTMLElement /* Search */
"search-query": HTMLInputElement /* Search input */ "search-query": HTMLInputElement /* Search input */
"search-result": HTMLElement /* Search results */ "search-result": HTMLElement /* Search results */

View File

@ -28,6 +28,7 @@ export * from "./dialog"
export * from "./header" export * from "./header"
export * from "./main" export * from "./main"
export * from "./palette" export * from "./palette"
export * from "./progress"
export * from "./search" export * from "./search"
export * from "./sidebar" export * from "./sidebar"
export * from "./source" export * from "./source"

View File

@ -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 }))
)
})
}

View File

@ -67,8 +67,9 @@ import { fetchSitemap } from "../sitemap"
* Setup options * Setup options
*/ */
interface SetupOptions { interface SetupOptions {
location$: Subject<URL> /* Location subject */ location$: Subject<URL> // Location subject
viewport$: Observable<Viewport> /* Viewport observable */ viewport$: Observable<Viewport> // Viewport observable
progress$: Subject<number> // Progress suject
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -138,7 +139,7 @@ function lookup(head: HTMLHeadElement): Map<string, HTMLElement> {
* @returns Document observable * @returns Document observable
*/ */
export function setupInstantNavigation( export function setupInstantNavigation(
{ location$, viewport$ }: SetupOptions { location$, viewport$, progress$ }: SetupOptions
): Observable<Document> { ): Observable<Document> {
const config = configuration() const config = configuration()
if (location.protocol === "file:") if (location.protocol === "file:")
@ -260,7 +261,7 @@ export function setupInstantNavigation(
startWith(getLocation()), startWith(getLocation()),
distinctUntilKeyChanged("pathname"), distinctUntilKeyChanged("pathname"),
skip(1), skip(1),
switchMap(url => request(url) switchMap(url => request(url, { progress$ })
.pipe( .pipe(
catchError(() => { catchError(() => {
setLocation(url, true) setLocation(url, true)

View File

@ -56,6 +56,7 @@
@import "main/components/nav"; @import "main/components/nav";
@import "main/components/pagination"; @import "main/components/pagination";
@import "main/components/post"; @import "main/components/post";
@import "main/components/progress";
@import "main/components/search"; @import "main/components/search";
@import "main/components/select"; @import "main/components/select";
@import "main/components/sidebar"; @import "main/components/sidebar";

View File

@ -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;
}

View File

@ -26,10 +26,8 @@
// Tasklist variables // Tasklist variables
:root { :root {
--md-tasklist-icon: --md-tasklist-icon: svg-load("octicons/check-circle-fill-24.svg");
svg-load("octicons/check-circle-fill-24.svg"); --md-tasklist-icon--checked: svg-load("octicons/check-circle-fill-24.svg");
--md-tasklist-icon--checked:
svg-load("octicons/check-circle-fill-24.svg");
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@ -379,6 +379,11 @@
<div class="md-dialog__inner md-typeset"></div> <div class="md-dialog__inner md-typeset"></div>
</div> </div>
<!-- Progress indicator -->
{% if "navigation.instant.progress" in features %}
{% include "partials/progress.html" %}
{% endif %}
<!-- Consent --> <!-- Consent -->
{% if config.extra.consent %} {% if config.extra.consent %}
<div class="md-consent" data-md-component="consent" id="__consent" hidden> <div class="md-consent" data-md-component="consent" id="__consent" hidden>

View 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>

View File

@ -99,5 +99,6 @@ declare global {
var screen$: Observable<boolean> /* Media screen observable */ var screen$: Observable<boolean> /* Media screen observable */
var print$: Observable<boolean> /* Media print observable */ var print$: Observable<boolean> /* Media print observable */
var alert$: Subject<string> /* Alert subject */ var alert$: Subject<string> /* Alert subject */
var progress$: Subject<number> /* Progress indicator subject */
var component$: Observable<Component>/* Component observable */ var component$: Observable<Component>/* Component observable */
} }