diff --git a/src/assets/javascripts/observables/agent/document/_/index.ts b/src/assets/javascripts/observables/agent/document/_/index.ts new file mode 100644 index 000000000..4120f60ba --- /dev/null +++ b/src/assets/javascripts/observables/agent/document/_/index.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-2020 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 { Observable, fromEvent } from "rxjs" +import { mapTo, shareReplay } from "rxjs/operators" + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Observable for document load events + */ +const load$ = fromEvent(document, "DOMContentLoaded") + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch document + * + * @return Document observable + */ +export function watchDocument(): Observable { + return load$ + .pipe( + mapTo(document), + shareReplay(1) + ) +} diff --git a/src/assets/javascripts/observables/agent/document/index.ts b/src/assets/javascripts/observables/agent/document/index.ts index 7c34b5d76..5db8091e8 100644 --- a/src/assets/javascripts/observables/agent/document/index.ts +++ b/src/assets/javascripts/observables/agent/document/index.ts @@ -20,88 +20,5 @@ * IN THE SOFTWARE. */ -import { Observable, fromEvent } from "rxjs" -import { ajax } from "rxjs/ajax" -import { - distinctUntilChanged, - map, - mapTo, - pluck, - shareReplay, - skip, - startWith, - switchMap -} from "rxjs/operators" - -/* ---------------------------------------------------------------------------- - * Helper types - * ------------------------------------------------------------------------- */ - -/** - * Switch options - */ -interface SwitchOptions { - location$: Observable /* Location observable */ -} - -/* ---------------------------------------------------------------------------- - * Data - * ------------------------------------------------------------------------- */ - -/** - * Observable for document load events - */ -const load$ = fromEvent(document, "DOMContentLoaded") - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Watch document - * - * @return Document observable - */ -export function watchDocument(): Observable { - return load$ - .pipe( - mapTo(document), - shareReplay(1) - ) -} - -/** - * Watch document switch - * - * This function returns an observables 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 (e.g. when only the - * fragment identifier changes). - * - * @param options - Options - * - * @return Document switch observable - */ -export function watchDocumentSwitch( - { location$ }: SwitchOptions -): Observable { - return location$ - .pipe( - startWith(location.href), - map(url => url.replace(/#[^#]+$/, "")), - distinctUntilChanged(), - skip(1), - - /* Fetch document */ - switchMap(url => ajax({ - url, - responseType: "document", - withCredentials: true - }) - .pipe( - pluck("response") - ) - ), - shareReplay(1) - ) -} +export * from "./_" +export * from "./switch" diff --git a/src/assets/javascripts/observables/agent/document/switch/index.ts b/src/assets/javascripts/observables/agent/document/switch/index.ts new file mode 100644 index 000000000..92510fabe --- /dev/null +++ b/src/assets/javascripts/observables/agent/document/switch/index.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-2020 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 { Observable } from "rxjs" +import { ajax } from "rxjs/ajax" +import { + distinctUntilChanged, + map, + pluck, + shareReplay, + skip, + startWith, + switchMap +} from "rxjs/operators" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + location$: Observable /* Location observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch document switch + * + * This function returns an observables 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 (e.g. when only the + * fragment identifier changes). + * + * @param options - Options + * + * @return Document switch observable + */ +export function watchDocumentSwitch( + { location$ }: WatchOptions +): Observable { + return location$ + .pipe( + startWith(location.href), + map(url => url.replace(/#[^#]+$/, "")), + distinctUntilChanged(), + skip(1), + + /* Fetch document */ + switchMap(url => ajax({ + url, + responseType: "document", + withCredentials: true + }) + .pipe( + pluck("response") + ) + ), + shareReplay(1) + ) +} diff --git a/src/assets/javascripts/observables/agent/element/offset/index.ts b/src/assets/javascripts/observables/agent/element/offset/index.ts index 338475046..5971694b5 100644 --- a/src/assets/javascripts/observables/agent/element/offset/index.ts +++ b/src/assets/javascripts/observables/agent/element/offset/index.ts @@ -47,9 +47,9 @@ export interface ElementOffset { * ------------------------------------------------------------------------- */ /** - * Options + * Watch options */ -interface Options { +interface WatchOptions { viewport$: Observable /* Viewport observable */ } @@ -82,7 +82,7 @@ export function getElementOffset(el: HTMLElement): ElementOffset { * @return Element offset observable */ export function watchElementOffset( - el: HTMLElement, { viewport$ }: Options + el: HTMLElement, { viewport$ }: WatchOptions ): Observable { const scroll$ = fromEvent(el, "scroll") const size$ = viewport$ diff --git a/src/assets/javascripts/observables/agent/viewport/relative/index.ts b/src/assets/javascripts/observables/agent/viewport/relative/index.ts index 5763b91d9..e281e038f 100644 --- a/src/assets/javascripts/observables/agent/viewport/relative/index.ts +++ b/src/assets/javascripts/observables/agent/viewport/relative/index.ts @@ -31,9 +31,9 @@ import { Viewport } from "../_" * ------------------------------------------------------------------------- */ /** - * Options + * Watch options */ -interface Options { +interface WatchOptions { header$: Observable
/* Header observable */ viewport$: Observable /* Viewport observable */ } @@ -51,7 +51,7 @@ interface Options { * @return Viewport observable */ export function watchViewportFrom( - el: HTMLElement, { header$, viewport$ }: Options + el: HTMLElement, { header$, viewport$ }: WatchOptions ): Observable { return combineLatest([viewport$, header$]) .pipe( diff --git a/src/assets/javascripts/observables/agent/worker/index.ts b/src/assets/javascripts/observables/agent/worker/index.ts index d3cd942bd..64c86d520 100644 --- a/src/assets/javascripts/observables/agent/worker/index.ts +++ b/src/assets/javascripts/observables/agent/worker/index.ts @@ -21,7 +21,13 @@ */ import { Observable, Subject, fromEvent } from "rxjs" -import { pluck, share, switchMapTo, tap, throttle } from "rxjs/operators" +import { + pluck, + share, + switchMapTo, + tap, + throttle +} from "rxjs/operators" /* ---------------------------------------------------------------------------- * Types @@ -52,11 +58,11 @@ export interface WorkerHandler< * ------------------------------------------------------------------------- */ /** - * Options + * Watch options * * @template T - Worker message type */ -interface Options { +interface WatchOptions { tx$: Observable /* Message transmission observable */ } @@ -78,7 +84,7 @@ interface Options { * @return Worker message observable */ export function watchWorker( - worker: Worker, { tx$ }: Options + worker: Worker, { tx$ }: WatchOptions ): Observable { /* Intercept messages from web worker */ diff --git a/src/assets/javascripts/observables/anchor/index.ts b/src/assets/javascripts/observables/anchor/index.ts index 3cd8e555e..1e59a9da3 100644 --- a/src/assets/javascripts/observables/anchor/index.ts +++ b/src/assets/javascripts/observables/anchor/index.ts @@ -67,9 +67,9 @@ export interface AnchorList { * ------------------------------------------------------------------------- */ /** - * Options + * Watch options */ -interface Options { +interface WatchOptions { header$: Observable
/* Header observable */ viewport$: Observable /* Viewport observable */ } @@ -99,7 +99,7 @@ interface Options { * @return Anchor list observable */ export function watchAnchorList( - els: HTMLAnchorElement[], { header$, viewport$ }: Options + els: HTMLAnchorElement[], { header$, viewport$ }: WatchOptions ): Observable { const table = new Map() for (const el of els) { diff --git a/src/assets/javascripts/observables/main/_/index.ts b/src/assets/javascripts/observables/main/_/index.ts index 1158c6eff..06aac96ae 100644 --- a/src/assets/javascripts/observables/main/_/index.ts +++ b/src/assets/javascripts/observables/main/_/index.ts @@ -49,9 +49,9 @@ export interface Main { * ------------------------------------------------------------------------- */ /** - * Options + * Watch options */ -interface Options { +interface WatchOptions { header$: Observable
/* Header observable */ viewport$: Observable /* Viewport observable */ } @@ -73,7 +73,7 @@ interface Options { * @return Main area observable */ export function watchMain( - el: HTMLElement, { header$, viewport$ }: Options + el: HTMLElement, { header$, viewport$ }: WatchOptions ): Observable
{ /* Compute necessary adjustment for header */ diff --git a/src/assets/javascripts/observables/main/sidebar/index.ts b/src/assets/javascripts/observables/main/sidebar/index.ts index bbd0419bb..f751e8e9b 100644 --- a/src/assets/javascripts/observables/main/sidebar/index.ts +++ b/src/assets/javascripts/observables/main/sidebar/index.ts @@ -64,9 +64,9 @@ export interface Sidebar { * ------------------------------------------------------------------------- */ /** - * Options + * Watch options */ -interface Options { +interface WatchOptions { main$: Observable
/* Main area observable */ viewport$: Observable /* Viewport observable */ } @@ -89,7 +89,7 @@ interface Options { * @return Sidebar observable */ export function watchSidebar( - el: HTMLElement, { main$, viewport$ }: Options + el: HTMLElement, { main$, viewport$ }: WatchOptions ): Observable { /* Adjust for internal main area offset */ diff --git a/src/assets/javascripts/observables/search/query/index.ts b/src/assets/javascripts/observables/search/query/index.ts index 8828d991a..53ba973b0 100644 --- a/src/assets/javascripts/observables/search/query/index.ts +++ b/src/assets/javascripts/observables/search/query/index.ts @@ -47,9 +47,9 @@ export interface SearchQuery { * ------------------------------------------------------------------------- */ /** - * Options + * Watch options */ -interface Options { +interface WatchOptions { transform?(value: string): string /* Transformation function */ } @@ -87,7 +87,7 @@ function defaultTransform(value: string): string { * @return Search query observable */ export function watchSearchQuery( - el: HTMLInputElement, { transform = defaultTransform }: Options = {} + el: HTMLInputElement, { transform = defaultTransform }: WatchOptions = {} ): Observable { /* Intercept keyboard events */ diff --git a/src/assets/javascripts/observables/search/result/index.ts b/src/assets/javascripts/observables/search/result/index.ts index ec13603a6..97edf19e7 100644 --- a/src/assets/javascripts/observables/search/result/index.ts +++ b/src/assets/javascripts/observables/search/result/index.ts @@ -53,9 +53,9 @@ import { SearchQuery } from "../query" * ------------------------------------------------------------------------- */ /** - * Options + * Paint options */ -interface Options { +interface PaintOptions { query$: Observable /* Search query observable */ fetch$: Observable /* Search trigger observable */ } @@ -73,7 +73,7 @@ interface Options { * @return Operator function */ export function paintSearchResult( - el: HTMLElement, { query$, fetch$ }: Options + el: HTMLElement, { query$, fetch$ }: PaintOptions ): MonoTypeOperatorFunction { const list = getElement(".md-search-result__list", el)! const meta = getElement(".md-search-result__meta", el)! diff --git a/src/assets/javascripts/observables/toggle/index.ts b/src/assets/javascripts/observables/toggle/index.ts index 8e0946c8e..2458ea38b 100644 --- a/src/assets/javascripts/observables/toggle/index.ts +++ b/src/assets/javascripts/observables/toggle/index.ts @@ -20,29 +20,111 @@ * IN THE SOFTWARE. */ -import { Observable, fromEvent } from "rxjs" -import { map, startWith } from "rxjs/operators" +import { NEVER, Observable, fromEvent, of } from "rxjs" +import { + map, + shareReplay, + startWith, + switchMap, + take +} from "rxjs/operators" + +import { getElement } from "../agent" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Toggle + */ +export type Toggle = + | "drawer" /* Toggle for drawer */ + | "search" /* Toggle for search */ + +/** + * Toggle map + */ +export type ToggleMap = { + [P in Toggle]?: HTMLInputElement +} + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + document$: Observable /* Document observable */ +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Toggle map observable + */ +let toggles$: Observable /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ /** - * Set toggle + * Watch toggles with given names * - * 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 - * used `CustomEvent` to programmatically change the value of a toggle, but this - * is a much simpler and cleaner solution. + * @param names - Toggle names + * @param options - Options * - * @param el - Toggle element - * @param value - Toggle value + * @return Toggle map observable */ -export function setToggle( - el: HTMLInputElement, value: boolean -): void { - if (el.checked !== value) - el.click() +export function watchToggleMap( + names: Toggle[], { document$ }: WatchOptions +): Observable { + toggles$ = document$ + .pipe( + take(1), + + /* Build toggle map */ + map(document => names.reduce((toggles, name) => { + const el = getElement(`[data-md-toggle=${name}]`, document) + return { + ...toggles, + ...typeof el !== "undefined" ? { [name]: el } : {} + } + }, {})) + ) + + /* Return toggle map as hot observable */ + return toggles$ + .pipe( + shareReplay(1) + ) +} + +/** + * Retrieve a toggle + * + * @template T - Element type + * + * @param name - Toggle name + * + * @return Element observable + */ +export function useToggle( + name: Toggle +): Observable { + return toggles$ + .pipe( + switchMap(toggles => { + return typeof toggles[name] !== "undefined" + ? of(toggles[name]!) + : NEVER + }) + ) } /* ------------------------------------------------------------------------- */