Improved browser history navigation in conjunction with instant loading

This commit is contained in:
squidfunk 2020-02-21 17:36:04 +01:00
parent 1020953fa5
commit 674e3456f8
12 changed files with 148 additions and 52 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,8 +1,8 @@
{
"assets/javascripts/bundle.js": "assets/javascripts/bundle.71def461.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.71def461.min.js.map",
"assets/javascripts/bundle.js": "assets/javascripts/bundle.95ab87dc.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.95ab87dc.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",
"assets/stylesheets/app.scss": "assets/stylesheets/app.0f079138.min.css"
"assets/stylesheets/app.scss": "assets/stylesheets/app.68c05372.min.css"
}

File diff suppressed because one or more lines are too long

View File

@ -43,7 +43,7 @@
{% endif %}
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/app.0f079138.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/app.68c05372.min.css' | url }}">
{% if palette.primary or palette.accent %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/app-palette.3f90c815.min.css' | url }}">
{% endif %}
@ -190,7 +190,7 @@
{% endblock %}
</div>
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.71def461.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.95ab87dc.min.js' | url }}"></script>
{%- set translations = {} -%}
{%- for key in [
"clipboard.copy",

View File

@ -45,14 +45,14 @@ import {
take,
mapTo,
shareReplay,
switchMapTo,
skip
sample
} from "rxjs/operators"
import {
watchToggle,
setToggle,
getElements,
getLocation,
watchMedia,
watchDocument,
watchLocation,
@ -271,7 +271,7 @@ export function initialize(config: unknown) {
)
)
.subscribe(hash => {
getElement(hash)!.scrollIntoView()
getElement(`[id="${hash}"]`)!.scrollIntoView()
})
// Scroll lock // document -> document$ => { body } !?
@ -305,17 +305,20 @@ export function initialize(config: unknown) {
.pipe(
take(1), // only initial load
switchMap(({ body }) => fromEvent(body, "click")),
switchMap(ev => {
withLatestFrom(viewport$),
switchMap(([ev, { offset }]) => {
if (ev.target && ev.target instanceof HTMLElement) {
const anchor = ev.target.closest("a")
if (anchor) {
if (/(:\/\/|^#)/.test(anchor.getAttribute("href")!) === false) {
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
const href = anchor.href
history.pushState(true, "", href)
return of(href)
// remember scroll position!
const href = link.href
history.replaceState(offset, document.title)
history.pushState({}, "", href)
return of(href) // anchor.href
}
}
}
@ -325,20 +328,110 @@ export function initialize(config: unknown) {
)
: 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)
// })
// deploy new location - can be written as instant$.subscribe(location$)
instant$.subscribe(url => {
console.log(`Load ${url}`)
location$.next(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(
switchMapTo(document$.pipe(skip(1))), // TODO: just use document$ and skip(1)
sample(document$),
withLatestFrom(document$),
)
.subscribe(() => {
.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 })
}
})
/* ----------------------------------------------------------------------- */

View File

@ -22,7 +22,7 @@
import * as ClipboardJS from "clipboard"
import { NEVER, Observable, Subject, fromEventPattern } from "rxjs"
import { mapTo, shareReplay, tap } from "rxjs/operators"
import { mapTo, share, tap } from "rxjs/operators"
import { getElements } from "observables"
import { renderClipboard } from "templates"
@ -76,7 +76,7 @@ export function setupClipboard(
new ClipboardJS(".md-clipboard").on("success", next)
})
.pipe(
shareReplay(1)
share()
)
/* Display notification for clipboard event */

View File

@ -54,11 +54,11 @@ export function setLocation(value: string): void {
*/
export function watchLocation(): Subject<string> {
const location$ = new Subject<string>()
fromEvent<PopStateEvent>(window, "popstate")
.pipe(
map(getLocation)
)
.subscribe(location$)
// fromEvent<PopStateEvent>(window, "popstate")
// .pipe(
// map(getLocation)
// )
// .subscribe(location$)
/* Return location subject */
return location$

View File

@ -33,7 +33,7 @@ import { filter, map, share, startWith } from "rxjs/operators"
* @return Location hash
*/
export function getLocationHash(): string {
return location.hash
return location.hash.substring(1)
}
/* ------------------------------------------------------------------------- */

View File

@ -42,6 +42,9 @@ export interface ViewportOffset {
/**
* Retrieve viewport offset
*
* On iOS Safari, viewport offset can be negative due to overflow scrolling.
* As this may induce strange behaviors downstream, we'll just limit it to 0.
*
* @return Viewport offset
*/
export function getViewportOffset(): ViewportOffset {