Refactored instant loading setup

This commit is contained in:
squidfunk 2020-02-21 10:18:49 +01:00
parent 9edf4e8068
commit c7e4063d86
13 changed files with 104 additions and 79 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,6 +1,6 @@
{ {
"assets/javascripts/bundle.js": "assets/javascripts/bundle.1defb77e.min.js", "assets/javascripts/bundle.js": "assets/javascripts/bundle.d3f83a35.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.1defb77e.min.js.map", "assets/javascripts/bundle.js.map": "assets/javascripts/bundle.d3f83a35.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",

View File

@ -190,7 +190,7 @@
{% endblock %} {% endblock %}
</div> </div>
{% block scripts %} {% block scripts %}
<script src="{{ 'assets/javascripts/bundle.1defb77e.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/bundle.d3f83a35.min.js' | url }}"></script>
{%- set translations = {} -%} {%- set translations = {} -%}
{%- for key in [ {%- for key in [
"clipboard.copy", "clipboard.copy",

View File

@ -27,8 +27,7 @@ import {
map, map,
scan, scan,
shareReplay, shareReplay,
switchMap, switchMap
tap
} from "rxjs/operators" } from "rxjs/operators"
import { getElement } from "observables" import { getElement } from "observables"
@ -86,20 +85,18 @@ let components$: Observable<ComponentMap>
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Watch components with given names * Setup bindings to elements with given names
* *
* This function returns an observable that will maintain bindings to the given * This function will maintain bindings to the elements identified by the given
* components in-between document switches and update the components in-place. * names in-between document switches and update the elements in-place.
* *
* @param names - Component names * @param names - Component names
* @param options - Options * @param options - Options
*
* @return Component map observable
*/ */
export function watchComponentMap( export function setupComponents(
names: Component[], { document$ }: WatchOptions names: Component[], { document$ }: WatchOptions
): Observable<ComponentMap> { ): void {
return components$ = document$ components$ = document$
.pipe( .pipe(
/* Build component map */ /* Build component map */
@ -141,6 +138,9 @@ export function watchComponentMap(
/** /**
* Retrieve a component * Retrieve a component
* *
* The returned observable will only re-emit if the element changed, i.e. if
* it was replaced from a document which was switched to.
*
* @template T - Element type * @template T - Element type
* *
* @param name - Component name * @param name - Component name

View File

@ -45,7 +45,8 @@ import {
take, take,
mapTo, mapTo,
shareReplay, shareReplay,
switchMapTo switchMapTo,
skip
} from "rxjs/operators" } from "rxjs/operators"
import { import {
@ -57,10 +58,10 @@ import {
watchLocation, watchLocation,
watchLocationHash, watchLocationHash,
watchViewport, watchViewport,
watchToggleMap, setupToggles,
useToggle, useToggle,
getElement, getElement,
watchDocumentSwitch setViewportOffset
} from "./observables" } from "./observables"
import { setupSearchWorker } from "./workers" import { setupSearchWorker } from "./workers"
@ -74,7 +75,7 @@ import {
mountTableOfContents, mountTableOfContents,
mountTabs, mountTabs,
useComponent, useComponent,
watchComponentMap, setupComponents,
mountHeaderTitle, mountHeaderTitle,
mountSearchQuery, mountSearchQuery,
mountSearchReset, mountSearchReset,
@ -114,14 +115,9 @@ export function initialize(config: unknown) {
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`) throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
const location$ = watchLocation() const location$ = watchLocation()
const document$ = watchDocument(
// instant loading config.feature.instant ? { location$ } : {}
const switch$ = config.feature.instant )
? watchDocumentSwitch({ location$ })
: NEVER
const load$ = watchDocument()
const document$ = merge(load$, switch$)
const hash$ = watchLocationHash() const hash$ = watchLocationHash()
const viewport$ = watchViewport() const viewport$ = watchViewport()
const tablet$ = watchMedia("(min-width: 960px)") const tablet$ = watchMedia("(min-width: 960px)")
@ -135,12 +131,14 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
watchToggleMap([ /* Setup toggle bindings */
setupToggles([
"drawer", /* Toggle for drawer */ "drawer", /* Toggle for drawer */
"search" /* Toggle for search */ "search" /* Toggle for search */
], { document$ }) ], { document$ })
watchComponentMap([ /* Setup components bindings */
setupComponents([
"container", /* Container */ "container", /* Container */
"header", /* Header */ "header", /* Header */
"header-title", /* Header title */ "header-title", /* Header title */
@ -155,6 +153,8 @@ export function initialize(config: unknown) {
"toc" /* Table of contents */ "toc" /* Table of contents */
], { document$ }) ], { document$ })
/* ----------------------------------------------------------------------- */
/* Create header observable */ /* Create header observable */
const header$ = useComponent("header") const header$ = useComponent("header")
.pipe( .pipe(
@ -300,13 +300,11 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
// instant loading // instant loading
const instant$ = config.feature.instant ? load$ // TODO: just use document$ and take(1) const instant$ = config.feature.instant ? document$ // TODO: just use document$ and take(1)
.pipe( .pipe(
take(1), // only initial load
switchMap(({ body }) => fromEvent(body, "click")), switchMap(({ body }) => fromEvent(body, "click")),
switchMap(ev => { switchMap(ev => {
// two cases: search results which should always load from same domain
// and/or
if (ev.target && ev.target instanceof HTMLElement) { if (ev.target && ev.target instanceof HTMLElement) {
const anchor = ev.target.closest("a") const anchor = ev.target.closest("a")
if (anchor) { if (anchor) {
@ -326,19 +324,20 @@ export function initialize(config: unknown) {
) )
: NEVER : NEVER
// deploy new 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)
}) })
// scroll to top when document is loaded // 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$ instant$
.pipe( .pipe(
switchMapTo(switch$), // TODO: just use document$ and skip(1) switchMapTo(document$.pipe(skip(1))), // TODO: just use document$ and skip(1)
) )
.subscribe(() => { .subscribe(() => {
window.scrollTo(0, 0) // TODO: or scroll element into view setViewportOffset({ y: 0 })
}) })
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */

View File

@ -75,8 +75,11 @@ export function setupClipboard(
const clipboard$ = fromEventPattern<ClipboardJS.Event>(next => { const clipboard$ = fromEventPattern<ClipboardJS.Event>(next => {
new ClipboardJS(".md-clipboard").on("success", next) new ClipboardJS(".md-clipboard").on("success", next)
}) })
.pipe(
shareReplay(1)
)
/* Display notification upon clipboard copy */ /* Display notification for clipboard event */
clipboard$ clipboard$
.pipe( .pipe(
tap(ev => ev.clearSelection()), tap(ev => ev.clearSelection()),
@ -84,9 +87,6 @@ export function setupClipboard(
) )
.subscribe(dialog$) .subscribe(dialog$)
/* Return clipboard as hot observable */ /* Return clipboard */
return clipboard$ return clipboard$
.pipe(
shareReplay(1)
)
} }

View File

@ -68,6 +68,7 @@ export function setupDialog(
switchMap(text => useComponent("container") switchMap(text => useComponent("container")
.pipe( .pipe(
map(container => container.appendChild(dialog)), map(container => container.appendChild(dialog)),
observeOn(animationFrameScheduler),
delay(1), // Strangley it doesnt work when we push things to the new animation frame... delay(1), // Strangley it doesnt work when we push things to the new animation frame...
tap(el => { tap(el => {
el.innerHTML = text el.innerHTML = text

View File

@ -20,9 +20,22 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, fromEvent } from "rxjs" import { NEVER, Observable, fromEvent, merge } from "rxjs"
import { mapTo, shareReplay } from "rxjs/operators" import { mapTo, shareReplay } from "rxjs/operators"
import { watchDocumentSwitch } from "../switch"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Watch options
*/
interface WatchOptions {
location$?: Observable<string> /* Location observable */
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -30,12 +43,24 @@ import { mapTo, shareReplay } from "rxjs/operators"
/** /**
* Watch document * Watch document
* *
* If the location observable is passed, instant loading will be enabled which
* means that new values will be emitted every time the location changes.
*
* @return Document observable * @return Document observable
*/ */
export function watchDocument(): Observable<Document> { export function watchDocument(
return fromEvent(document, "DOMContentLoaded") { location$ }: WatchOptions = {}
): Observable<Document> {
return merge(
fromEvent(document, "DOMContentLoaded")
.pipe(
mapTo(document)
),
typeof location$ !== "undefined"
? watchDocumentSwitch({ location$ })
: NEVER
)
.pipe( .pipe(
mapTo(document),
shareReplay(1) shareReplay(1)
) )
} }

View File

@ -75,17 +75,15 @@ let toggles$: Observable<ToggleMap>
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Watch toggles with given names * Setup bindings to toggles with given names
* *
* @param names - Toggle names * @param names - Toggle names
* @param options - Options * @param options - Options
*
* @return Toggle map observable
*/ */
export function watchToggleMap( export function setupToggles(
names: Toggle[], { document$ }: WatchOptions names: Toggle[], { document$ }: WatchOptions
): Observable<ToggleMap> { ): void {
return toggles$ = document$ toggles$ = document$
.pipe( .pipe(
/* Ignore document switches */ /* Ignore document switches */
@ -108,7 +106,8 @@ export function watchToggleMap(
/** /**
* Retrieve a toggle * Retrieve a toggle
* *
* @template T - Element type * The returned observable will only re-emit if the element changed, i.e. if
* it was replaced from a document which was switched to.
* *
* @param name - Toggle name * @param name - Toggle name
* *
@ -136,7 +135,7 @@ export function useToggle(
* Simulating a click event seems to be the most cross-browser compatible way * Simulating a click event seems to be the most cross-browser compatible way
* of changing the value while also emitting a `change` event. Before, Material * of changing the value while also emitting a `change` event. Before, Material
* used `CustomEvent` to programmatically change the value of a toggle, but this * used `CustomEvent` to programmatically change the value of a toggle, but this
* is a much simpler and cleaner solution. * is a much simpler and cleaner solution which doesn't require a polyfill.
* *
* @param el - Toggle element * @param el - Toggle element
* @param value - Toggle value * @param value - Toggle value

View File

@ -22,7 +22,7 @@
import { Subject, from } from "rxjs" import { Subject, from } from "rxjs"
import { ajax } from "rxjs/ajax" import { ajax } from "rxjs/ajax"
import { map, pluck } from "rxjs/operators" import { map, pluck, shareReplay } from "rxjs/operators"
import { SearchIndexOptions } from "integrations/search" import { SearchIndexOptions } from "integrations/search"
import { import {
@ -106,7 +106,8 @@ export function setupSearchWorker(
} }
} }
return message return message
}) }),
shareReplay(1)
) )
/* Fetch index if it wasn't passed explicitly */ /* Fetch index if it wasn't passed explicitly */