mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Improved scroll restoration implementation
This commit is contained in:
parent
a49b1421e7
commit
c362179234
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
24
material/assets/javascripts/bundle.dcf1ce56.min.js
vendored
Normal file
24
material/assets/javascripts/bundle.dcf1ce56.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/javascripts/bundle.dcf1ce56.min.js.map
Normal file
1
material/assets/javascripts/bundle.dcf1ce56.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.95ab87dc.min.js",
|
||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.95ab87dc.min.js.map",
|
||||
"assets/javascripts/bundle.js": "assets/javascripts/bundle.dcf1ce56.min.js",
|
||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.dcf1ce56.min.js.map",
|
||||
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.926ffd9e.min.js",
|
||||
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.926ffd9e.min.js.map",
|
||||
"assets/stylesheets/app-palette.scss": "assets/stylesheets/app-palette.3f90c815.min.css",
|
||||
|
@ -190,7 +190,7 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% block scripts %}
|
||||
<script src="{{ 'assets/javascripts/bundle.95ab87dc.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/bundle.dcf1ce56.min.js' | url }}"></script>
|
||||
{%- set translations = {} -%}
|
||||
{%- for key in [
|
||||
"clipboard.copy",
|
||||
|
@ -45,7 +45,13 @@ import {
|
||||
take,
|
||||
mapTo,
|
||||
shareReplay,
|
||||
sample
|
||||
sample,
|
||||
share,
|
||||
map,
|
||||
pluck,
|
||||
debounceTime,
|
||||
distinctUntilKeyChanged,
|
||||
distinctUntilChanged
|
||||
} from "rxjs/operators"
|
||||
|
||||
import {
|
||||
@ -61,7 +67,8 @@ import {
|
||||
setupToggles,
|
||||
useToggle,
|
||||
getElement,
|
||||
setViewportOffset
|
||||
setViewportOffset,
|
||||
ViewportOffset
|
||||
} from "./observables"
|
||||
import { setupSearchWorker } from "./workers"
|
||||
|
||||
@ -300,139 +307,177 @@ export function initialize(config: unknown) {
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
// instant loading
|
||||
const instant$ = config.feature.instant ? document$ // TODO: just use document$ and take(1)
|
||||
.pipe(
|
||||
take(1), // only initial load
|
||||
switchMap(({ body }) => fromEvent(body, "click")),
|
||||
withLatestFrom(viewport$),
|
||||
switchMap(([ev, { offset }]) => {
|
||||
if (ev.target && ev.target instanceof HTMLElement) {
|
||||
const link = ev.target.closest("a")
|
||||
if (link) {
|
||||
if (/(:\/\/|^#)/.test(link.getAttribute("href")!) === false) {
|
||||
ev.preventDefault()
|
||||
|
||||
// we must copy the value, or weird stuff will happen
|
||||
// remember scroll position!
|
||||
const href = link.href
|
||||
history.replaceState(offset, document.title)
|
||||
history.pushState({}, "", href)
|
||||
return of(href) // anchor.href
|
||||
/**
|
||||
* Location change
|
||||
*/
|
||||
interface LocationChange {
|
||||
url: URL // TODO: use URL!?
|
||||
data?: ViewportOffset
|
||||
}
|
||||
|
||||
function isInternalLink(el: HTMLAnchorElement | URL) {
|
||||
return el.hostname === location.hostname
|
||||
}
|
||||
|
||||
function isAnchorLink(el: HTMLAnchorElement | URL) {
|
||||
return el.hash.length > 0
|
||||
}
|
||||
|
||||
function compareLocationChange(
|
||||
{ url: a }: LocationChange, { url: b }: LocationChange
|
||||
) {
|
||||
return a.href === b.href
|
||||
}
|
||||
|
||||
// instant loading
|
||||
if (config.feature.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"]`
|
||||
])
|
||||
for (const el of getElements<HTMLLinkElement>(selector))
|
||||
el.href = el.href
|
||||
|
||||
/* Intercept internal link clicks */
|
||||
const internal$ = 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 && isInternalLink(el)) {
|
||||
if (!isAnchorLink(el))
|
||||
ev.preventDefault()
|
||||
return of(el.href)
|
||||
}
|
||||
}
|
||||
return NEVER
|
||||
}),
|
||||
shareReplay(1)
|
||||
distinctUntilChanged(),
|
||||
map<string, LocationChange>(href => ({ url: new URL(href) })),
|
||||
share()
|
||||
)
|
||||
: NEVER
|
||||
|
||||
// the location might change, but popstate might not be triggered which is
|
||||
// the case when we hit the back button on the same page. scroll to top.
|
||||
// location$
|
||||
// .pipe(
|
||||
// bufferCount(2, 1)
|
||||
// )
|
||||
// .subscribe(x => {
|
||||
// console.log(x)
|
||||
/* Intercept internal links to dispatch */
|
||||
const dispatch$ = internal$
|
||||
.pipe(
|
||||
filter(({ url }) => !isAnchorLink(url)),
|
||||
share()
|
||||
)
|
||||
|
||||
/* Intercept popstate events (history back and forward) */
|
||||
const popstate$ = fromEvent<PopStateEvent>(window, "popstate")
|
||||
.pipe(
|
||||
map<PopStateEvent, LocationChange>(ev => ({
|
||||
url: new URL(getLocation()),
|
||||
data: ev.state
|
||||
})),
|
||||
share()
|
||||
)
|
||||
|
||||
/* Emit location change */
|
||||
merge(dispatch$, popstate$)
|
||||
.pipe(
|
||||
pluck("url")
|
||||
)
|
||||
.subscribe(location$)
|
||||
|
||||
/* Add dispatched link to history */
|
||||
internal$
|
||||
.pipe(
|
||||
distinctUntilChanged(compareLocationChange),
|
||||
filter(({ url }) => !isAnchorLink(url))
|
||||
)
|
||||
.subscribe(({ url }) => {
|
||||
// console.log(`History.Push ${url}`)
|
||||
history.pushState({}, "", url.toString())
|
||||
})
|
||||
|
||||
/* Persist viewport offset in history before hash change */
|
||||
viewport$
|
||||
.pipe(
|
||||
debounceTime(250),
|
||||
distinctUntilKeyChanged("offset"),
|
||||
)
|
||||
.subscribe(({ offset }) => {
|
||||
console.log("Update", offset)
|
||||
history.replaceState(offset, "")
|
||||
})
|
||||
|
||||
// /* Edge case: go back from anchor to same
|
||||
// // // TODO: better to just replace the state when this is encountered.
|
||||
// // pop$
|
||||
// // .pipe(
|
||||
// // filter(({ href }) => !/#/.test(href)) // TODO: kind of sucks
|
||||
// // )
|
||||
// // .subscribe(({ data }) => {
|
||||
// // // console.log("Detected", data) // detects too much...
|
||||
// // setViewportOffset(data || { y: 0 }) // TOOD: must wait for document sample!
|
||||
// // })
|
||||
|
||||
/* */
|
||||
merge(dispatch$, popstate$)
|
||||
.pipe(
|
||||
sample(document$),
|
||||
withLatestFrom(document$),
|
||||
)
|
||||
.subscribe(([{ url: href, data }, { title, head }]) => {
|
||||
console.log("Done", href.href, data)
|
||||
|
||||
// setDocumentTitle
|
||||
document.title = title
|
||||
|
||||
// replace meta tags
|
||||
for (const selector of [
|
||||
`link[rel="canonical"]`,
|
||||
`meta[name="author"]`,
|
||||
`meta[name="description"]`
|
||||
]) {
|
||||
const next = getElement(selector, head)
|
||||
const prev = getElement(selector, document.head)
|
||||
if (
|
||||
typeof next !== "undefined" &&
|
||||
typeof prev !== "undefined"
|
||||
) {
|
||||
prev.replaceWith(next)
|
||||
}
|
||||
}
|
||||
|
||||
// // TODO: this doesnt work as expected
|
||||
// if (!data) {
|
||||
// const { hash } = new URL(href)
|
||||
// if (hash) {
|
||||
// const el = getElement(hash)
|
||||
// if (typeof el !== "undefined") {
|
||||
// el.scrollIntoView()
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// console.log(ev)
|
||||
// if (!data)
|
||||
setViewportOffset(data || { y: 0 }) // push state!
|
||||
})
|
||||
|
||||
// internal$.subscribe(({ url }) => {
|
||||
// console.log(`Internal ${url}`)
|
||||
// })
|
||||
|
||||
// deploy new location - can be written as instant$.subscribe(location$)
|
||||
instant$.subscribe(url => {
|
||||
console.log(`Load ${url}`)
|
||||
location$.next(url)
|
||||
// dispatch$.subscribe(({ url }) => {
|
||||
// console.log(`Push ${url}`)
|
||||
// })
|
||||
|
||||
popstate$.subscribe(({ url }) => {
|
||||
console.log(`Pop ${url}`)
|
||||
})
|
||||
|
||||
if ("scrollRestoration" in history)
|
||||
history.scrollRestoration = "manual"
|
||||
|
||||
const pop$ = fromEvent<PopStateEvent>(window, "popstate")
|
||||
.pipe(
|
||||
shareReplay(1) // TODO: share() should be enough
|
||||
)
|
||||
|
||||
pop$
|
||||
.subscribe(() => location$.next(getLocation()))
|
||||
|
||||
pop$
|
||||
.pipe(
|
||||
sample(document$),
|
||||
withLatestFrom(document$),
|
||||
)
|
||||
.subscribe(([ev, { title, head }]) => {
|
||||
|
||||
document.title = title
|
||||
|
||||
// replace meta tags
|
||||
for (const selector of [
|
||||
"link[rel=canonical]",
|
||||
"meta[name=author]",
|
||||
"meta[name=description]"
|
||||
]) {
|
||||
const next = getElement(selector, head)
|
||||
const prev = getElement(selector, document.head)
|
||||
if (
|
||||
typeof next !== "undefined" &&
|
||||
typeof prev !== "undefined"
|
||||
) {
|
||||
prev.replaceWith(next)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(ev)
|
||||
if (ev.state)
|
||||
setViewportOffset(ev.state)
|
||||
})
|
||||
|
||||
// make links absolute, so they remain stable
|
||||
for (const selector of [
|
||||
"link[rel='shortcut icon']",
|
||||
"link[rel='stylesheet']"
|
||||
]) {
|
||||
for (const el of getElements<HTMLLinkElement>(selector))
|
||||
el.href = el.href
|
||||
}
|
||||
|
||||
// if a new url is deployed via instant loading, switch to document observable
|
||||
// to exactly know when the content was loaded. then go to top.
|
||||
instant$
|
||||
.pipe(
|
||||
sample(document$),
|
||||
withLatestFrom(document$),
|
||||
)
|
||||
.subscribe(([url, { title, head }]) => {
|
||||
document.title = title
|
||||
|
||||
// replace meta tags
|
||||
for (const selector of [
|
||||
"link[rel=canonical]",
|
||||
"meta[name=author]",
|
||||
"meta[name=description]"
|
||||
]) {
|
||||
const next = getElement(selector, head)
|
||||
const prev = getElement(selector, document.head)
|
||||
if (
|
||||
typeof next !== "undefined" &&
|
||||
typeof prev !== "undefined"
|
||||
) {
|
||||
prev.replaceWith(next)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this doesnt work as expected
|
||||
const { hash } = new URL(url)
|
||||
if (hash) {
|
||||
const el = getElement(hash)
|
||||
if (typeof el !== "undefined") {
|
||||
el.scrollIntoView()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// console.log("scroll to top")
|
||||
setViewportOffset({ y: 0 })
|
||||
}
|
||||
})
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
|
@ -33,7 +33,7 @@ import { watchDocumentSwitch } from "../switch"
|
||||
* Watch options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
location$?: Observable<string> /* Location observable */
|
||||
location$?: Observable<URL> /* Location observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
|
@ -25,6 +25,7 @@ import { ajax } from "rxjs/ajax"
|
||||
import {
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
pluck,
|
||||
share,
|
||||
@ -43,7 +44,7 @@ import { getLocation, setLocation } from "../../location"
|
||||
* Watch options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
location$: Observable<string> /* Location observable */
|
||||
location$: Observable<URL> /* Location observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -69,8 +70,9 @@ export function watchDocumentSwitch(
|
||||
): Observable<Document> {
|
||||
return location$
|
||||
.pipe(
|
||||
startWith(getLocation()),
|
||||
map(url => url.replace(/#[^#]*$/, "")),
|
||||
startWith(location), // TODO: getLocation should return URL or Location
|
||||
filter(url => url.hash.length === 0), // use isAnchorLink
|
||||
map(url => url.href),
|
||||
distinctUntilChanged(),
|
||||
skip(1),
|
||||
|
||||
|
@ -52,8 +52,8 @@ export function setLocation(value: string): void {
|
||||
*
|
||||
* @return Location subject
|
||||
*/
|
||||
export function watchLocation(): Subject<string> {
|
||||
const location$ = new Subject<string>()
|
||||
export function watchLocation(): Subject<URL> {
|
||||
const location$ = new Subject<URL>()
|
||||
// fromEvent<PopStateEvent>(window, "popstate")
|
||||
// .pipe(
|
||||
// map(getLocation)
|
||||
|
Loading…
Reference in New Issue
Block a user