From aeded5b844491024b3e01f014c510fbec6917e46 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Fri, 22 Nov 2019 16:46:22 +0100 Subject: [PATCH] Added component map observables --- src/assets/javascripts/component/_/index.ts | 127 ++++++++++++++++++ .../javascripts/component/anchor/_/index.ts | 3 +- .../javascripts/component/container/index.ts | 3 +- src/assets/javascripts/component/index.ts | 1 + .../javascripts/polyfill/details/index.ts | 2 +- src/assets/javascripts/ui/location/index.ts | 34 ++++- 6 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 src/assets/javascripts/component/_/index.ts diff --git a/src/assets/javascripts/component/_/index.ts b/src/assets/javascripts/component/_/index.ts new file mode 100644 index 000000000..b595994fb --- /dev/null +++ b/src/assets/javascripts/component/_/index.ts @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-2019 Martin Donath + * + * 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 { MonoTypeOperatorFunction, Observable, of, pipe } from "rxjs" +import { scan, shareReplay, tap } from "rxjs/operators" + +import { getElement } from "../../ui" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Components + */ +export type Component = + | "header" /* Header */ + | "title" /* Header title */ + | "search" /* Search */ + | "query" /* Search input */ + | "reset" /* Search reset */ + | "result" /* Search results */ + | "container" /* Container */ + | "tabs" /* Tabs */ + | "navigation" /* Navigation */ + | "toc" /* Table of contents */ + | "footer" /* Footer */ + +/** + * Component map + */ +export type ComponentMap = { + [P in Component]?: HTMLElement +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve the component to element mapping + * + * The document must be passed as a parameter to support retrieving elements + * from the document object returned through asynchronous loading. + * + * @param document - Document of reference + * + * @return Component map observable + */ +export function watchComponentMap( + document: Document +): Observable { + + /* Build component map */ + const map$ = of([ + "header", /* Header */ + "title", /* Header title */ + "search", /* Search */ + "query", /* Search input */ + "reset", /* Search reset */ + "result", /* Search results */ + "container", /* Container */ + "tabs", /* Tabs */ + "navigation", /* Navigation */ + "toc", /* Table of contents */ + "footer" /* Footer */ + ].reduce((map, name) => { + const el = getElement(`[data-md-component=${name}]`, document) + return { + ...map, + ...typeof el !== "undefined" ? { [name]: el } : {} + } + }, {})) + + /* Return component map as hot observable */ + return map$ + .pipe( + shareReplay({ bufferSize: 1, refCount: true }) + ) +} + +/* ------------------------------------------------------------------------- */ + +/** + * Paint component map from source observable + * + * This operator function will swap the components in the previous component + * map with the new components identified by the given names. + * + * @param names - Components to paint + * + * @return Operator function + */ +export function paintComponentMap( + names: Component[] = ["title", "tabs", "container", "footer"] +): MonoTypeOperatorFunction { + return pipe( + scan((prev, next) => { + for (const name of names) { + if (name in prev && typeof prev[name] !== "undefined") { + prev[name]!.replaceWith(next[name]!) + prev[name] = next[name] + } + } + return prev + }) + ) +} diff --git a/src/assets/javascripts/component/anchor/_/index.ts b/src/assets/javascripts/component/anchor/_/index.ts index cd2f2ddc1..938b0da38 100644 --- a/src/assets/javascripts/component/anchor/_/index.ts +++ b/src/assets/javascripts/component/anchor/_/index.ts @@ -100,7 +100,8 @@ export function watchAnchorList( ): Observable { const table = new Map() for (const el of els) { - const target = getElement(decodeURIComponent(el.hash)) + const id = decodeURIComponent(el.hash.substring(1)) + const target = getElement(`[id="${id}"]`) if (typeof target !== "undefined") table.set(el, target) } diff --git a/src/assets/javascripts/component/container/index.ts b/src/assets/javascripts/component/container/index.ts index 63c7751d1..4ae167ee5 100644 --- a/src/assets/javascripts/component/container/index.ts +++ b/src/assets/javascripts/component/container/index.ts @@ -25,8 +25,7 @@ import { distinctUntilChanged, map, pluck, - shareReplay, - tap + shareReplay } from "rxjs/operators" import { ViewportOffset, ViewportSize } from "../../ui" diff --git a/src/assets/javascripts/component/index.ts b/src/assets/javascripts/component/index.ts index b5453cf95..c7225272c 100644 --- a/src/assets/javascripts/component/index.ts +++ b/src/assets/javascripts/component/index.ts @@ -20,6 +20,7 @@ * IN THE SOFTWARE. */ +export * from "./_" export * from "./anchor" export * from "./container" export * from "./header" diff --git a/src/assets/javascripts/polyfill/details/index.ts b/src/assets/javascripts/polyfill/details/index.ts index 34f5fdd51..dd9771ece 100644 --- a/src/assets/javascripts/polyfill/details/index.ts +++ b/src/assets/javascripts/polyfill/details/index.ts @@ -69,7 +69,7 @@ document.addEventListener("DOMContentLoaded", () => { /* Retrieve all summaries and polyfill open/close functionality */ const summaries = document.querySelectorAll("details > summary") summaries.forEach(summary => { - summary.addEventListener("click", ev => { + summary.addEventListener("click", () => { const details = summary.parentNode as HTMLElement if (details.hasAttribute("open")) { details.removeAttribute("open") diff --git a/src/assets/javascripts/ui/location/index.ts b/src/assets/javascripts/ui/location/index.ts index 96fd14f52..de3536b6c 100644 --- a/src/assets/javascripts/ui/location/index.ts +++ b/src/assets/javascripts/ui/location/index.ts @@ -20,7 +20,7 @@ * IN THE SOFTWARE. */ -import { Observable, fromEvent } from "rxjs" +import { Observable, Subject, fromEvent } from "rxjs" import { filter, map, share, startWith } from "rxjs/operators" /* ---------------------------------------------------------------------------- @@ -28,24 +28,46 @@ import { filter, map, share, startWith } from "rxjs/operators" * ------------------------------------------------------------------------- */ /** - * Observable for window hash changes + * Observable for window hash change events */ -const hash$ = fromEvent(window, "hashchange") +const hashchange$ = fromEvent(window, "hashchange") + +/** + * Observable for window pop state events + */ +const popstate$ = fromEvent(window, "popstate") /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ +/** + * Create a subject to watch or alter the location + * + * @return Location subject + */ +export function watchLocation(): Subject { + const location$ = new Subject() + popstate$ + .pipe( + map(() => location.href) + ) + .subscribe(location$) + + /* Return subject */ + return location$ +} + /** * Create an observable to watch the location hash * * @return Location hash observable */ export function watchLocationHash(): Observable { - return hash$ + return hashchange$ .pipe( - map(() => document.location.hash), - startWith(document.location.hash), + map(() => location.hash), + startWith(location.hash), filter(hash => hash.length > 0), share() )