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

View File

@ -45,14 +45,14 @@ import {
take, take,
mapTo, mapTo,
shareReplay, shareReplay,
switchMapTo, sample
skip
} from "rxjs/operators" } from "rxjs/operators"
import { import {
watchToggle, watchToggle,
setToggle, setToggle,
getElements, getElements,
getLocation,
watchMedia, watchMedia,
watchDocument, watchDocument,
watchLocation, watchLocation,
@ -271,7 +271,7 @@ export function initialize(config: unknown) {
) )
) )
.subscribe(hash => { .subscribe(hash => {
getElement(hash)!.scrollIntoView() getElement(`[id="${hash}"]`)!.scrollIntoView()
}) })
// Scroll lock // document -> document$ => { body } !? // Scroll lock // document -> document$ => { body } !?
@ -305,17 +305,20 @@ export function initialize(config: unknown) {
.pipe( .pipe(
take(1), // only initial load take(1), // only initial load
switchMap(({ body }) => fromEvent(body, "click")), switchMap(({ body }) => fromEvent(body, "click")),
switchMap(ev => { withLatestFrom(viewport$),
switchMap(([ev, { offset }]) => {
if (ev.target && ev.target instanceof HTMLElement) { if (ev.target && ev.target instanceof HTMLElement) {
const anchor = ev.target.closest("a") const link = ev.target.closest("a")
if (anchor) { if (link) {
if (/(:\/\/|^#)/.test(anchor.getAttribute("href")!) === false) { if (/(:\/\/|^#)/.test(link.getAttribute("href")!) === false) {
ev.preventDefault() ev.preventDefault()
// we must copy the value, or weird stuff will happen // we must copy the value, or weird stuff will happen
const href = anchor.href // remember scroll position!
history.pushState(true, "", href) const href = link.href
return of(href) history.replaceState(offset, document.title)
history.pushState({}, "", href)
return of(href) // anchor.href
} }
} }
} }
@ -325,20 +328,110 @@ export function initialize(config: unknown) {
) )
: NEVER : 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$) // deploy new location - can be written as instant$.subscribe(location$)
instant$.subscribe(url => { instant$.subscribe(url => {
console.log(`Load ${url}`) console.log(`Load ${url}`)
location$.next(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 // 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. // to exactly know when the content was loaded. then go to top.
instant$ instant$
.pipe( .pipe(
switchMapTo(document$.pipe(skip(1))), // TODO: just use document$ and skip(1) sample(document$),
withLatestFrom(document$),
) )
.subscribe(() => { .subscribe(([url, { title, head }]) => {
setViewportOffset({ y: 0 }) 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 * as ClipboardJS from "clipboard"
import { NEVER, Observable, Subject, fromEventPattern } from "rxjs" 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 { getElements } from "observables"
import { renderClipboard } from "templates" import { renderClipboard } from "templates"
@ -76,7 +76,7 @@ export function setupClipboard(
new ClipboardJS(".md-clipboard").on("success", next) new ClipboardJS(".md-clipboard").on("success", next)
}) })
.pipe( .pipe(
shareReplay(1) share()
) )
/* Display notification for clipboard event */ /* Display notification for clipboard event */

View File

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

View File

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

View File

@ -42,6 +42,9 @@ export interface ViewportOffset {
/** /**
* Retrieve viewport offset * 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 * @return Viewport offset
*/ */
export function getViewportOffset(): ViewportOffset { export function getViewportOffset(): ViewportOffset {