Refactored document into subject and moved switching into instant loading

This commit is contained in:
squidfunk 2020-03-28 12:37:51 +01:00
parent 1cd1a056f8
commit 0e80ab45b6
18 changed files with 101 additions and 217 deletions

View File

@ -6,7 +6,7 @@ template: overrides/main.html
## Highlights
* Reactive architecture try `__material.dialog$.next("Hi!")` in the console
* Reactive architecture try `app.dialog$.next("Hi!")` in the console
* [Instant loading][5] make Material behave like a Single Page Application
* Improved CSS customization with [CSS variables][1] set your brand's colors
* Improved CSS resilience, e.g. proper sidebar locking for customized headers
@ -191,7 +191,7 @@ matches the new structure:
- <meta name="lang:{{ key }}" content="{{ lang.t(key) }}">
- {% endfor %}
<link rel="shortcut icon" href="{{ config.theme.favicon | url }}">
<meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-5.0.0b2-1">
<meta name="generator" content="mkdocs-{{ mkdocs_version }}, mkdocs-material-5.0.0">
{% endblock %}
@@ -56,9 +42,9 @@
{% endif %}
@ -225,7 +225,7 @@ matches the new structure:
{% if config.extra.manifest %}
<link rel="manifest" href="{{ config.extra.manifest | url }}" crossorigin="use-credentials">
{% endif %}
@@ -95,47 +78,46 @@
@@ -95,47 +77,46 @@
{% endblock %}
{% block extrahead %}{% endblock %}
</head>
@ -294,7 +294,7 @@ matches the new structure:
{% block site_nav %}
{% if nav %}
<div class="md-sidebar md-sidebar--primary" data-md-component="navigation">
@@ -160,41 +142,25 @@
@@ -160,41 +141,25 @@
<article class="md-content__inner md-typeset">
{% block content %}
{% if page.edit_url %}
@ -349,15 +349,12 @@ matches the new structure:
{% endif %}
{% endblock %}
{% block disqus %}
@@ -208,29 +174,40 @@
@@ -208,29 +174,35 @@
{% include "partials/footer.html" %}
{% endblock %}
</div>
+ {% block config %}
+ <script>var __config={}</script>
+ {% endblock %}
{% block scripts %}
- <script src="{{ 'assets/javascripts/application.df00da5d.js' | url }}"></script>
- <script src="{{ 'assets/javascripts/application.********.js' | url }}"></script>
- {% if lang.t("search.language") != "en" %}
- {% set languages = lang.t("search.language").split(",") %}
- {% if languages | length and languages[0] != "" %}
@ -379,8 +376,8 @@ matches the new structure:
- {% endif %}
- {% endif %}
- <script>app.initialize({version:"{{ mkdocs_version }}",url:{base:"{{ base_url }}"}})</script>
+ <script src="{{ 'assets/javascripts/vendor.31a2e7b9.min.js' | url }}"></script>
+ <script src="{{ 'assets/javascripts/bundle.5b33ad8d.min.js' | url }}"></script>
+ <script src="{{ 'assets/javascripts/vendor.********.min.js' | url }}"></script>
+ <script src="{{ 'assets/javascripts/bundle.********.min.js' | url }}"></script>
+ {%- set translations = {} -%}
+ {%- for key in [
+ "clipboard.copy",
@ -396,18 +393,17 @@ matches the new structure:
+ {%- set _ = translations.update({ key: lang.t(key) }) -%}
+ {%- endfor -%}
+ <script id="__lang" type="application/json">
+ {{ translations | tojson }}
+ {{- translations | tojson -}}
+ </script>
+ {% block config %}{% endblock %}
+ <script>
+ __material = initialize(Object.assign({
+ url: {
+ app = initialize({
+ base: "{{ base_url }}",
+ worker: {
+ search: "{{ 'assets/javascripts/worker/search.edc88caf.min.js' | url }}"
+ }
+ },
+ features: {{ config.theme.features | tojson }}
+ }, __config))
+ features: {{ config.theme.features | tojson }},
+ search: Object.assign({
+ worker: "{{ 'assets/javascripts/worker/search.********.min.js' | url }}"
+ }, typeof search !== "undefined" && search)
+ })
+ </script>
{% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script>

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

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.aa7a7592.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.aa7a7592.min.js.map",
"assets/javascripts/vendor.js": "assets/javascripts/vendor.3340e0de.min.js",
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.3340e0de.min.js.map",
"assets/javascripts/bundle.js": "assets/javascripts/bundle.864d2fd8.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.864d2fd8.min.js.map",
"assets/javascripts/vendor.js": "assets/javascripts/vendor.cfd486f9.min.js",
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.cfd486f9.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.3bc815f0.min.js",
"assets/javascripts/worker/search.js.map": "assets/javascripts/worker/search.3bc815f0.min.js.map",
"assets/stylesheets/main.css": "assets/stylesheets/main.b32d3181.min.css",

View File

@ -174,8 +174,8 @@
{% endblock %}
</div>
{% block scripts %}
<script src="{{ 'assets/javascripts/vendor.3340e0de.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.aa7a7592.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/vendor.cfd486f9.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.864d2fd8.min.js' | url }}"></script>
{%- set translations = {} -%}
{%- for key in [
"clipboard.copy",

View File

@ -1,66 +0,0 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { NEVER, Observable, fromEvent, merge } from "rxjs"
import { mapTo, shareReplay } from "rxjs/operators"
import { watchDocumentSwitch } from "../switch"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Watch options
*/
interface WatchOptions {
location$?: Observable<URL> /* Location observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* 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
*/
export function watchDocument(
{ location$ }: WatchOptions = {}
): Observable<Document> {
return merge(
fromEvent(document, "DOMContentLoaded")
.pipe(
mapTo(document)
),
typeof location$ !== "undefined"
? watchDocumentSwitch({ location$ })
: NEVER
)
.pipe(
shareReplay(1)
)
}

View File

@ -20,5 +20,30 @@
* IN THE SOFTWARE.
*/
export * from "./_"
export * from "./switch"
import { ReplaySubject, Subject, fromEvent } from "rxjs"
import { mapTo } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch document
*
* Documents must be implemented as subjects, so all downstream observables are
* automatically updated when a new document is emitted. This enabled features
* like instant loading.
*
* @return Document subject
*/
export function watchDocument(): Subject<Document> {
const document$ = new ReplaySubject<Document>()
fromEvent(document, "DOMContentLoaded")
.pipe(
mapTo(document)
)
.subscribe(document$)
/* Return document */
return document$
}

View File

@ -1,96 +0,0 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { NEVER, Observable } from "rxjs"
import { ajax } from "rxjs/ajax"
import {
catchError,
distinctUntilKeyChanged,
map,
share,
skip,
switchMap
} from "rxjs/operators"
import { setLocation } from "../../location"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Watch options
*/
interface WatchOptions {
location$: Observable<URL> /* Location observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch document switch
*
* This function returns an observable that fetches a document if the provided
* location observable emits a new value (i.e. URL). If the emitted URL points
* to the same page, the request is effectively ignored (i.e. when only the
* fragment identifier changes).
*
* Theoretically, we could use `responseType: "document"`, but since all MkDocs
* links are relative, we need to make sure that the current location matches
* the document we just loaded. Otherwise any relative links in the document
* may use the old location.
*
* @param options - Options
*
* @return Document observable
*/
export function watchDocumentSwitch(
{ location$ }: WatchOptions
): Observable<Document> {
const dom = new DOMParser()
return location$
.pipe(
distinctUntilKeyChanged("pathname"),
skip(1),
/* Fetch document */
switchMap(url => ajax({
url: url.href,
responseType: "text",
withCredentials: true
})
.pipe(
map(({ response }): Document => {
history.pushState({}, "", url.toString()) // TODO: abstract into function
return dom.parseFromString(response, "text/html")
}),
catchError(() => {
setLocation(url)
return NEVER
})
)
),
share()
)
}

View File

@ -59,7 +59,7 @@ export function getViewportSize(): ViewportSize {
* @return Viewport size observable
*/
export function watchViewportSize(): Observable<ViewportSize> {
return fromEvent(window, "resize")
return fromEvent(window, "resize", { passive: true })
.pipe(
map(getViewportSize),
startWith(getViewportSize())

View File

@ -49,13 +49,6 @@ export interface SearchQuery {
focus: boolean /* Query focus */
}
/* ------------------------------------------------------------------------- */
/**
* Search query transform
*/
export type SearchQueryTransform = (value: string) => string
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */

View File

@ -146,19 +146,17 @@ export function initialize(config: unknown) {
if (!isConfig(config))
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
/* Set up user interface observables */
/* Set up subjects */
const document$ = watchDocument()
const location$ = watchLocation()
/* Set up user interface observables */
const base$ = watchLocationBase(config.base, { location$ })
const hash$ = watchLocationHash()
const viewport$ = watchViewport()
const tablet$ = watchMedia("(min-width: 960px)")
const screen$ = watchMedia("(min-width: 1220px)")
/* Set up document observable */
const document$ = config.features.includes("instant")
? watchDocument({ location$ })
: watchDocument()
/* ----------------------------------------------------------------------- */
/* Set up component bindings */

View File

@ -84,6 +84,6 @@ export function setupDialog(
)
.subscribe()
/* Return dialog subject */
/* Return dialog */
return dialog$
}

View File

@ -20,9 +20,11 @@
* IN THE SOFTWARE.
*/
import { Observable, Subject, fromEvent, merge } from "rxjs"
import { NEVER, Observable, Subject, fromEvent, merge } from "rxjs"
import { ajax } from "rxjs//ajax"
import {
bufferCount,
catchError,
debounceTime,
distinctUntilChanged,
distinctUntilKeyChanged,
@ -31,6 +33,8 @@ import {
pluck,
sample,
share,
skip,
switchMap,
withLatestFrom
} from "rxjs/operators"
@ -39,6 +43,8 @@ import {
ViewportOffset,
getElement,
isAnchorLocation,
replaceElement,
setLocation,
setLocationHash,
setViewportOffset
} from "browser"
@ -61,7 +67,7 @@ interface State {
* Setup options
*/
interface SetupOptions {
document$: Observable<Document> /* Document observable */
document$: Subject<Document> /* Document subject */
viewport$: Observable<Viewport> /* Viewport observable */
link$: Observable<HTMLAnchorElement> /* Internal link observable */
location$: Subject<URL> /* Location subject */
@ -113,6 +119,34 @@ export function setupInstantLoading(
)
.subscribe(location$)
const dom = new DOMParser()
location$
.pipe(
distinctUntilKeyChanged("pathname"),
skip(1),
/* Fetch document */
switchMap(url => ajax({
url: url.href,
responseType: "text",
withCredentials: true
})
.pipe(
map(({ response }): Document => {
// TODO: only do this, if
history.pushState({}, "", url.toString()) // TODO: abstract into function
return dom.parseFromString(response, "text/html")
}),
catchError(() => {
setLocation(url)
return NEVER
})
)
),
share()
)
.subscribe(document$)
/* History: debounce update of viewport offset */
viewport$
.pipe(
@ -174,7 +208,7 @@ export function setupInstantLoading(
typeof next !== "undefined" &&
typeof prev !== "undefined"
) {
prev.replaceWith(next)
replaceElement(prev, next)
}
}
})