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": "assets/javascripts/bundle.dcf1ce56.min.js",
|
||||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.95ab87dc.min.js.map",
|
"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": "assets/javascripts/worker/search.926ffd9e.min.js",
|
||||||
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.926ffd9e.min.js.map",
|
"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",
|
"assets/stylesheets/app-palette.scss": "assets/stylesheets/app-palette.3f90c815.min.css",
|
||||||
|
@ -190,7 +190,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ 'assets/javascripts/bundle.95ab87dc.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/bundle.dcf1ce56.min.js' | url }}"></script>
|
||||||
{%- set translations = {} -%}
|
{%- set translations = {} -%}
|
||||||
{%- for key in [
|
{%- for key in [
|
||||||
"clipboard.copy",
|
"clipboard.copy",
|
||||||
|
@ -45,7 +45,13 @@ import {
|
|||||||
take,
|
take,
|
||||||
mapTo,
|
mapTo,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
sample
|
sample,
|
||||||
|
share,
|
||||||
|
map,
|
||||||
|
pluck,
|
||||||
|
debounceTime,
|
||||||
|
distinctUntilKeyChanged,
|
||||||
|
distinctUntilChanged
|
||||||
} from "rxjs/operators"
|
} from "rxjs/operators"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -61,7 +67,8 @@ import {
|
|||||||
setupToggles,
|
setupToggles,
|
||||||
useToggle,
|
useToggle,
|
||||||
getElement,
|
getElement,
|
||||||
setViewportOffset
|
setViewportOffset,
|
||||||
|
ViewportOffset
|
||||||
} from "./observables"
|
} from "./observables"
|
||||||
import { setupSearchWorker } from "./workers"
|
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)
|
* Location change
|
||||||
.pipe(
|
*/
|
||||||
take(1), // only initial load
|
interface LocationChange {
|
||||||
switchMap(({ body }) => fromEvent(body, "click")),
|
url: URL // TODO: use URL!?
|
||||||
withLatestFrom(viewport$),
|
data?: ViewportOffset
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
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
|
/* Intercept internal links to dispatch */
|
||||||
// the case when we hit the back button on the same page. scroll to top.
|
const dispatch$ = internal$
|
||||||
// location$
|
.pipe(
|
||||||
// .pipe(
|
filter(({ url }) => !isAnchorLink(url)),
|
||||||
// bufferCount(2, 1)
|
share()
|
||||||
// )
|
)
|
||||||
// .subscribe(x => {
|
|
||||||
// console.log(x)
|
/* 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$)
|
// dispatch$.subscribe(({ url }) => {
|
||||||
instant$.subscribe(url => {
|
// console.log(`Push ${url}`)
|
||||||
console.log(`Load ${url}`)
|
// })
|
||||||
location$.next(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
|
* Watch options
|
||||||
*/
|
*/
|
||||||
interface WatchOptions {
|
interface WatchOptions {
|
||||||
location$?: Observable<string> /* Location observable */
|
location$?: Observable<URL> /* Location observable */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
|
@ -25,6 +25,7 @@ import { ajax } from "rxjs/ajax"
|
|||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
map,
|
map,
|
||||||
pluck,
|
pluck,
|
||||||
share,
|
share,
|
||||||
@ -43,7 +44,7 @@ import { getLocation, setLocation } from "../../location"
|
|||||||
* Watch options
|
* Watch options
|
||||||
*/
|
*/
|
||||||
interface WatchOptions {
|
interface WatchOptions {
|
||||||
location$: Observable<string> /* Location observable */
|
location$: Observable<URL> /* Location observable */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
@ -69,8 +70,9 @@ export function watchDocumentSwitch(
|
|||||||
): Observable<Document> {
|
): Observable<Document> {
|
||||||
return location$
|
return location$
|
||||||
.pipe(
|
.pipe(
|
||||||
startWith(getLocation()),
|
startWith(location), // TODO: getLocation should return URL or Location
|
||||||
map(url => url.replace(/#[^#]*$/, "")),
|
filter(url => url.hash.length === 0), // use isAnchorLink
|
||||||
|
map(url => url.href),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
skip(1),
|
skip(1),
|
||||||
|
|
||||||
|
@ -52,8 +52,8 @@ export function setLocation(value: string): void {
|
|||||||
*
|
*
|
||||||
* @return Location subject
|
* @return Location subject
|
||||||
*/
|
*/
|
||||||
export function watchLocation(): Subject<string> {
|
export function watchLocation(): Subject<URL> {
|
||||||
const location$ = new Subject<string>()
|
const location$ = new Subject<URL>()
|
||||||
// fromEvent<PopStateEvent>(window, "popstate")
|
// fromEvent<PopStateEvent>(window, "popstate")
|
||||||
// .pipe(
|
// .pipe(
|
||||||
// map(getLocation)
|
// map(getLocation)
|
||||||
|
Loading…
Reference in New Issue
Block a user