1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2024-11-24 07:30:12 +01:00

Refactored instant loading setup

This commit is contained in:
squidfunk 2020-03-28 17:40:46 +01:00
parent 4d370fe903
commit edc9d6fc61
11 changed files with 114 additions and 116 deletions

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

@ -1,6 +1,6 @@
{
"assets/javascripts/bundle.js": "assets/javascripts/bundle.630a9b34.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.630a9b34.min.js.map",
"assets/javascripts/bundle.js": "assets/javascripts/bundle.edc2ff56.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.edc2ff56.min.js.map",
"assets/javascripts/vendor.js": "assets/javascripts/vendor.c1fcc1cc.min.js",
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.c1fcc1cc.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.3bc815f0.min.js",

View File

@ -175,7 +175,7 @@
</div>
{% block scripts %}
<script src="{{ 'assets/javascripts/vendor.c1fcc1cc.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.630a9b34.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.edc2ff56.min.js' | url }}"></script>
{%- set translations = {} -%}
{%- for key in [
"clipboard.copy",

View File

@ -49,6 +49,7 @@ export function getLocationHash(): string {
export function setLocationHash(hash: string): void {
const el = document.createElement("a")
el.href = hash
el.addEventListener("click", ev => ev.stopPropagation())
el.click()
}

View File

@ -20,7 +20,8 @@
* IN THE SOFTWARE.
*/
// TODO: remove this after we finished refactoring
// DISCLAIMER: this file is still WIP. There're some refactoring opportunities
// which must be tackled after we gathered some feedback on v5.
// tslint:disable
import "../stylesheets/main.scss"
@ -32,8 +33,6 @@ import {
combineLatest,
animationFrameScheduler,
fromEvent,
of,
NEVER,
from
} from "rxjs"
import { ajax } from "rxjs/ajax"
@ -46,9 +45,7 @@ import {
observeOn,
take,
shareReplay,
share,
pluck,
skip
pluck
} from "rxjs/operators"
import {
@ -61,7 +58,6 @@ import {
watchLocationHash,
watchViewport,
isLocalLocation,
isAnchorLocation,
setLocationHash,
watchLocationBase
} from "browser"
@ -255,7 +251,6 @@ export function initialize(config: unknown) {
)
)
const worker = setupSearchWorker(config.search.worker, {
base$, index$
})
@ -299,10 +294,9 @@ export function initialize(config: unknown) {
tap(() => setToggle("search", false)),
delay(125), // ensure that it runs after the body scroll reset...
)
.subscribe(hash => setLocationHash(`#${hash}`)) // TODO: must be unified
.subscribe(hash => setLocationHash(`#${hash}`))
// Scroll lock // document -> document$ => { body } !?
// put into search...
// TODO: scroll restoration must be centralized
combineLatest([
watchToggle("search"),
tablet$,
@ -313,7 +307,7 @@ export function initialize(config: unknown) {
const active = toggle && !tablet
return document$
.pipe(
delay(active ? 400 : 100), // TOOD: directly combine this with the hash!
delay(active ? 400 : 100),
observeOn(animationFrameScheduler),
tap(({ body }) => active
? setScrollLock(body, y)
@ -326,62 +320,34 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */
/* Intercept internal link clicks */
const link$ = fromEvent<MouseEvent>(document.body, "click")
/* Always close drawer on click */
fromEvent<MouseEvent>(document.body, "click")
.pipe(
filter(ev => !(ev.metaKey || ev.ctrlKey)),
switchMap(ev => {
filter(ev => {
if (ev.target instanceof HTMLElement) {
const el = ev.target.closest("a") // TODO: abstract as link click?
if (el && isLocalLocation(el)) {
if (!isAnchorLocation(el) && config.features.includes("instant"))
ev.preventDefault()
return of(el)
return true
}
}
return NEVER
}),
share()
return false
})
)
/* Always close drawer on click */
link$.subscribe(() => {
.subscribe(() => {
setToggle("drawer", false)
})
/* Hack: ensure that page loads restore scroll offset */
fromEvent(window, "beforeunload")
.subscribe(() => {
history.scrollRestoration = "auto"
})
// instant loading
if (config.features.includes("instant")) {
/* Disable automatic scroll restoration, as it doesn't work nicely */
if ("scrollRestoration" in history)
history.scrollRestoration = "manual"
/* Resolve relative links for stability */
for (const selector of [
`link[rel="shortcut icon"]`,
// `link[rel="stylesheet"]` // reduce style computations
])
for (const el of getElements<HTMLLinkElement>(selector))
el.href = el.href
setupInstantLoading({
document$, link$, location$, viewport$
})
}
/* Enable instant loading, if not on file:// protocol */
if (config.features.includes("instant") && location.protocol !== "file:")
setupInstantLoading({ document$, location$, viewport$ })
/* ----------------------------------------------------------------------- */
// if we use a single tab outside of search, unhide all permalinks.
// TODO: experimental. necessary!?
/* Unhide permalinks on first tab */
keyboard$
.pipe(
filter(key => key.mode === "global" && ["Tab"].includes(key.type)),
filter(key => key.mode === "global" && key.type === "Tab"),
take(1)
)
.subscribe(() => {
@ -395,6 +361,7 @@ export function initialize(config: unknown) {
/* Browser observables */
document$,
location$,
viewport$,
/* Component observables */
@ -406,7 +373,7 @@ export function initialize(config: unknown) {
tabs$,
toc$,
/* Integation observables */
/* Integration observables */
clipboard$,
keyboard$,
dialog$

View File

@ -25,7 +25,7 @@ import { NEVER, Observable, Subject, fromEventPattern } from "rxjs"
import { mapTo, share, tap } from "rxjs/operators"
import { getElements } from "browser"
import { renderClipboard } from "templates"
import { renderClipboardButton } from "templates"
import { translate } from "utilities"
/* ----------------------------------------------------------------------------
@ -66,7 +66,7 @@ export function setupClipboard(
blocks.forEach((block, index) => {
const parent = block.parentElement!
parent.id = `__code_${index}`
parent.insertBefore(renderClipboard(parent.id), block)
parent.insertBefore(renderClipboardButton(parent.id), block)
})
})

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { NEVER, Observable, Subject, fromEvent, merge } from "rxjs"
import { NEVER, Observable, Subject, fromEvent, merge, of } from "rxjs"
import { ajax } from "rxjs//ajax"
import {
bufferCount,
@ -43,9 +43,11 @@ import {
ViewportOffset,
getElement,
isAnchorLocation,
isLocalLocation,
replaceElement,
setLocation,
setLocationHash,
setToggle,
setViewportOffset
} from "browser"
@ -68,9 +70,8 @@ interface State {
*/
interface SetupOptions {
document$: Subject<Document> /* Document subject */
viewport$: Observable<Viewport> /* Viewport observable */
link$: Observable<HTMLAnchorElement> /* Internal link observable */
location$: Subject<URL> /* Location subject */
viewport$: Observable<Viewport> /* Viewport observable */
}
/* ----------------------------------------------------------------------------
@ -97,19 +98,51 @@ interface SetupOptions {
* location change is dispatched regularly.
*
* @param options - Options
*
* @return TODO: return type?
*/
export function setupInstantLoading(
{ document$, viewport$, link$, location$ }: SetupOptions
) { // TODO: add return type
const state$ = link$
{ document$, viewport$, location$ }: SetupOptions
): void {
/* Disable automatic scroll restoration */
if ("scrollRestoration" in history)
history.scrollRestoration = "manual"
/* Hack: ensure that reloads restore viewport offset */
fromEvent(window, "beforeunload")
.subscribe(() => {
history.scrollRestoration = "auto"
})
/* Hack: ensure absolute favicon link to omit 404s on document switch */
const favicon = getElement<HTMLLinkElement>(`link[rel="shortcut icon"]`)
if (typeof favicon !== "undefined")
favicon.href = favicon.href // tslint:disable-line no-self-assignment
/* Intercept link clicks and convert to state change */
const state$ = fromEvent<MouseEvent>(document.body, "click")
.pipe(
filter(ev => !(ev.metaKey || ev.ctrlKey)),
switchMap(ev => {
if (ev.target instanceof HTMLElement) {
const el = ev.target.closest("a")
if (el && isLocalLocation(el)) {
if (!isAnchorLocation(el))
ev.preventDefault()
return of(el)
}
}
return NEVER
}),
map(el => ({ url: new URL(el.href) })),
share<State>()
)
/* Intercept internal links to dispatch */
/* Always close search on link click */
state$.subscribe(() => {
setToggle("search", false)
})
/* Filter state changes to dispatch */
const push$ = state$
.pipe(
distinctUntilChanged((prev, next) => prev.url.href === next.url.href),
@ -121,11 +154,11 @@ export function setupInstantLoading(
const pop$ = fromEvent<PopStateEvent>(window, "popstate")
.pipe(
filter(ev => ev.state !== null),
map<PopStateEvent, State>(ev => ({
map(ev => ({
url: new URL(location.href),
offset: ev.state
})),
share()
share<State>()
)
/* Emit location change */
@ -135,13 +168,11 @@ export function setupInstantLoading(
)
.subscribe(location$)
const dom = new DOMParser()
/* Fetch document on location change */
const ajax$ = location$
.pipe(
distinctUntilKeyChanged("pathname"),
skip(1),
/* Fetch document */
switchMap(url => ajax({
url: url.href,
responseType: "text",
@ -154,9 +185,9 @@ export function setupInstantLoading(
})
)
)
// share()
)
/* Set new location as soon as the document was fetched */
push$
.pipe(
sample(ajax$)
@ -165,55 +196,30 @@ export function setupInstantLoading(
history.pushState({}, "", url.toString())
})
/* Parse and emit document */
const dom = new DOMParser()
ajax$
.pipe(
map(({ response }) => {
return dom.parseFromString(response, "text/html")
})
map(({ response }) => dom.parseFromString(response, "text/html"))
)
.subscribe(document$)
/* History: debounce update of viewport offset */
viewport$
.pipe(
debounceTime(250),
distinctUntilKeyChanged("offset")
)
.subscribe(({ offset }) => {
history.replaceState(offset, "")
})
/* Apply viewport offset from history */
merge(state$, pop$)
.pipe(
bufferCount(2, 1),
filter(([prev, next]) => {
return prev.url.pathname === next.url.pathname
&& !isAnchorLocation(next.url)
}),
map(([, state]) => state)
)
.subscribe(({ offset }) => {
setViewportOffset(offset || { y: 0 })
})
/* Intercept actual instant loading */
/* Intercept instant loading */
const instant$ = merge(push$, pop$)
.pipe(
sample(document$)
)
// TODO: from here on, everything is beta.... ###############################
// TODO: this must be combined with search scroll restoration on mobile
instant$.subscribe(({ url, offset }) => {
if (url.hash && !offset) {
// console.log("set hash!")
setLocationHash(url.hash) // must delay, if search is open!
setLocationHash(url.hash)
} else {
setViewportOffset(offset || { y: 0 })
}
})
/* Replace document metadata */
instant$
.pipe(
withLatestFrom(document$)
@ -238,4 +244,28 @@ export function setupInstantLoading(
}
}
})
/* Debounce update of viewport offset */
viewport$
.pipe(
debounceTime(250),
distinctUntilKeyChanged("offset")
)
.subscribe(({ offset }) => {
history.replaceState(offset, "")
})
/* Set viewport offset from history */
merge(state$, pop$)
.pipe(
bufferCount(2, 1),
filter(([prev, next]) => {
return prev.url.pathname === next.url.pathname
&& !isAnchorLocation(next.url)
}),
map(([, state]) => state)
)
.subscribe(({ offset }) => {
setViewportOffset(offset || { y: 0 })
})
}

View File

@ -53,14 +53,14 @@ const path =
*
* @return Element
*/
export function renderClipboard(
export function renderClipboardButton(
id: string
) {
return (
<button
class={css.container}
title={translate("clipboard.copy")}
data-clipboard-target={`#${id} code`}
data-clipboard-target={`#${id} > code`}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d={path}></path>