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 %}
{% block scripts %}
{{ super() }}
<script src="{{ 'assets/javascripts/custom.a4bbca43.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/custom.9458f965.min.js' | url }}"></script>
{% 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 %}
{% endblock %}
{% 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 %}
{% set palette = config.theme.palette %}
<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__inner md-typeset"></div>
</div>
{% if "navigation.instant.progress" in features %}
{% include "partials/progress.html" %}
{% endif %}
{% if config.extra.consent %}
<div class="md-consent" data-md-component="consent" id="__consent" hidden>
<div class="md-consent__overlay"></div>
@ -250,7 +253,7 @@
</script>
{% endblock %}
{% 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 %}
{{ script | script_tag }}
{% 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.indexes
# - navigation.instant
# - navigation.instant.prefetch
# - navigation.instant.progress
# - navigation.prune
- navigation.sections
- navigation.tabs

View File

@ -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) */

View File

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

View File

@ -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 */

View File

@ -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 */

View File

@ -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"

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
*/
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)

View File

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

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
: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");
}
// ----------------------------------------------------------------------------

View File

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

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 print$: Observable<boolean> /* Media print observable */
var alert$: Subject<string> /* Alert subject */
var progress$: Subject<number> /* Progress indicator subject */
var component$: Observable<Component>/* Component observable */
}