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:
parent
4d370fe903
commit
edc9d6fc61
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
material/assets/javascripts/bundle.edc2ff56.min.js
vendored
Normal file
2
material/assets/javascripts/bundle.edc2ff56.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/javascripts/bundle.edc2ff56.min.js.map
Normal file
1
material/assets/javascripts/bundle.edc2ff56.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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,68 +320,40 @@ 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
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
setToggle("drawer", false)
|
||||
})
|
||||
|
||||
/* Always close drawer on click */
|
||||
link$.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(() => {
|
||||
for (const link of getElements(".headerlink"))
|
||||
link.style.visibility = "visible"
|
||||
})
|
||||
.subscribe(() => {
|
||||
for (const link of getElements(".headerlink"))
|
||||
link.style.visibility = "visible"
|
||||
})
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
@ -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$
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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 })
|
||||
})
|
||||
}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user