mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Merge branch 'refactor/rxjs-typescript' into feature/landing-page
This commit is contained in:
commit
9c48baa791
@ -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
2
material/assets/javascripts/bundle.edc2ff56.min.js
vendored
Normal file
2
material/assets/javascripts/bundle.edc2ff56.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/javascripts/bundle.edc2ff56.min.js.map
Normal file
1
material/assets/javascripts/bundle.edc2ff56.min.js.map
Normal file
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
1
material/assets/javascripts/vendor.c1fcc1cc.min.js.map
Normal file
1
material/assets/javascripts/vendor.c1fcc1cc.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,8 +1,8 @@
|
||||
{
|
||||
"assets/javascripts/bundle.js": "assets/javascripts/bundle.eb3d5d63.min.js",
|
||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.eb3d5d63.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.edc2ff56.min.js",
|
||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.edc2ff56.min.js.map",
|
||||
"assets/javascripts/vendor.js": "assets/javascripts/vendor.c1fcc1cc.min.js",
|
||||
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.c1fcc1cc.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",
|
||||
|
@ -174,8 +174,8 @@
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% block scripts %}
|
||||
<script src="{{ 'assets/javascripts/vendor.3340e0de.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/bundle.eb3d5d63.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/vendor.c1fcc1cc.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/bundle.edc2ff56.min.js' | url }}"></script>
|
||||
{%- set translations = {} -%}
|
||||
{%- for key in [
|
||||
"clipboard.copy",
|
||||
|
@ -52,7 +52,7 @@ theme:
|
||||
language: en
|
||||
features:
|
||||
- tabs
|
||||
# - instant
|
||||
- instant
|
||||
palette:
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
@ -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$
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
@ -49,6 +49,7 @@ export function getLocationHash(): string {
|
||||
export function setLocationHash(hash: string): void {
|
||||
const el = document.createElement("a")
|
||||
el.href = hash
|
||||
el.addEventListener("click", ev => ev.stopPropagation())
|
||||
el.click()
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -49,13 +49,6 @@ export interface SearchQuery {
|
||||
focus: boolean /* Query focus */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search query transform
|
||||
*/
|
||||
export type SearchQueryTransform = (value: string) => string
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
@ -20,7 +20,8 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// TODO: remove this after we finished refactoring
|
||||
// DISCLAIMER: this file is still WIP. There're some refactoring opportunities
|
||||
// which must be tackled after we gathered some feedback on v5.
|
||||
// tslint:disable
|
||||
|
||||
import "../stylesheets/main.scss"
|
||||
@ -32,8 +33,6 @@ import {
|
||||
combineLatest,
|
||||
animationFrameScheduler,
|
||||
fromEvent,
|
||||
of,
|
||||
NEVER,
|
||||
from
|
||||
} from "rxjs"
|
||||
import { ajax } from "rxjs/ajax"
|
||||
@ -46,7 +45,6 @@ import {
|
||||
observeOn,
|
||||
take,
|
||||
shareReplay,
|
||||
share,
|
||||
pluck
|
||||
} from "rxjs/operators"
|
||||
|
||||
@ -60,7 +58,6 @@ import {
|
||||
watchLocationHash,
|
||||
watchViewport,
|
||||
isLocalLocation,
|
||||
isAnchorLocation,
|
||||
setLocationHash,
|
||||
watchLocationBase
|
||||
} from "browser"
|
||||
@ -146,19 +143,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 */
|
||||
@ -256,7 +251,6 @@ export function initialize(config: unknown) {
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
const worker = setupSearchWorker(config.search.worker, {
|
||||
base$, index$
|
||||
})
|
||||
@ -300,10 +294,9 @@ export function initialize(config: unknown) {
|
||||
tap(() => setToggle("search", false)),
|
||||
delay(125), // ensure that it runs after the body scroll reset...
|
||||
)
|
||||
.subscribe(hash => setLocationHash(`#${hash}`)) // TODO: must be unified
|
||||
.subscribe(hash => setLocationHash(`#${hash}`))
|
||||
|
||||
// Scroll lock // document -> document$ => { body } !?
|
||||
// put into search...
|
||||
// TODO: scroll restoration must be centralized
|
||||
combineLatest([
|
||||
watchToggle("search"),
|
||||
tablet$,
|
||||
@ -314,7 +307,7 @@ export function initialize(config: unknown) {
|
||||
const active = toggle && !tablet
|
||||
return document$
|
||||
.pipe(
|
||||
delay(active ? 400 : 100), // TOOD: directly combine this with the hash!
|
||||
delay(active ? 400 : 100),
|
||||
observeOn(animationFrameScheduler),
|
||||
tap(({ body }) => active
|
||||
? setScrollLock(body, y)
|
||||
@ -327,57 +320,34 @@ export function initialize(config: unknown) {
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
/* Intercept internal link clicks */
|
||||
const link$ = fromEvent<MouseEvent>(document.body, "click")
|
||||
/* Always close drawer on click */
|
||||
fromEvent<MouseEvent>(document.body, "click")
|
||||
.pipe(
|
||||
filter(ev => !(ev.metaKey || ev.ctrlKey)),
|
||||
switchMap(ev => {
|
||||
filter(ev => {
|
||||
if (ev.target instanceof HTMLElement) {
|
||||
const el = ev.target.closest("a") // TODO: abstract as link click?
|
||||
if (el && isLocalLocation(el)) {
|
||||
if (!isAnchorLocation(el) && config.features.includes("instant"))
|
||||
ev.preventDefault()
|
||||
return of(el)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return NEVER
|
||||
}),
|
||||
share()
|
||||
return false
|
||||
})
|
||||
)
|
||||
|
||||
/* Always close drawer on click */
|
||||
link$.subscribe(() => {
|
||||
.subscribe(() => {
|
||||
setToggle("drawer", false)
|
||||
})
|
||||
|
||||
// instant loading
|
||||
if (config.features.includes("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"]` // reduce style computations
|
||||
])
|
||||
for (const el of getElements<HTMLLinkElement>(selector))
|
||||
el.href = el.href
|
||||
|
||||
setupInstantLoading({
|
||||
document$, link$, location$, viewport$
|
||||
})
|
||||
|
||||
}
|
||||
/* Enable instant loading, if not on file:// protocol */
|
||||
if (config.features.includes("instant") && location.protocol !== "file:")
|
||||
setupInstantLoading({ document$, location$, viewport$ })
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
// if we use a single tab outside of search, unhide all permalinks.
|
||||
// TODO: experimental. necessary!?
|
||||
/* Unhide permalinks on first tab */
|
||||
keyboard$
|
||||
.pipe(
|
||||
filter(key => key.mode === "global" && ["Tab"].includes(key.type)),
|
||||
filter(key => key.mode === "global" && key.type === "Tab"),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => {
|
||||
@ -391,6 +361,7 @@ export function initialize(config: unknown) {
|
||||
|
||||
/* Browser observables */
|
||||
document$,
|
||||
location$,
|
||||
viewport$,
|
||||
|
||||
/* Component observables */
|
||||
@ -402,7 +373,7 @@ export function initialize(config: unknown) {
|
||||
tabs$,
|
||||
toc$,
|
||||
|
||||
/* Integation observables */
|
||||
/* Integration observables */
|
||||
clipboard$,
|
||||
keyboard$,
|
||||
dialog$
|
||||
|
@ -25,7 +25,7 @@ import { NEVER, Observable, Subject, fromEventPattern } from "rxjs"
|
||||
import { mapTo, share, tap } from "rxjs/operators"
|
||||
|
||||
import { getElements } from "browser"
|
||||
import { renderClipboard } from "templates"
|
||||
import { renderClipboardButton } from "templates"
|
||||
import { translate } from "utilities"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -66,7 +66,7 @@ export function setupClipboard(
|
||||
blocks.forEach((block, index) => {
|
||||
const parent = block.parentElement!
|
||||
parent.id = `__code_${index}`
|
||||
parent.insertBefore(renderClipboard(parent.id), block)
|
||||
parent.insertBefore(renderClipboardButton(parent.id), block)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -84,6 +84,6 @@ export function setupDialog(
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
/* Return dialog subject */
|
||||
/* Return dialog */
|
||||
return dialog$
|
||||
}
|
||||
|
@ -20,9 +20,11 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable, Subject, fromEvent, merge } from "rxjs"
|
||||
import { NEVER, Observable, Subject, fromEvent, merge, of } 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,7 +43,11 @@ import {
|
||||
ViewportOffset,
|
||||
getElement,
|
||||
isAnchorLocation,
|
||||
isLocalLocation,
|
||||
replaceElement,
|
||||
setLocation,
|
||||
setLocationHash,
|
||||
setToggle,
|
||||
setViewportOffset
|
||||
} from "browser"
|
||||
|
||||
@ -61,10 +69,9 @@ interface State {
|
||||
* Setup options
|
||||
*/
|
||||
interface SetupOptions {
|
||||
document$: Observable<Document> /* Document observable */
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
link$: Observable<HTMLAnchorElement> /* Internal link observable */
|
||||
document$: Subject<Document> /* Document subject */
|
||||
location$: Subject<URL> /* Location subject */
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -74,20 +81,68 @@ interface SetupOptions {
|
||||
/**
|
||||
* Set up instant loading
|
||||
*
|
||||
* @param options - Options
|
||||
* When fetching, 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 could use the old location.
|
||||
*
|
||||
* @return TODO ?
|
||||
* This is the reason why we need to synchronize history events and the process
|
||||
* of fetching the document for navigation changes (except `popstate` events):
|
||||
*
|
||||
* 1. Fetch document via `XMLHTTPRequest`
|
||||
* 2. Set new location via `history.pushState`
|
||||
* 3. Parse and emit fetched document
|
||||
*
|
||||
* For `popstate` events, we must not use `history.pushState`, or the forward
|
||||
* history will be irreversibly overwritten. In case the request fails, the
|
||||
* location change is dispatched regularly.
|
||||
*
|
||||
* @param options - Options
|
||||
*/
|
||||
export function setupInstantLoading(
|
||||
{ document$, viewport$, link$, location$ }: SetupOptions
|
||||
) { // TODO: add return type
|
||||
const state$ = link$
|
||||
{ document$, viewport$, location$ }: SetupOptions
|
||||
): void {
|
||||
|
||||
/* Disable automatic scroll restoration */
|
||||
if ("scrollRestoration" in history)
|
||||
history.scrollRestoration = "manual"
|
||||
|
||||
/* Hack: ensure that reloads restore viewport offset */
|
||||
fromEvent(window, "beforeunload")
|
||||
.subscribe(() => {
|
||||
history.scrollRestoration = "auto"
|
||||
})
|
||||
|
||||
/* Hack: ensure absolute favicon link to omit 404s on document switch */
|
||||
const favicon = getElement<HTMLLinkElement>(`link[rel="shortcut icon"]`)
|
||||
if (typeof favicon !== "undefined")
|
||||
favicon.href = favicon.href // tslint:disable-line no-self-assignment
|
||||
|
||||
/* Intercept link clicks and convert to state change */
|
||||
const state$ = 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 && isLocalLocation(el)) {
|
||||
if (!isAnchorLocation(el))
|
||||
ev.preventDefault()
|
||||
return of(el)
|
||||
}
|
||||
}
|
||||
return NEVER
|
||||
}),
|
||||
map(el => ({ url: new URL(el.href) })),
|
||||
share<State>()
|
||||
)
|
||||
|
||||
/* Intercept internal links to dispatch */
|
||||
/* Always close search on link click */
|
||||
state$.subscribe(() => {
|
||||
setToggle("search", false)
|
||||
})
|
||||
|
||||
/* Filter state changes to dispatch */
|
||||
const push$ = state$
|
||||
.pipe(
|
||||
distinctUntilChanged((prev, next) => prev.url.href === next.url.href),
|
||||
@ -99,11 +154,11 @@ export function setupInstantLoading(
|
||||
const pop$ = fromEvent<PopStateEvent>(window, "popstate")
|
||||
.pipe(
|
||||
filter(ev => ev.state !== null),
|
||||
map<PopStateEvent, State>(ev => ({
|
||||
map(ev => ({
|
||||
url: new URL(location.href),
|
||||
offset: ev.state
|
||||
})),
|
||||
share()
|
||||
share<State>()
|
||||
)
|
||||
|
||||
/* Emit location change */
|
||||
@ -113,47 +168,58 @@ export function setupInstantLoading(
|
||||
)
|
||||
.subscribe(location$)
|
||||
|
||||
/* History: debounce update of viewport offset */
|
||||
viewport$
|
||||
/* Fetch document on location change */
|
||||
const ajax$ = location$
|
||||
.pipe(
|
||||
debounceTime(250),
|
||||
distinctUntilKeyChanged("offset")
|
||||
distinctUntilKeyChanged("pathname"),
|
||||
skip(1),
|
||||
switchMap(url => ajax({
|
||||
url: url.href,
|
||||
responseType: "text",
|
||||
withCredentials: true
|
||||
})
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
setLocation(url)
|
||||
return NEVER
|
||||
})
|
||||
)
|
||||
.subscribe(({ offset }) => {
|
||||
history.replaceState(offset, "")
|
||||
)
|
||||
)
|
||||
|
||||
/* Set new location as soon as the document was fetched */
|
||||
push$
|
||||
.pipe(
|
||||
sample(ajax$)
|
||||
)
|
||||
.subscribe(({ url }) => {
|
||||
history.pushState({}, "", url.toString())
|
||||
})
|
||||
|
||||
/* Apply viewport offset from history */
|
||||
merge(state$, pop$)
|
||||
/* Parse and emit document */
|
||||
const dom = new DOMParser()
|
||||
ajax$
|
||||
.pipe(
|
||||
bufferCount(2, 1),
|
||||
filter(([prev, next]) => {
|
||||
return prev.url.pathname === next.url.pathname
|
||||
&& !isAnchorLocation(next.url)
|
||||
}),
|
||||
map(([, state]) => state)
|
||||
map(({ response }) => dom.parseFromString(response, "text/html"))
|
||||
)
|
||||
.subscribe(({ offset }) => {
|
||||
setViewportOffset(offset || { y: 0 })
|
||||
})
|
||||
.subscribe(document$)
|
||||
|
||||
/* Intercept actual instant loading */
|
||||
/* Intercept instant loading */
|
||||
const instant$ = merge(push$, pop$)
|
||||
.pipe(
|
||||
sample(document$)
|
||||
)
|
||||
|
||||
// TODO: from here on, everything is beta.... ###############################
|
||||
|
||||
// TODO: this must be combined with search scroll restoration on mobile
|
||||
instant$.subscribe(({ url, offset }) => {
|
||||
if (url.hash && !offset) {
|
||||
// console.log("set hash!")
|
||||
setLocationHash(url.hash) // must delay, if search is open!
|
||||
setLocationHash(url.hash)
|
||||
} else {
|
||||
setViewportOffset(offset || { y: 0 })
|
||||
}
|
||||
})
|
||||
|
||||
/* Replace document metadata */
|
||||
instant$
|
||||
.pipe(
|
||||
withLatestFrom(document$)
|
||||
@ -174,8 +240,32 @@ export function setupInstantLoading(
|
||||
typeof next !== "undefined" &&
|
||||
typeof prev !== "undefined"
|
||||
) {
|
||||
prev.replaceWith(next)
|
||||
replaceElement(prev, next)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/* Debounce update of viewport offset */
|
||||
viewport$
|
||||
.pipe(
|
||||
debounceTime(250),
|
||||
distinctUntilKeyChanged("offset")
|
||||
)
|
||||
.subscribe(({ offset }) => {
|
||||
history.replaceState(offset, "")
|
||||
})
|
||||
|
||||
/* Set viewport offset from history */
|
||||
merge(state$, pop$)
|
||||
.pipe(
|
||||
bufferCount(2, 1),
|
||||
filter(([prev, next]) => {
|
||||
return prev.url.pathname === next.url.pathname
|
||||
&& !isAnchorLocation(next.url)
|
||||
}),
|
||||
map(([, state]) => state)
|
||||
)
|
||||
.subscribe(({ offset }) => {
|
||||
setViewportOffset(offset || { y: 0 })
|
||||
})
|
||||
}
|
||||
|
@ -53,14 +53,14 @@ const path =
|
||||
*
|
||||
* @return Element
|
||||
*/
|
||||
export function renderClipboard(
|
||||
export function renderClipboardButton(
|
||||
id: string
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
class={css.container}
|
||||
title={translate("clipboard.copy")}
|
||||
data-clipboard-target={`#${id} code`}
|
||||
data-clipboard-target={`#${id} > code`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d={path}></path>
|
||||
|
Loading…
Reference in New Issue
Block a user