1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2025-02-17 18:49:21 +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": "assets/javascripts/bundle.edc2ff56.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.630a9b34.min.js.map", "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": "assets/javascripts/vendor.c1fcc1cc.min.js",
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.c1fcc1cc.min.js.map", "assets/javascripts/vendor.js.map": "assets/javascripts/vendor.c1fcc1cc.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.3bc815f0.min.js", "assets/javascripts/worker/search.js": "assets/javascripts/worker/search.3bc815f0.min.js",

View File

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

View File

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

View File

@ -20,7 +20,8 @@
* IN THE SOFTWARE. * 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 // tslint:disable
import "../stylesheets/main.scss" import "../stylesheets/main.scss"
@ -32,8 +33,6 @@ import {
combineLatest, combineLatest,
animationFrameScheduler, animationFrameScheduler,
fromEvent, fromEvent,
of,
NEVER,
from from
} from "rxjs" } from "rxjs"
import { ajax } from "rxjs/ajax" import { ajax } from "rxjs/ajax"
@ -46,9 +45,7 @@ import {
observeOn, observeOn,
take, take,
shareReplay, shareReplay,
share, pluck
pluck,
skip
} from "rxjs/operators" } from "rxjs/operators"
import { import {
@ -61,7 +58,6 @@ import {
watchLocationHash, watchLocationHash,
watchViewport, watchViewport,
isLocalLocation, isLocalLocation,
isAnchorLocation,
setLocationHash, setLocationHash,
watchLocationBase watchLocationBase
} from "browser" } from "browser"
@ -255,7 +251,6 @@ export function initialize(config: unknown) {
) )
) )
const worker = setupSearchWorker(config.search.worker, { const worker = setupSearchWorker(config.search.worker, {
base$, index$ base$, index$
}) })
@ -299,10 +294,9 @@ export function initialize(config: unknown) {
tap(() => setToggle("search", false)), tap(() => setToggle("search", false)),
delay(125), // ensure that it runs after the body scroll reset... 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 } !? // TODO: scroll restoration must be centralized
// put into search...
combineLatest([ combineLatest([
watchToggle("search"), watchToggle("search"),
tablet$, tablet$,
@ -313,7 +307,7 @@ export function initialize(config: unknown) {
const active = toggle && !tablet const active = toggle && !tablet
return document$ return document$
.pipe( .pipe(
delay(active ? 400 : 100), // TOOD: directly combine this with the hash! delay(active ? 400 : 100),
observeOn(animationFrameScheduler), observeOn(animationFrameScheduler),
tap(({ body }) => active tap(({ body }) => active
? setScrollLock(body, y) ? setScrollLock(body, y)
@ -326,68 +320,40 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
/* Intercept internal link clicks */ /* Always close drawer on click */
const link$ = fromEvent<MouseEvent>(document.body, "click") fromEvent<MouseEvent>(document.body, "click")
.pipe( .pipe(
filter(ev => !(ev.metaKey || ev.ctrlKey)), filter(ev => !(ev.metaKey || ev.ctrlKey)),
switchMap(ev => { filter(ev => {
if (ev.target instanceof HTMLElement) { if (ev.target instanceof HTMLElement) {
const el = ev.target.closest("a") // TODO: abstract as link click? const el = ev.target.closest("a") // TODO: abstract as link click?
if (el && isLocalLocation(el)) { if (el && isLocalLocation(el)) {
if (!isAnchorLocation(el) && config.features.includes("instant")) return true
ev.preventDefault()
return of(el)
} }
} }
return NEVER return false
}), })
share()
) )
.subscribe(() => {
setToggle("drawer", false)
})
/* Always close drawer on click */ /* Enable instant loading, if not on file:// protocol */
link$.subscribe(() => { if (config.features.includes("instant") && location.protocol !== "file:")
setToggle("drawer", false) setupInstantLoading({ document$, location$, viewport$ })
})
/* 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$
})
}
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
// if we use a single tab outside of search, unhide all permalinks. /* Unhide permalinks on first tab */
// TODO: experimental. necessary!?
keyboard$ keyboard$
.pipe( .pipe(
filter(key => key.mode === "global" && ["Tab"].includes(key.type)), filter(key => key.mode === "global" && key.type === "Tab"),
take(1) take(1)
) )
.subscribe(() => { .subscribe(() => {
for (const link of getElements(".headerlink")) for (const link of getElements(".headerlink"))
link.style.visibility = "visible" link.style.visibility = "visible"
}) })
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
@ -395,6 +361,7 @@ export function initialize(config: unknown) {
/* Browser observables */ /* Browser observables */
document$, document$,
location$,
viewport$, viewport$,
/* Component observables */ /* Component observables */
@ -406,7 +373,7 @@ export function initialize(config: unknown) {
tabs$, tabs$,
toc$, toc$,
/* Integation observables */ /* Integration observables */
clipboard$, clipboard$,
keyboard$, keyboard$,
dialog$ dialog$

View File

@ -25,7 +25,7 @@ import { NEVER, Observable, Subject, fromEventPattern } from "rxjs"
import { mapTo, share, tap } from "rxjs/operators" import { mapTo, share, tap } from "rxjs/operators"
import { getElements } from "browser" import { getElements } from "browser"
import { renderClipboard } from "templates" import { renderClipboardButton } from "templates"
import { translate } from "utilities" import { translate } from "utilities"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -66,7 +66,7 @@ export function setupClipboard(
blocks.forEach((block, index) => { blocks.forEach((block, index) => {
const parent = block.parentElement! const parent = block.parentElement!
parent.id = `__code_${index}` 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. * 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 { ajax } from "rxjs//ajax"
import { import {
bufferCount, bufferCount,
@ -43,9 +43,11 @@ import {
ViewportOffset, ViewportOffset,
getElement, getElement,
isAnchorLocation, isAnchorLocation,
isLocalLocation,
replaceElement, replaceElement,
setLocation, setLocation,
setLocationHash, setLocationHash,
setToggle,
setViewportOffset setViewportOffset
} from "browser" } from "browser"
@ -68,9 +70,8 @@ interface State {
*/ */
interface SetupOptions { interface SetupOptions {
document$: Subject<Document> /* Document subject */ document$: Subject<Document> /* Document subject */
viewport$: Observable<Viewport> /* Viewport observable */
link$: Observable<HTMLAnchorElement> /* Internal link observable */
location$: Subject<URL> /* Location subject */ location$: Subject<URL> /* Location subject */
viewport$: Observable<Viewport> /* Viewport observable */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
@ -97,19 +98,51 @@ interface SetupOptions {
* location change is dispatched regularly. * location change is dispatched regularly.
* *
* @param options - Options * @param options - Options
*
* @return TODO: return type?
*/ */
export function setupInstantLoading( export function setupInstantLoading(
{ document$, viewport$, link$, location$ }: SetupOptions { document$, viewport$, location$ }: SetupOptions
) { // TODO: add return type ): void {
const state$ = link$
/* 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( .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) })), map(el => ({ url: new URL(el.href) })),
share<State>() 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$ const push$ = state$
.pipe( .pipe(
distinctUntilChanged((prev, next) => prev.url.href === next.url.href), distinctUntilChanged((prev, next) => prev.url.href === next.url.href),
@ -121,11 +154,11 @@ export function setupInstantLoading(
const pop$ = fromEvent<PopStateEvent>(window, "popstate") const pop$ = fromEvent<PopStateEvent>(window, "popstate")
.pipe( .pipe(
filter(ev => ev.state !== null), filter(ev => ev.state !== null),
map<PopStateEvent, State>(ev => ({ map(ev => ({
url: new URL(location.href), url: new URL(location.href),
offset: ev.state offset: ev.state
})), })),
share() share<State>()
) )
/* Emit location change */ /* Emit location change */
@ -135,13 +168,11 @@ export function setupInstantLoading(
) )
.subscribe(location$) .subscribe(location$)
const dom = new DOMParser() /* Fetch document on location change */
const ajax$ = location$ const ajax$ = location$
.pipe( .pipe(
distinctUntilKeyChanged("pathname"), distinctUntilKeyChanged("pathname"),
skip(1), skip(1),
/* Fetch document */
switchMap(url => ajax({ switchMap(url => ajax({
url: url.href, url: url.href,
responseType: "text", responseType: "text",
@ -154,9 +185,9 @@ export function setupInstantLoading(
}) })
) )
) )
// share()
) )
/* Set new location as soon as the document was fetched */
push$ push$
.pipe( .pipe(
sample(ajax$) sample(ajax$)
@ -165,55 +196,30 @@ export function setupInstantLoading(
history.pushState({}, "", url.toString()) history.pushState({}, "", url.toString())
}) })
/* Parse and emit document */
const dom = new DOMParser()
ajax$ ajax$
.pipe( .pipe(
map(({ response }) => { map(({ response }) => dom.parseFromString(response, "text/html"))
return dom.parseFromString(response, "text/html")
})
) )
.subscribe(document$) .subscribe(document$)
/* History: debounce update of viewport offset */ /* Intercept instant loading */
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 */
const instant$ = merge(push$, pop$) const instant$ = merge(push$, pop$)
.pipe( .pipe(
sample(document$) sample(document$)
) )
// TODO: from here on, everything is beta.... ############################### // TODO: this must be combined with search scroll restoration on mobile
instant$.subscribe(({ url, offset }) => { instant$.subscribe(({ url, offset }) => {
if (url.hash && !offset) { if (url.hash && !offset) {
// console.log("set hash!") setLocationHash(url.hash)
setLocationHash(url.hash) // must delay, if search is open!
} else { } else {
setViewportOffset(offset || { y: 0 }) setViewportOffset(offset || { y: 0 })
} }
}) })
/* Replace document metadata */
instant$ instant$
.pipe( .pipe(
withLatestFrom(document$) 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 * @return Element
*/ */
export function renderClipboard( export function renderClipboardButton(
id: string id: string
) { ) {
return ( return (
<button <button
class={css.container} class={css.container}
title={translate("clipboard.copy")} 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"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d={path}></path> <path d={path}></path>