mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-22 13:53:43 -04:00
Refactored instant loading and some other components
This commit is contained in:
@@ -22,10 +22,10 @@
|
||||
|
||||
export * from "./document"
|
||||
export * from "./element"
|
||||
export * from "./fetch"
|
||||
export * from "./keyboard"
|
||||
export * from "./location"
|
||||
export * from "./media"
|
||||
export * from "./request"
|
||||
export * from "./toggle"
|
||||
export * from "./viewport"
|
||||
export * from "./worker"
|
||||
|
||||
@@ -20,8 +20,14 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable, fromEvent, merge } from "rxjs"
|
||||
import { filter, map, mapTo, startWith } from "rxjs/operators"
|
||||
import { NEVER, Observable, fromEvent, merge } from "rxjs"
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
mapTo,
|
||||
startWith,
|
||||
switchMap
|
||||
} from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@@ -57,3 +63,24 @@ export function watchPrint(): Observable<void> {
|
||||
mapTo(undefined)
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Toggle an observable with another one
|
||||
*
|
||||
* @template T - Data type
|
||||
*
|
||||
* @param toggle$ - Toggle observable
|
||||
* @param factory - Observable factory
|
||||
*
|
||||
* @returns Toggled observable
|
||||
*/
|
||||
export function at<T>(
|
||||
toggle$: Observable<boolean>, factory: () => Observable<T>
|
||||
): Observable<T> {
|
||||
return toggle$
|
||||
.pipe(
|
||||
switchMap(active => active ? factory() : NEVER)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,21 +28,29 @@ import {
|
||||
switchMap
|
||||
} from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Data
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* XML parser
|
||||
*/
|
||||
const dom = new DOMParser()
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Fetch given URL as JSON
|
||||
* Fetch the given URL
|
||||
*
|
||||
* @param url - Request URL
|
||||
* @param options - Request options
|
||||
*
|
||||
* @returns Response observable
|
||||
*/
|
||||
export function request(
|
||||
url: string, options: RequestInit = { credentials: "same-origin" }
|
||||
): Observable<Response> {
|
||||
return from(fetch(url, options))
|
||||
.pipe(
|
||||
filter(res => res.status === 200),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch JSON from the given URL
|
||||
*
|
||||
* @template T - Data type
|
||||
*
|
||||
@@ -51,32 +59,31 @@ const dom = new DOMParser()
|
||||
*
|
||||
* @returns Data observable
|
||||
*/
|
||||
export function fetchJSON<T>(
|
||||
url: string, options: RequestInit = { credentials: "same-origin" }
|
||||
export function requestJSON<T>(
|
||||
url: string, options?: RequestInit
|
||||
): Observable<T> {
|
||||
return from(fetch(url, options))
|
||||
return request(url, options)
|
||||
.pipe(
|
||||
filter(res => res.status === 200),
|
||||
switchMap(res => res.json()),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch given URL as XML
|
||||
* Fetch XML from the given URL
|
||||
*
|
||||
* @param url - Request URL
|
||||
* @param options - Request options
|
||||
*
|
||||
* @returns Data observable
|
||||
*/
|
||||
export function fetchXML(
|
||||
url: string, options: RequestInit = { credentials: "same-origin" }
|
||||
export function requestXML(
|
||||
url: string, options?: RequestInit
|
||||
): Observable<Document> {
|
||||
return from(fetch(url, options))
|
||||
const dom = new DOMParser()
|
||||
return request(url, options)
|
||||
.pipe(
|
||||
filter(res => res.status === 200),
|
||||
switchMap(res => res.json()),
|
||||
switchMap(res => res.text()),
|
||||
map(res => dom.parseFromString(res, "text/xml")),
|
||||
shareReplay(1)
|
||||
)
|
||||
@@ -64,14 +64,14 @@ export interface Dialog {
|
||||
* Watch options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
message$: Subject<string> /* Message subject */
|
||||
alert$: Subject<string> /* Alert subject */
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
message$: Subject<string> /* Message subject */
|
||||
alert$: Subject<string> /* Alert subject */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -87,9 +87,9 @@ interface MountOptions {
|
||||
* @returns Dialog observable
|
||||
*/
|
||||
export function watchDialog(
|
||||
_el: HTMLElement, { message$ }: WatchOptions
|
||||
_el: HTMLElement, { alert$ }: WatchOptions
|
||||
): Observable<Dialog> {
|
||||
return message$
|
||||
return alert$
|
||||
.pipe(
|
||||
switchMap(message => merge(
|
||||
of(true),
|
||||
@@ -111,7 +111,7 @@ export function watchDialog(
|
||||
* @returns Dialog component observable
|
||||
*/
|
||||
export function mountDialog(
|
||||
el: HTMLElement, { message$ }: MountOptions
|
||||
el: HTMLElement, options: MountOptions
|
||||
): Observable<Component<Dialog>> {
|
||||
const internal$ = new Subject<Dialog>()
|
||||
internal$
|
||||
@@ -127,7 +127,7 @@ export function mountDialog(
|
||||
})
|
||||
|
||||
/* Create and return component */
|
||||
return watchDialog(el, { message$ })
|
||||
return watchDialog(el, options)
|
||||
.pipe(
|
||||
tap(internal$),
|
||||
finalize(() => internal$.complete()),
|
||||
|
||||
@@ -97,7 +97,8 @@ export function watchHeader(
|
||||
distinctUntilChanged((a, b) => (
|
||||
a.sticky === b.sticky &&
|
||||
a.height === b.height
|
||||
))
|
||||
)),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -134,7 +135,6 @@ export function mountHeader(
|
||||
main$.subscribe(main => internal$.next(main))
|
||||
return header$
|
||||
.pipe(
|
||||
map(state => ({ ref: el, ...state })),
|
||||
shareReplay(1)
|
||||
map(state => ({ ref: el, ...state }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
distinctUntilChanged,
|
||||
distinctUntilKeyChanged,
|
||||
map,
|
||||
shareReplay,
|
||||
switchMap
|
||||
} from "rxjs/operators"
|
||||
|
||||
@@ -120,7 +119,6 @@ export function watchMain(
|
||||
a.offset === b.offset &&
|
||||
a.height === b.height &&
|
||||
a.active === b.active
|
||||
)),
|
||||
shareReplay(1)
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Observable, merge } from "rxjs"
|
||||
import { filter, sample, take } from "rxjs/operators"
|
||||
|
||||
import { configuration } from "~/_"
|
||||
import { fetchJSON, getElementOrThrow } from "~/browser"
|
||||
import { requestJSON, getElementOrThrow } from "~/browser"
|
||||
import {
|
||||
SearchIndex,
|
||||
isSearchQueryMessage,
|
||||
@@ -59,7 +59,7 @@ export type Search =
|
||||
* @returns Promise resolving with search index
|
||||
*/
|
||||
function fetchSearchIndex(url: string) {
|
||||
return __search?.index || fetchJSON<SearchIndex>(url)
|
||||
return __search?.index || requestJSON<SearchIndex>(url)
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Repo, User } from "github-types"
|
||||
import { Observable } from "rxjs"
|
||||
import { defaultIfEmpty, map } from "rxjs/operators"
|
||||
|
||||
import { fetchJSON } from "~/browser"
|
||||
import { requestJSON } from "~/browser"
|
||||
import { round } from "~/utilities"
|
||||
|
||||
import { SourceFacts } from "../_"
|
||||
@@ -47,7 +47,7 @@ export function fetchSourceFactsFromGitHub(
|
||||
const url = typeof repo !== "undefined"
|
||||
? `https://api.github.com/repos/${user}/${repo}`
|
||||
: `https://api.github.com/users/${user}`
|
||||
return fetchJSON<Repo & User>(url)
|
||||
return requestJSON<Repo & User>(url)
|
||||
.pipe(
|
||||
map(data => {
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ProjectSchema } from "gitlab"
|
||||
import { Observable } from "rxjs"
|
||||
import { defaultIfEmpty, map } from "rxjs/operators"
|
||||
|
||||
import { fetchJSON } from "~/browser"
|
||||
import { requestJSON } from "~/browser"
|
||||
import { round } from "~/utilities"
|
||||
|
||||
import { SourceFacts } from "../_"
|
||||
@@ -45,7 +45,7 @@ export function fetchSourceFactsFromGitLab(
|
||||
base: string, project: string
|
||||
): Observable<SourceFacts> {
|
||||
const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}`
|
||||
return fetchJSON<ProjectSchema>(url)
|
||||
return requestJSON<ProjectSchema>(url)
|
||||
.pipe(
|
||||
map(({ star_count, forks_count }) => ([
|
||||
`${round(star_count)} Stars`,
|
||||
|
||||
@@ -21,12 +21,21 @@
|
||||
*/
|
||||
|
||||
import "focus-visible"
|
||||
import { NEVER, Observable, Subject, merge } from "rxjs"
|
||||
import { switchMap } from "rxjs/operators"
|
||||
|
||||
import { Subject, defer, merge } from "rxjs"
|
||||
import {
|
||||
map,
|
||||
mergeWith,
|
||||
shareReplay,
|
||||
switchMap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { feature } from "./_"
|
||||
import {
|
||||
at,
|
||||
getElementOrThrow,
|
||||
getElements,
|
||||
watchDocument,
|
||||
watchLocation,
|
||||
watchLocationTarget,
|
||||
watchMedia,
|
||||
watchPrint,
|
||||
@@ -46,7 +55,8 @@ import {
|
||||
watchMain
|
||||
} from "./components"
|
||||
import {
|
||||
setupClipboardJS
|
||||
setupClipboardJS,
|
||||
setupInstantLoading
|
||||
} from "./integrations"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -57,61 +67,53 @@ import {
|
||||
document.documentElement.classList.remove("no-js")
|
||||
document.documentElement.classList.add("js")
|
||||
|
||||
/* Set up subjects */
|
||||
/* Set up navigation observables */
|
||||
const document$ = watchDocument()
|
||||
const location$ = watchLocation()
|
||||
const target$ = watchLocationTarget()
|
||||
|
||||
/* Set up user interface observables */
|
||||
/* Set up media observables */
|
||||
const viewport$ = watchViewport()
|
||||
const tablet$ = watchMedia("(min-width: 960px)")
|
||||
const screen$ = watchMedia("(min-width: 1220px)")
|
||||
const print$ = watchPrint()
|
||||
|
||||
// these elements MUST be available
|
||||
const header = getElementOrThrow("[data-md-component=header]")
|
||||
const main = getElementOrThrow("[data-md-component=main]")
|
||||
/* Set up Clipboard.js integration */
|
||||
const alert$ = new Subject<string>()
|
||||
setupClipboardJS({ alert$ })
|
||||
|
||||
const header$ = watchHeader(header)
|
||||
const main$ = watchMain(main, { header$, viewport$ })
|
||||
/* Set up instant loading, if enabled */
|
||||
if (feature("navigation.instant"))
|
||||
setupInstantLoading({ document$, location$, viewport$ })
|
||||
|
||||
/* Setup Clipboard.js integration */
|
||||
const message$ = new Subject<string>()
|
||||
setupClipboardJS({ message$ })
|
||||
/* Set up header observable */
|
||||
const header$ = watchHeader(
|
||||
getElementOrThrow("[data-md-component=header]")
|
||||
)
|
||||
|
||||
// TODO: watchElements + general mount function that takes a factory...
|
||||
// + a toggle function (optionally)
|
||||
/* Set up main area observable */
|
||||
const main$ = document$
|
||||
.pipe(
|
||||
map(() => getElementOrThrow("[data-md-component=main]")),
|
||||
switchMap(el => watchMain(el, { viewport$, header$ })),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
// TODO: catch errors on all components when mounting, so one error doesn't
|
||||
// take down the whole site.
|
||||
|
||||
const app$ = merge(
|
||||
|
||||
/* Content */
|
||||
...getElements("[data-md-component=content]")
|
||||
.map(child => mountContent(child, { target$, viewport$, print$ })),
|
||||
/* Set up control component observables */
|
||||
const control$ = merge(
|
||||
|
||||
/* Dialog */
|
||||
...getElements("[data-md-component=dialog]")
|
||||
.map(child => mountDialog(child, { message$ })),
|
||||
.map(child => mountDialog(child, { alert$ })),
|
||||
|
||||
/* Header */
|
||||
/* Header */
|
||||
...getElements("[data-md-component=header]")
|
||||
.map(child => mountHeader(child, { viewport$, header$, main$ })),
|
||||
|
||||
/* Header title */
|
||||
...getElements("[data-md-component=header-title]")
|
||||
.map(child => mountHeaderTitle(child, { viewport$, header$ })),
|
||||
|
||||
/* Search */
|
||||
...getElements("[data-md-component=search]")
|
||||
.map(child => mountSearch(child)),
|
||||
|
||||
/* Sidebar */
|
||||
...getElements("[data-md-component=sidebar]")
|
||||
.map(child => child.getAttribute("data-md-type") === "navigation"
|
||||
? at(screen$, () => mountSidebar(child, { viewport$, header$, main$ }))
|
||||
: at(tablet$, () => mountSidebar(child, { viewport$, header$, main$ }))
|
||||
),
|
||||
|
||||
/* Repository information */
|
||||
...getElements("[data-md-component=source]")
|
||||
.map(child => mountSource(child as HTMLAnchorElement)),
|
||||
@@ -119,30 +121,46 @@ const app$ = merge(
|
||||
/* Navigation tabs */
|
||||
...getElements("[data-md-component=tabs]")
|
||||
.map(child => mountTabs(child, { viewport$, header$ })),
|
||||
)
|
||||
|
||||
/* Set up content component observables */
|
||||
const content$ = defer(() => merge(
|
||||
|
||||
/* Content */
|
||||
...getElements("[data-md-component=content]")
|
||||
.map(child => mountContent(child, { target$, viewport$, print$ })),
|
||||
|
||||
/* Header title */
|
||||
...getElements("[data-md-component=header-title]")
|
||||
.map(child => mountHeaderTitle(child, { viewport$, header$ })),
|
||||
|
||||
/* Sidebar */
|
||||
...getElements("[data-md-component=sidebar]")
|
||||
.map(child => child.getAttribute("data-md-type") === "navigation"
|
||||
? at(screen$, () => mountSidebar(child, { viewport$, header$, main$ }))
|
||||
: at(tablet$, () => mountSidebar(child, { viewport$, header$, main$ }))
|
||||
),
|
||||
|
||||
/* Table of contents */
|
||||
...getElements("[data-md-component=toc]")
|
||||
.map(child => mountTableOfContents(child, { viewport$, header$ })),
|
||||
)
|
||||
))
|
||||
|
||||
// eslint-disable-next-line
|
||||
app$.subscribe(console.log)
|
||||
/* Set up component observables */
|
||||
const component$ = document$
|
||||
.pipe(
|
||||
switchMap(() => content$),
|
||||
mergeWith(control$)
|
||||
)
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Test
|
||||
*
|
||||
* @param toggle$ - Toggle observable
|
||||
* @param factory - Observable factory
|
||||
*
|
||||
* @returns New observable
|
||||
*/
|
||||
function at<T>(
|
||||
toggle$: Observable<boolean>, factory: () => Observable<T>
|
||||
) {
|
||||
return toggle$
|
||||
.pipe(
|
||||
switchMap(active => active ? factory() : NEVER),
|
||||
)
|
||||
/* Export to window */
|
||||
export {
|
||||
document$,
|
||||
component$,
|
||||
viewport$,
|
||||
location$,
|
||||
target$,
|
||||
screen$,
|
||||
tablet$,
|
||||
print$
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import { translation } from "~/_"
|
||||
* Setup options
|
||||
*/
|
||||
interface SetupOptions {
|
||||
message$: Subject<string> /* Message subject */
|
||||
alert$: Subject<string> /* Alert subject */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -46,13 +46,13 @@ interface SetupOptions {
|
||||
* @param options - Options
|
||||
*/
|
||||
export function setupClipboardJS(
|
||||
{ message$ }: SetupOptions
|
||||
{ alert$ }: SetupOptions
|
||||
): void {
|
||||
if (!ClipboardJS.isSupported()) {
|
||||
if (ClipboardJS.isSupported()) {
|
||||
new Observable<ClipboardJS.Event>(subscriber => {
|
||||
new ClipboardJS("[data-clipboard-target], [data-clipboard-text]")
|
||||
.on("success", ev => subscriber.next(ev))
|
||||
})
|
||||
.subscribe(() => message$.next(translation("clipboard.copied")))
|
||||
.subscribe(() => alert$.next(translation("clipboard.copied")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-self-assign": "off"
|
||||
"no-self-assign": "off",
|
||||
"no-null/no-null": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,42 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable, Subject, fromEvent } from "rxjs"
|
||||
import {
|
||||
NEVER,
|
||||
Observable,
|
||||
Subject,
|
||||
fromEvent,
|
||||
merge,
|
||||
of
|
||||
} from "rxjs"
|
||||
import {
|
||||
bufferCount,
|
||||
catchError,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
distinctUntilKeyChanged,
|
||||
filter,
|
||||
map,
|
||||
sample,
|
||||
share,
|
||||
skip,
|
||||
skipUntil,
|
||||
switchMap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { Viewport, ViewportOffset, getElement } from "~/browser"
|
||||
import { configuration } from "~/_"
|
||||
import {
|
||||
Viewport,
|
||||
ViewportOffset,
|
||||
getElement,
|
||||
getElements,
|
||||
replaceElement,
|
||||
request,
|
||||
requestXML,
|
||||
setLocation,
|
||||
setLocationHash,
|
||||
setViewportOffset
|
||||
} from "~/browser"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@@ -49,6 +82,42 @@ interface SetupOptions {
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Preprocess a list of URLs
|
||||
*
|
||||
* This function replaces the `site_url` in the sitemap with the actual base
|
||||
* URL, to allow instant loading to work in occasions like Netlify previews.
|
||||
*
|
||||
* @param urls - URLs
|
||||
*
|
||||
* @returns Processed URLs
|
||||
*/
|
||||
function preprocess(urls: string[]): string[] {
|
||||
if (urls.length < 2)
|
||||
return urls
|
||||
|
||||
/* Compute references URLs */
|
||||
const [root, next] = urls.sort((a, b) => a.length - b.length)
|
||||
|
||||
/* Compute common prefix */
|
||||
let index = 0
|
||||
if (root === next)
|
||||
index = root.length
|
||||
else
|
||||
while (root.charCodeAt(index) === root.charCodeAt(index))
|
||||
index++
|
||||
|
||||
/* Replace common prefix (i.e. base) with effective base */
|
||||
const config = configuration()
|
||||
return urls.map(url => (
|
||||
url.replace(root.slice(0, index), `${config.base}/`)
|
||||
))
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -72,12 +141,12 @@ interface SetupOptions {
|
||||
* history will be irreversibly overwritten. In case the request fails, the
|
||||
* location change is dispatched regularly.
|
||||
*
|
||||
* @param urls - URLs to load with XHR
|
||||
* @param options - Options
|
||||
*/
|
||||
export function setupInstantLoading(
|
||||
urls: string[], { document$, location$, viewport$ }: SetupOptions
|
||||
{ document$, location$, viewport$ }: SetupOptions
|
||||
): void {
|
||||
const config = configuration()
|
||||
if (location.protocol === "file:")
|
||||
return
|
||||
|
||||
@@ -97,6 +166,149 @@ export function setupInstantLoading(
|
||||
if (typeof favicon !== "undefined")
|
||||
favicon.href = favicon.href
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.log(urls, document$, location$, viewport$)
|
||||
/* Intercept internal navigation */
|
||||
const push$ = requestXML(`${config.base}/sitemap.xml`)
|
||||
.pipe(
|
||||
map(sitemap => preprocess(getElements("loc", sitemap)
|
||||
.map(node => node.textContent!)
|
||||
)),
|
||||
switchMap(urls => 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 && !el.target && urls.includes(el.href)) {
|
||||
ev.preventDefault()
|
||||
return of<HistoryState>({
|
||||
url: new URL(el.href)
|
||||
})
|
||||
}
|
||||
}
|
||||
return NEVER
|
||||
})
|
||||
)
|
||||
),
|
||||
share()
|
||||
)
|
||||
|
||||
/* Intercept history back and forward */
|
||||
const pop$ = fromEvent<PopStateEvent>(window, "popstate")
|
||||
.pipe(
|
||||
filter(ev => ev.state !== null),
|
||||
map(ev => ({
|
||||
url: new URL(location.href),
|
||||
offset: ev.state
|
||||
} as HistoryState)),
|
||||
share()
|
||||
)
|
||||
|
||||
/* Emit location change */
|
||||
merge(push$, pop$)
|
||||
.pipe(
|
||||
distinctUntilChanged((a, b) => a.url.href === b.url.href),
|
||||
map(({ url }) => url)
|
||||
)
|
||||
.subscribe(location$)
|
||||
|
||||
/* Fetch document via `XMLHTTPRequest` */
|
||||
const response$ = location$
|
||||
.pipe(
|
||||
distinctUntilKeyChanged("pathname"),
|
||||
skip(1),
|
||||
switchMap(url => request(url.href)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
setLocation(url)
|
||||
return NEVER
|
||||
})
|
||||
)
|
||||
),
|
||||
share()
|
||||
)
|
||||
|
||||
/* Set new location via `history.pushState` */
|
||||
push$
|
||||
.pipe(
|
||||
sample(response$)
|
||||
)
|
||||
.subscribe(({ url }) => {
|
||||
history.pushState({}, "", url.toString())
|
||||
})
|
||||
|
||||
/* Parse and emit fetched document */
|
||||
const dom = new DOMParser()
|
||||
response$
|
||||
.pipe(
|
||||
switchMap(res => res.text()),
|
||||
map(res => dom.parseFromString(res, "text/html"))
|
||||
)
|
||||
.subscribe(document$)
|
||||
|
||||
/* Emit history state change */
|
||||
merge(push$, pop$)
|
||||
.pipe(
|
||||
sample(document$)
|
||||
)
|
||||
.subscribe(({ url, offset }) => {
|
||||
if (url.hash && !offset)
|
||||
setLocationHash(url.hash)
|
||||
else
|
||||
setViewportOffset(offset || { y: 0 })
|
||||
})
|
||||
|
||||
/* Replace components */
|
||||
document$
|
||||
.pipe(
|
||||
skip(1)
|
||||
)
|
||||
.subscribe(replacement => {
|
||||
|
||||
/* Replace meta tags and components */
|
||||
for (const selector of [
|
||||
|
||||
/* Meta tags */
|
||||
"title",
|
||||
"link[rel='canonical']",
|
||||
"meta[name='author']",
|
||||
"meta[name='description']",
|
||||
|
||||
/* Components */
|
||||
"[data-md-component=announce]",
|
||||
"[data-md-component=header-title]",
|
||||
"[data-md-component=container]",
|
||||
"[data-md-component=skip]"
|
||||
]) {
|
||||
const next = getElement(selector, replacement)
|
||||
const prev = getElement(selector)
|
||||
if (
|
||||
typeof next !== "undefined" &&
|
||||
typeof prev !== "undefined"
|
||||
) {
|
||||
replaceElement(prev, next)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/* Debounce update of viewport offset */
|
||||
viewport$
|
||||
.pipe(
|
||||
skipUntil(push$),
|
||||
debounceTime(250),
|
||||
distinctUntilKeyChanged("offset")
|
||||
)
|
||||
.subscribe(({ offset }) => {
|
||||
history.replaceState(offset, "")
|
||||
})
|
||||
|
||||
/* Set viewport offset from history */
|
||||
merge(push$, pop$)
|
||||
.pipe(
|
||||
bufferCount(2, 1),
|
||||
filter(([a, b]) => a.url.pathname === b.url.pathname),
|
||||
map(([, state]) => state)
|
||||
)
|
||||
.subscribe(({ offset }) => {
|
||||
setViewportOffset(offset || { y: 0 })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -346,11 +346,11 @@
|
||||
{% block footer %}
|
||||
{% include "partials/footer.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Dialog -->
|
||||
<div class="md-dialog" data-md-component="dialog">
|
||||
<div class="md-dialog__inner md-typeset"></div>
|
||||
</div>
|
||||
<!-- Dialog -->
|
||||
<div class="md-dialog" data-md-component="dialog">
|
||||
<div class="md-dialog__inner md-typeset"></div>
|
||||
</div>
|
||||
|
||||
<!-- Theme-related configuration -->
|
||||
|
||||
Reference in New Issue
Block a user