diff --git a/src/assets/javascripts/component/anchor/_/index.ts b/src/assets/javascripts/component/anchor/_/index.ts deleted file mode 100644 index 938b0da38..000000000 --- a/src/assets/javascripts/component/anchor/_/index.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * 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 { difference, reverse } from "ramda" -import { - MonoTypeOperatorFunction, - Observable, - animationFrameScheduler, - combineLatest, - pipe -} from "rxjs" -import { - distinctUntilChanged, - finalize, - map, - observeOn, - scan, - shareReplay, - switchMap, - tap -} from "rxjs/operators" - -import { ViewportOffset, ViewportSize, getElement } from "../../../ui" -import { Header } from "../../header" -import { - resetAnchorActive, - resetAnchorBlur, - setAnchorActive, - setAnchorBlur -} from "../element" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Anchor list - */ -export interface AnchorList { - done: HTMLAnchorElement[][] /* Done anchors */ - next: HTMLAnchorElement[][] /* Next anchors */ -} - -/* ---------------------------------------------------------------------------- - * Function types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - size$: Observable /* Viewport size observable */ - offset$: Observable /* Viewport offset observable */ - header$: Observable
/* Header observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Create an observable to watch an anchor list - * - * This is effectively a scroll-spy implementation which will account for the - * fixed header and automatically re-calculate anchor offsets when the viewport - * is resized. The returned observable will only emit if the anchor list needs - * to be repainted. - * - * This implementation tracks an anchor element's entire path starting from its - * level up to the top-most anchor element, e.g. `[h3, h2, h1]`. Although the - * Material theme currently doesn't make use of this information, it enables - * the styling of the entire hierarchy through customization. - * - * @param els - Anchor elements - * @param options - Options - * - * @return Anchor list observable - */ -export function watchAnchorList( - els: HTMLAnchorElement[], { size$, offset$, header$ }: WatchOptions -): Observable { - const table = new Map() - for (const el of els) { - const id = decodeURIComponent(el.hash.substring(1)) - const target = getElement(`[id="${id}"]`) - if (typeof target !== "undefined") - table.set(el, target) - } - - /* Compute necessary adjustment for header */ - const adjust$ = header$ - .pipe( - map(header => 18 + header.height) - ) - - /* Compute partition of done and next anchors */ - const partition$ = size$.pipe( - - /* Build index to map anchor paths to vertical offsets */ - map(() => { - let path: HTMLAnchorElement[] = [] - return [...table].reduce((index, [anchor, target]) => { - while (path.length) { - const last = table.get(path[path.length - 1])! - if (last.tagName >= target.tagName) { - path.pop() - } else { - break - } - } - return index.set( - reverse(path = [...path, anchor]), - target.offsetTop - ) - }, new Map()) - }), - - /* Re-compute partition when viewport offset changes */ - switchMap(index => combineLatest(offset$, adjust$) - .pipe( - scan(([done, next], [{ y }, adjust]) => { - - /* Look forward */ - while (next.length) { - const [, offset] = next[0] - if (offset - adjust < y) { - done = [...done, next.shift()!] - } else { - break - } - } - - /* Look backward */ - while (done.length) { - const [, offset] = done[done.length - 1] - if (offset - adjust >= y) { - next = [done.pop()!, ...next] - } else { - break - } - } - - /* Return partition */ - return [done, next] - }, [[], [...index]]), - distinctUntilChanged((a, b) => { - return a[0] === b[0] - && a[1] === b[1] - }) - ) - ) - ) - - /* Extract anchor list and return hot observable */ - return partition$ - .pipe( - map(([done, next]) => ({ - done: done.map(([path]) => path), - next: next.map(([path]) => path) - })), - shareReplay({ bufferSize: 1, refCount: true }) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Paint anchor list from source observable - * - * This operator function will keep track of the anchor list in-between emits - * in order to optimize rendering by only repainting anchor list migrations. - * After determining which anchors need to be repainted, the actual rendering - * is deferred to the next animation frame. - * - * @param els - Anchor elements - * - * @return Operator function - */ -export function paintAnchorList( - els: HTMLAnchorElement[] -): MonoTypeOperatorFunction { - return pipe( - - /* Extract anchor list migrations only */ - scan((a, b) => { - const begin = Math.max(0, Math.min(b.done.length, a.done.length) - 1) - const end = Math.max(b.done.length, a.done.length) - return { - done: b.done.slice(begin, end + 1), - next: difference(b.next, a.next) - } - }, { done: [], next: [] }), - - /* Defer repaint to next animation frame */ - observeOn(animationFrameScheduler), - tap(({ done, next }) => { - - /* Look forward */ - for (const [el] of next) { - resetAnchorActive(el) - resetAnchorBlur(el) - } - - /* Look backward */ - for (const [index, [el]] of done.entries()) { - setAnchorActive(el, index === done.length - 1) - setAnchorBlur(el, true) - } - }), - - /* Reset on complete or error */ - finalize(() => { - for (const el of els) { - resetAnchorActive(el) - resetAnchorBlur(el) - } - }) - ) -} diff --git a/src/assets/javascripts/component/anchor/element/index.ts b/src/assets/javascripts/component/anchor/element/index.ts deleted file mode 100644 index 7f6d4eb92..000000000 --- a/src/assets/javascripts/component/anchor/element/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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. - */ - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set anchor blur - * - * @param el - Anchor element - * @param value - Whether the anchor is blurred - */ -export function setAnchorBlur( - el: HTMLElement, value: boolean -): void { - el.setAttribute("data-md-state", value ? "blur" : "") -} - -/** - * Reset anchor blur - * - * @param el - Anchor element - */ -export function resetAnchorBlur( - el: HTMLElement -): void { - el.removeAttribute("data-md-state") -} - -/* ------------------------------------------------------------------------- */ - -/** - * Set anchor active - * - * @param el - Anchor element - * @param value - Whether the anchor is active - */ -export function setAnchorActive( - el: HTMLElement, value: boolean -): void { - el.classList.toggle("md-nav__link--active", value) -} - -/** - * Reset anchor active - * - * @param el - Anchor element - */ -export function resetAnchorActive( - el: HTMLElement -): void { - el.classList.remove("md-nav__link--active") -} diff --git a/src/assets/javascripts/component/header/element/index.ts b/src/assets/javascripts/config/index.ts similarity index 72% rename from src/assets/javascripts/component/header/element/index.ts rename to src/assets/javascripts/config/index.ts index 16921e03e..c399adf11 100644 --- a/src/assets/javascripts/component/header/element/index.ts +++ b/src/assets/javascripts/config/index.ts @@ -20,29 +20,29 @@ * IN THE SOFTWARE. */ +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Configuration + */ +export interface Config { + base: string /* Base URL */ +} + /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ /** - * Set header shadow + * Ensure that the given value is a valid configuration * - * @param el - Header element - * @param value - Whether the shadow is shown - */ -export function setHeaderShadow( - el: HTMLElement, value: boolean -): void { - el.setAttribute("data-md-state", value ? "shadow" : "") -} - -/** - * Reset header shadow + * @param config - Configuration * - * @param el - Header element + * @return Test result */ -export function resetHeaderShadow( - el: HTMLElement -): void { - el.removeAttribute("data-md-state") +export function isConfig(config: any): config is Config { + return typeof config === "object" + && typeof config.base === "string" } diff --git a/src/assets/javascripts/index.ts b/src/assets/javascripts/index.ts index 55d37898c..d97535ade 100644 --- a/src/assets/javascripts/index.ts +++ b/src/assets/javascripts/index.ts @@ -20,412 +20,128 @@ * IN THE SOFTWARE. */ -import { findLast } from "ramda" -import { - NEVER, - animationFrameScheduler, - fromEvent, - merge, - of -} from "rxjs" -import { AjaxRequest, ajax } from "rxjs/ajax" -import { - delay, - filter, - map, - mapTo, - observeOn, - scan, - shareReplay, - switchMap, - tap -} from "rxjs/operators" - -import "./polyfill" +import { shareReplay, switchMap } from "rxjs/operators" +import { isConfig } from "./config" import { - paintAnchorList, - paintComponentMap, - paintHeaderShadow, - paintSidebar, - pluckComponent, - setNavigationOverflowScrolling, - watchAnchorList, + setupSidebar, + switchComponent, + switchMapIfActive, watchComponentMap, watchHeader, - watchMain, - watchNavigationIndex, - watchSidebar -} from "./component" + watchMain +} from "./theme" import { - getElement, - getElements, + watchDocument, + watchDocumentSwitch, watchLocation, - watchLocationHash, + watchLocationFragment, watchMedia, watchViewportOffset, - watchViewportSize, - withElement + watchViewportSize } from "./ui" -import { toggle } from "./utilities" -// ---------------------------------------------------------------------------- -// Disclaimer: this file is currently heavy WIP -// ---------------------------------------------------------------------------- +// TBD -const offset$ = watchViewportOffset() -const size$ = watchViewportSize() +const names = [ + "header", /* Header */ + "title", /* Header title */ + "search", /* Search */ + "query", /* Search input */ + "reset", /* Search reset */ + "result", /* Search results */ + "container", /* Container */ + "main", /* Main area */ + "hero", /* Hero */ + "tabs", /* Tabs */ + "navigation", /* Navigation */ + "toc" /* Table of contents */ +] as const // TODO: put this somewhere else... (merge with config!) JSON schema!? -const aboveScreen$ = watchMedia("(min-width: 1220px)") -const belowScreen$ = watchMedia("(max-width: 1219px)") +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ -const aboveTablet$ = watchMedia("(min-width: 960px)") -const belowTablet$ = watchMedia("(max-width: 959px)") +/** + * Initialize Material for MkDocs + * + * @param config - Configuration + */ +export function initialize(config: unknown) { + if (!isConfig(config)) + throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`) -// ---------------------------------------------------------------------------- + /* ----------------------------------------------------------------------- */ -// modernizr for the poor -document.documentElement.classList.remove("no-js") -document.documentElement.classList.add("js") + /* Create viewport observables */ + const offset$ = watchViewportOffset() + const size$ = watchViewportSize() -// ---------------------------------------------------------------------------- + /* Create media observables */ + const screen$ = watchMedia("(min-width: 1220px)") + const tablet$ = watchMedia("(min-width: 960px)") -// Observable that resolves with document when loaded -const init$ = fromEvent(document, "DOMContentLoaded") - .pipe( - mapTo(document), - shareReplay({ bufferSize: 1, refCount: true }) - ) + /* Create location observables */ + const url$ = watchLocation() + const fragment$ = watchLocationFragment() -// Location subject -const location$ = watchLocation() + /* Create document observables */ + const load$ = watchDocument() + const switch$ = watchDocumentSwitch({ url$ }) -// Observable that resolves with document on XHR load -const reload$ = location$ - .pipe( - switchMap(url => load(url)) - ) + /* ----------------------------------------------------------------------- */ -// Extract and (re-)paint components -const components$ = merge(init$, reload$) - .pipe( - switchMap(watchComponentMap), - paintComponentMap(), - shareReplay({ bufferSize: 1, refCount: true }) - ) + /* Create component map observable */ + const components$ = watchComponentMap(names, { load$, switch$ }) -// ---------------------------------------------------------------------------- - -const header$ = components$ - .pipe( - pluckComponent("header"), - switchMap(watchHeader) - ) - -const main$ = components$ - .pipe( - pluckComponent("main"), - switchMap(el => watchMain(el, { size$, offset$, header$ })), - shareReplay({ bufferSize: 1, refCount: true}) - ) - -// ---------------------------------------------------------------------------- - -/* Component: sidebar with navigation */ -components$ - .pipe( - pluckComponent("navigation"), - switchMap(el => aboveScreen$ - .pipe( - toggle(() => watchSidebar(el, { offset$, main$ }) - .pipe( - paintSidebar(el) - ) - ) - )) - ) - .subscribe() - -/* Component: sidebar with table of contents (missing on 404 page) */ -components$ - .pipe( - pluckComponent("toc"), - switchMap(el => aboveTablet$ - .pipe( - toggle(() => watchSidebar(el, { offset$, main$ }) - .pipe( - paintSidebar(el) - ) - ) - )) - ) - .subscribe() - -/* Component: link blurring for table of contents */ -components$ - .pipe( - pluckComponent("toc"), - map(el => getElements(".md-nav__link", el)), - switchMap(els => aboveTablet$ - .pipe( - toggle(() => watchAnchorList(els, { size$, offset$, header$ }) - .pipe( - paintAnchorList(els) - ) - ) - ) - ) - ) - .subscribe() - -/* Component: header shadow toggle */ -components$ - .pipe( - pluckComponent("header"), - switchMap(el => main$.pipe( - paintHeaderShadow(el) - )) - ) - .subscribe() - -// ---------------------------------------------------------------------------- -// Refactor: -// ---------------------------------------------------------------------------- - -// Observable that catches all internal links without the necessity of rebinding -// as events are bubbled up through the DOM. -init$ - .pipe( - switchMap(({ body }) => fromEvent(body, "click")), - switchMap(ev => { - - /* Walk up as long as we're not in a details tag */ - let parent = ev.target as Node | undefined - while (parent && !(parent instanceof HTMLAnchorElement)) - parent = parent.parentNode // TODO: fix errors... - - if (parent) { // this one OR (!) one of - // its parents... - if (!/(:\/\/|^#[^\/]+$)/.test(parent.getAttribute("href")!)) { - ev.preventDefault() - console.log("> ", parent.href) - - // Extract URL; push to state, then emit new URL - const href = parent.href - history.pushState({}, "", href) // move this somewhere else!??? - return of(href) - } - } - return NEVER - }), - shareReplay({ bufferSize: 1, refCount: true }) - ) - .subscribe(location$) - -// ---------------------------------------------------------------------------- - -const nav2 = getElement("[data-md-component=navigation]")! -const index$ = watchNavigationIndex(nav2) // TODO: maybe rename into setup!? merge with sidebar? -belowScreen$ - .pipe( - toggle(() => index$ - .pipe( - switchMap(index => merge(...[...index.keys()] - .map(input => fromEvent(input, "change")) - ) - .pipe( - mapTo(index) - ) - ), - map(index => getElement("ul", index.get( - findLast(input => input.checked, [...index.keys()])!) - )!), // find the TOP MOST! <-- this is the actively displayed on mobile - - // this is the paint job... - - // dispatch action - TODO: document why this crap is even necessary - scan((prev, next) => { - if (prev) - setNavigationOverflowScrolling(prev, false) // TODO: resetOverflowScrolling .... - return next - }), - delay(250), - tap(next => { - setNavigationOverflowScrolling(next, true) // setNavigationScrollfix - }) - ) - ) - ) - .subscribe() - -// ---------------------------------------------------------------------------- - -function isNavigationCollapsible(el: HTMLElement): boolean { - return el.getAttribute("data-md-component") === "collapsible" // TODO: maybe better remove again -} - -aboveScreen$ - .pipe( - toggle(() => index$ - .pipe( - // map(index => ) - // filter shit from index... - switchMap(index => [...index.keys()] - .filter(input => isNavigationCollapsible(index.get(input)!)) - .map(input => { - const el = index.get(input)! - // this doesnt work... - el.setAttribute("data-md-height", `${el.offsetHeight}`) // TODO: this is a hack - return input - }) - .map(input => fromEvent(input, "change") - .pipe( - map(() => { - const el = index.get(input)! - let height = parseInt(el.getAttribute("data-md-height")!, 10) - // always goes from data-md-height... wrong... - if (!input.checked) { - el.style.maxHeight = `${height}px` - - /* Set target height */ - height = 0 - - } else { - el.style.maxHeight = "initial" // 100%!? - el.style.transitionDuration = "initial" - - /* Retrieve target height */ - height = el.offsetHeight - console.log("expand to height") - - /* Reset state and set start height */ - // el.removeAttribute("data-md-state") - el.style.maxHeight = "0px" - } - - /* Force style recalculation */ - el.offsetHeight // tslint:disable-line - el.style.transitionDuration = "" - return height - }), // max height is set... just read it. - observeOn(animationFrameScheduler), - tap(height => { - const el = index.get(input)! - // el.setAttribute("data-md-state", "animate") - el.style.maxHeight = `${height}px` - console.log("setting shit...") - - el.setAttribute("data-md-height", `${height}`) - }), - delay(250), - tap(() => { - const el = index.get(input)! - console.log("DONE") - // el.removeAttribute("data-md-state") - el.style.maxHeight = "" - }) - ) - .subscribe() // merge shit and return it... - ) - ) - ) - ) - ) - .subscribe() - -// ---------------------------------------------------------------------------- - -/* Open details after anchor jump */ -const hash$ = watchLocationHash() -hash$ - .pipe( - withElement(), // TODO: somehow ugly... not so nice and cool - tap(el => { - let parent = el.parentNode - while (parent && !(parent instanceof HTMLDetailsElement)) // TODO: put this into a FUNCTION! - parent = parent.parentNode - - /* If there's a details tag, open it */ - if (parent && !parent.open) { - parent.open = true - - /* Hack: force reload for repositioning */ // TODO. what happens here!? - const hash = location.hash - location.hash = " " - location.hash = hash // tslint:disable-line - // TODO: setLocationHash() + forceLocationHashChange - } - }) - ) - .subscribe() - -// ---------------------------------------------------------------------------- - -// setupAnchorToggle? -const drawerToggle = getElement("[data-md-toggle=drawer]")! -const searchToggle = getElement("[data-md-toggle=search]")! - -/* Listener: close drawer when anchor links are clicked */ -hash$ - .pipe( - tap(() => setToggle(drawerToggle, false)) - ) - .subscribe() - -/* Listener: open search on focus */ -const query = getElement("[data-md-component=query]")! -if (query) { - fromEvent(query, "focus") + /* Create header observable */ + const header$ = components$ .pipe( - tap(() => setToggle(searchToggle, true)) + switchComponent("header"), + switchMap(watchHeader) + ) + + /* Create main area observable */ + const main$ = components$ + .pipe( + switchComponent("main"), + switchMap(el => watchMain(el, { size$, offset$, header$ })), + shareReplay(1) + ) + + /* ----------------------------------------------------------------------- */ + + /* Create sidebar with navigation */ + screen$ + .pipe( + switchMapIfActive(() => components$ // TODO: write an observable creation function... + .pipe( + switchComponent("navigation"), + switchMap(el => setupSidebar(el, { offset$, main$ })) + ) + ) ) .subscribe() -} -/* Listener: focus input after opening search */ -fromEvent(searchToggle, "change") - .pipe( - filter(() => searchToggle.checked), - delay(400), - tap(() => query.focus()) - ) - .subscribe() + /* Create sidebar with table of contents (missing on 404 page) */ + tablet$ + .pipe( + switchMapIfActive(() => components$ + .pipe( + switchComponent("toc"), + switchMap(el => setupSidebar(el, { offset$, main$ })) + ) + ) + ) + .subscribe(console.log) -// data-md-toggle! -function setToggle(toggle: HTMLInputElement, active: boolean): void { - if (toggle.checked !== active) { - toggle.checked = active - toggle.dispatchEvent(new CustomEvent("change")) + /* Return all observables */ + return { + ui: { + document: { load$, switch$ }, + location: { url$, fragment$ }, + media: { screen$, tablet$ }, + viewport: { offset$, size$ } + } } } - -// ---------------------------------------------------------------------------- - -// Asynchronously load a document -function load(url: string) { - - const options: AjaxRequest = { - responseType: "document", - withCredentials: true - } // TODO: remove favicon from source!? patch... - - return ajax({ url, ...options }) - .pipe( - map(({ response }) => { - if (!(response instanceof Document)) // TODO: what to do in case of error? - throw Error("Unknown error...") - - return response - }) - ) -} - -// ---------------------------------------------------------------------------- - -// function isLocal(el: HTMLAnchorElement): boolean { -// return /(:\/\/|^#[^\/]+$)/.test(el.getAttribute("href")!) -// } - -export function app(config: any) { - console.log("called app with", config) -} diff --git a/src/assets/javascripts/polyfill/details/index.ts b/src/assets/javascripts/polyfill/details/index.ts deleted file mode 100644 index dd9771ece..000000000 --- a/src/assets/javascripts/polyfill/details/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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. - */ - -/* ---------------------------------------------------------------------------- - * Helper functions - * ------------------------------------------------------------------------- */ - -/** - * Check if browser supports the `
` tag - * - * As this polyfill is executed at the end of ``, not all checks from the - * original source were necessary, so the script was stripped down a little. - * - * @see https://bit.ly/2O1teyP - Original source - * - * @return Test result - */ -function isSupported(): boolean { - const details = document.createElement("details") - if (!("open" in details)) - return false - - /* Insert summary and append element */ - details.innerHTML = "__" - details.style.display = "block" - document.body.appendChild(details) - - /* Measure height difference */ - const h0 = details.offsetHeight - details.open = true - const h1 = details.offsetHeight - - /* Remove element and return test result */ - document.body.removeChild(details) - return h1 - h0 !== 0 -} - -/* ---------------------------------------------------------------------------- - * Polyfill - * ------------------------------------------------------------------------- */ - -/* Execute polyfill when DOM is available */ -document.addEventListener("DOMContentLoaded", () => { - if (isSupported()) - return - - /* Indicate presence of details polyfill */ - document.documentElement.classList.add("no-details") - - /* Retrieve all summaries and polyfill open/close functionality */ - const summaries = document.querySelectorAll("details > summary") - summaries.forEach(summary => { - summary.addEventListener("click", () => { - const details = summary.parentNode as HTMLElement - if (details.hasAttribute("open")) { - details.removeAttribute("open") - } else { - details.setAttribute("open", "") - } - }) - }) -}) diff --git a/src/assets/javascripts/polyfill/index.ts b/src/assets/javascripts/polyfill/index.ts deleted file mode 100644 index 3879dcefa..000000000 --- a/src/assets/javascripts/polyfill/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 "./custom-event" -import "./details" diff --git a/src/assets/javascripts/component/_/index.ts b/src/assets/javascripts/theme/component/_/index.ts similarity index 51% rename from src/assets/javascripts/component/_/index.ts rename to src/assets/javascripts/theme/component/_/index.ts index 580bbffee..5d5f4174f 100644 --- a/src/assets/javascripts/component/_/index.ts +++ b/src/assets/javascripts/theme/component/_/index.ts @@ -21,17 +21,10 @@ */ import { keys } from "ramda" -import { - MonoTypeOperatorFunction, - NEVER, - Observable, - OperatorFunction, - of, - pipe -} from "rxjs" -import { scan, shareReplay, switchMap } from "rxjs/operators" +import { NEVER, Observable, OperatorFunction, merge, of, pipe } from "rxjs" +import { map, scan, shareReplay, switchMap } from "rxjs/operators" -import { getElement } from "../../ui" +import { getElement } from "../../utilities" /* ---------------------------------------------------------------------------- * Types @@ -61,92 +54,82 @@ export type ComponentMap = { [P in Component]?: HTMLElement } +/* ---------------------------------------------------------------------------- + * Function types + * ------------------------------------------------------------------------- */ + +/** + * Options + */ +interface Options { + load$: Observable /* Document observable */ + switch$: Observable /* Document switch observable */ +} + /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ /** - * Retrieve the component to element mapping + * Watch component map * - * The document must be passed as a parameter to support retrieving elements - * from the document object returned through asynchronous loading. + * This function returns an observable that will maintain bindings to the given + * components in-between document switches and update the document in-place. * - * @param document - Document of reference + * @param names - Components + * @param options - Options * * @return Component map observable */ export function watchComponentMap( - document: Document + names: Component[], { load$, switch$ }: Options ): Observable { + const components$ = merge(load$, switch$) + .pipe( - /* Build component map */ - const map$ = of([ - "header", /* Header */ - "title", /* Header title */ - "search", /* Search */ - "query", /* Search input */ - "reset", /* Search reset */ - "result", /* Search results */ - "container", /* Container */ - "main", /* Main area */ - "hero", /* Hero */ - "tabs", /* Tabs */ - "navigation", /* Navigation */ - "toc" /* Table of contents */ - ].reduce((map, name) => { - const el = getElement(`[data-md-component=${name}]`, document) - return { - ...map, - ...typeof el !== "undefined" ? { [name]: el } : {} - } - }, {})) + /* Build component map */ + map(document => names.reduce((components, name) => { + const el = getElement(`[data-md-component=${name}]`, document) + return { + ...components, + ...typeof el !== "undefined" ? { [name]: el } : {} + } + }, {})), + + /* Re-compute component map on document switch */ + scan((prev, next) => { + for (const name of keys(prev)) { + switch (name) { + + /* Top-level components: update */ + case "title": + case "container": + if (name in prev && typeof prev[name] !== "undefined") { + prev[name]!.replaceWith(next[name]!) + prev[name] = next[name] + } + break + + /* All other components: rebind */ + default: + prev[name] = getElement(`[data-md-component=${name}]`) + } + } + return prev + }) + ) /* Return component map as hot observable */ - return map$ + return components$ .pipe( - shareReplay({ bufferSize: 1, refCount: true }) + shareReplay(1) ) } /* ------------------------------------------------------------------------- */ /** - * 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 and rebind all - * remaining components, as they may be children of swapped components. - * - * @param names - Components to paint - * - * @return Operator function - */ -export function paintComponentMap( - names: Component[] = ["title", "container"] -): MonoTypeOperatorFunction { - return pipe( - scan((prev, next) => { - for (const name of keys(prev)) { - - /* Swap component */ - if (names.includes(name)) { - if (name in prev && typeof prev[name] !== "undefined") { - prev[name]!.replaceWith(next[name]!) - prev[name] = next[name] - } - - /* Bind component */ - } else { - prev[name] = getElement(`[data-md-component=${name}]`) - } - } - return prev - }) - ) -} - -/** - * Pluck a component from the component map + * Switch to component * * @template T - Element type * @@ -154,13 +137,13 @@ export function paintComponentMap( * * @return Operator function */ -export function pluckComponent( +export function switchComponent( name: Component -): OperatorFunction { +): OperatorFunction { return pipe( - switchMap(map => { - return typeof map[name] !== "undefined" - ? of(map[name]!) + switchMap(components => { + return typeof components[name] !== "undefined" + ? of(components[name] as T) : NEVER }) ) diff --git a/src/assets/javascripts/component/header/_/index.ts b/src/assets/javascripts/theme/component/header/_/index.ts similarity index 65% rename from src/assets/javascripts/component/header/_/index.ts rename to src/assets/javascripts/theme/component/header/_/index.ts index bf0bbc243..21ec46220 100644 --- a/src/assets/javascripts/component/header/_/index.ts +++ b/src/assets/javascripts/theme/component/header/_/index.ts @@ -20,19 +20,7 @@ * IN THE SOFTWARE. */ -import { MonoTypeOperatorFunction, Observable, of, pipe } from "rxjs" -import { - distinctUntilKeyChanged, - finalize, - shareReplay, - tap -} from "rxjs/operators" - -import { Main } from "../../main" -import { - resetHeaderShadow, - setHeaderShadow -} from "../element" +import { Observable, defer, of } from "rxjs" /* ---------------------------------------------------------------------------- * Types @@ -51,7 +39,7 @@ export interface Header { * ------------------------------------------------------------------------- */ /** - * Create an observable to watch the header + * Watch header * * The header is wrapped in an observable to pave the way for auto-hiding or * other dynamic behaviors that may be implemented later on. @@ -63,38 +51,14 @@ export interface Header { export function watchHeader( el: HTMLElement ): Observable
{ - const sticky = getComputedStyle(el) - .getPropertyValue("position") === "fixed" + return defer(() => { + const sticky = getComputedStyle(el) + .getPropertyValue("position") === "fixed" - /* Return header as hot observable */ - return of({ - sticky, - height: sticky ? el.offsetHeight : 0 - }) - .pipe( - shareReplay({ bufferSize: 1, refCount: true }) - ) -} - -/* ------------------------------------------------------------------------- */ - -/** - * Paint header shadow from source observable - * - * @param el - Header element - * - * @return Operator function - */ -export function paintHeaderShadow( - el: HTMLElement -): MonoTypeOperatorFunction
{ - return pipe( - distinctUntilKeyChanged("active"), - tap(({ active }) => { - setHeaderShadow(el, active) - }), - finalize(() => { - resetHeaderShadow(el) + /* Return header as hot observable */ + return of({ + sticky, + height: sticky ? el.offsetHeight : 0 }) - ) + }) } diff --git a/src/assets/javascripts/component/header/index.ts b/src/assets/javascripts/theme/component/header/index.ts similarity index 97% rename from src/assets/javascripts/component/header/index.ts rename to src/assets/javascripts/theme/component/header/index.ts index 634b1a4ff..d41a92f82 100644 --- a/src/assets/javascripts/component/header/index.ts +++ b/src/assets/javascripts/theme/component/header/index.ts @@ -21,4 +21,3 @@ */ export * from "./_" -export * from "./element" diff --git a/src/assets/javascripts/component/index.ts b/src/assets/javascripts/theme/component/index.ts similarity index 95% rename from src/assets/javascripts/component/index.ts rename to src/assets/javascripts/theme/component/index.ts index 8ab75bae6..932cf7a40 100644 --- a/src/assets/javascripts/component/index.ts +++ b/src/assets/javascripts/theme/component/index.ts @@ -21,8 +21,6 @@ */ export * from "./_" -export * from "./anchor" export * from "./header" export * from "./main" -export * from "./navigation" export * from "./sidebar" diff --git a/src/assets/javascripts/component/main/index.ts b/src/assets/javascripts/theme/component/main/index.ts similarity index 83% rename from src/assets/javascripts/component/main/index.ts rename to src/assets/javascripts/theme/component/main/index.ts index 6d3407427..961e91883 100644 --- a/src/assets/javascripts/component/main/index.ts +++ b/src/assets/javascripts/theme/component/main/index.ts @@ -28,7 +28,7 @@ import { shareReplay } from "rxjs/operators" -import { ViewportOffset, ViewportSize } from "../../ui" +import { ViewportOffset, ViewportSize } from "../../../ui" import { Header } from "../header" /* ---------------------------------------------------------------------------- @@ -49,9 +49,9 @@ export interface Main { * ------------------------------------------------------------------------- */ /** - * Watch options + * Options */ -interface WatchOptions { +interface Options { size$: Observable /* Viewport size observable */ offset$: Observable /* Viewport offset observable */ header$: Observable
/* Header observable */ @@ -62,7 +62,12 @@ interface WatchOptions { * ------------------------------------------------------------------------- */ /** - * Create an observable to watch the main area + * Watch main area + * + * This function returns an observable that computes the visual parameters of + * the main area from the viewport height and vertical offset, as well as the + * height of the header element. The height of the main area is corrected by + * the height of the header (if fixed) and footer element. * * @param el - Main area element * @param options - Options @@ -70,7 +75,7 @@ interface WatchOptions { * @return Main area observable */ export function watchMain( - el: HTMLElement, { size$, offset$, header$ }: WatchOptions + el: HTMLElement, { size$, offset$, header$ }: Options ): Observable
{ /* Compute necessary adjustment for header */ @@ -80,7 +85,7 @@ export function watchMain( ) /* Compute the main area's visible height */ - const height$ = combineLatest(offset$, size$, adjust$) + const height$ = combineLatest([offset$, size$, adjust$]) .pipe( map(([{ y }, { height }, adjust]) => { const top = el.offsetTop @@ -93,20 +98,20 @@ export function watchMain( ) /* Compute whether the viewport offset is past the main area's top */ - const active$ = combineLatest(offset$, adjust$) + const active$ = combineLatest([offset$, adjust$]) .pipe( map(([{ y }, adjust]) => y >= el.offsetTop - adjust), distinctUntilChanged() ) /* Combine into a single hot observable */ - return combineLatest(height$, adjust$, active$) + return combineLatest([height$, adjust$, active$]) .pipe( map(([height, adjust, active]) => ({ offset: el.offsetTop - adjust, height, active })), - shareReplay({ bufferSize: 1, refCount: true }) + shareReplay(1) ) } diff --git a/src/assets/javascripts/component/sidebar/_/index.ts b/src/assets/javascripts/theme/component/sidebar/_/index.ts similarity index 71% rename from src/assets/javascripts/component/sidebar/_/index.ts rename to src/assets/javascripts/theme/component/sidebar/_/index.ts index f02e712d1..02f18adf1 100644 --- a/src/assets/javascripts/component/sidebar/_/index.ts +++ b/src/assets/javascripts/theme/component/sidebar/_/index.ts @@ -21,13 +21,7 @@ */ import { equals } from "ramda" -import { - MonoTypeOperatorFunction, - Observable, - animationFrameScheduler, - combineLatest, - pipe -} from "rxjs" +import { Observable, animationFrameScheduler, combineLatest } from "rxjs" import { distinctUntilChanged, finalize, @@ -37,7 +31,7 @@ import { tap } from "rxjs/operators" -import { ViewportOffset } from "../../../ui" +import { ViewportOffset } from "../../../../ui" import { Main } from "../../main" import { resetSidebarHeight, @@ -63,9 +57,9 @@ export interface Sidebar { * ------------------------------------------------------------------------- */ /** - * Watch options + * Options */ -interface WatchOptions { +interface Options { offset$: Observable /* Viewport offset observable */ main$: Observable
/* Main area observable */ } @@ -75,7 +69,12 @@ interface WatchOptions { * ------------------------------------------------------------------------- */ /** - * Create an observable to watch a sidebar + * Watch sidebar + * + * This function returns an observable that computes the visual parameters of + * the given element (a sidebar) from the vertical viewport offset, as well as + * the height of the main area. When the page is scrolled beyond the header, + * the sidebar is locked and fills the remaining space. * * @param el - Sidebar element * @param options - Options @@ -83,7 +82,7 @@ interface WatchOptions { * @return Sidebar observable */ export function watchSidebar( - el: HTMLElement, { offset$, main$ }: WatchOptions + el: HTMLElement, { offset$, main$ }: Options ): Observable { /* Adjust for internal main area offset */ @@ -93,7 +92,7 @@ export function watchSidebar( ) /* Compute the sidebar's available height */ - const height$ = combineLatest(offset$, main$) + const height$ = combineLatest([offset$, main$]) .pipe( map(([{ y }, { offset, height }]) => { return height - adjust + Math.min(adjust, Math.max(0, y - offset)) @@ -101,45 +100,45 @@ export function watchSidebar( ) /* Compute whether the sidebar should be locked */ - const lock$ = combineLatest(offset$, main$) + const lock$ = combineLatest([offset$, main$]) .pipe( map(([{ y }, { offset }]) => y >= offset + adjust) ) /* Combine into single hot observable */ - return combineLatest(height$, lock$) + return combineLatest([height$, lock$]) .pipe( map(([height, lock]) => ({ height, lock })), distinctUntilChanged(equals), - shareReplay({ bufferSize: 1, refCount: true }) + shareReplay(1) ) } -/* ------------------------------------------------------------------------- */ - /** - * Paint sidebar from source observable + * Setup sidebar * * @param el - Sidebar element + * @param options - Options * - * @return Operator function + * @return Sidebar observable */ -export function paintSidebar( - el: HTMLElement -): MonoTypeOperatorFunction { - return pipe( +export function setupSidebar( + el: HTMLElement, options: Options +): Observable { + return watchSidebar(el, options) + .pipe( + observeOn(animationFrameScheduler), - /* Defer repaint to next animation frame */ - observeOn(animationFrameScheduler), - tap(({ height, lock }) => { - setSidebarHeight(el, height) - setSidebarLock(el, lock) - }), + /* Apply mutations (side effects) */ + tap(({ height, lock }) => { + setSidebarHeight(el, height) + setSidebarLock(el, lock) + }), - /* Reset on complete or error */ - finalize(() => { - resetSidebarHeight(el) - resetSidebarLock(el) - }) - ) + /* Reset on complete or error */ + finalize(() => { + resetSidebarHeight(el) + resetSidebarLock(el) + }) + ) } diff --git a/src/assets/javascripts/component/sidebar/element/index.ts b/src/assets/javascripts/theme/component/sidebar/element/index.ts similarity index 100% rename from src/assets/javascripts/component/sidebar/element/index.ts rename to src/assets/javascripts/theme/component/sidebar/element/index.ts diff --git a/src/assets/javascripts/component/anchor/index.ts b/src/assets/javascripts/theme/component/sidebar/index.ts similarity index 100% rename from src/assets/javascripts/component/anchor/index.ts rename to src/assets/javascripts/theme/component/sidebar/index.ts diff --git a/src/assets/javascripts/polyfill/custom-event/index.ts b/src/assets/javascripts/theme/index.ts similarity index 93% rename from src/assets/javascripts/polyfill/custom-event/index.ts rename to src/assets/javascripts/theme/index.ts index d9195c9a5..b731351c4 100644 --- a/src/assets/javascripts/polyfill/custom-event/index.ts +++ b/src/assets/javascripts/theme/index.ts @@ -20,4 +20,6 @@ * IN THE SOFTWARE. */ -import "custom-event-polyfill" +export * from "./component" +export * from "./utilities" +// export * from "./worker" diff --git a/src/assets/javascripts/ui/element/index.ts b/src/assets/javascripts/theme/utilities/element/index.ts similarity index 64% rename from src/assets/javascripts/ui/element/index.ts rename to src/assets/javascripts/theme/utilities/element/index.ts index e21cb60c1..2b549dfb1 100644 --- a/src/assets/javascripts/ui/element/index.ts +++ b/src/assets/javascripts/theme/utilities/element/index.ts @@ -20,11 +20,6 @@ * IN THE SOFTWARE. */ -import { OperatorFunction, pipe } from "rxjs" -import { filter, map } from "rxjs/operators" - -import { toArray } from "../../utilities" - /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ @@ -58,44 +53,5 @@ export function getElement( export function getElements( selector: string, node: ParentNode = document ): T[] { - return toArray(node.querySelectorAll(selector)) -} - -/* ---------------------------------------------------------------------------- - * Operators - * ------------------------------------------------------------------------- */ - -/** - * Retrieve an element matching the query selector - * - * @template T - Element type - * - * @param node - Node of reference - * - * @return Operator function - */ -export function withElement( - node: ParentNode = document -): OperatorFunction { - return pipe( - map(selector => getElement(selector, node)!), - filter(Boolean) - ) -} - -/** - * Retrieve all elements matching the query selector - * - * @template T - Element type - * - * @param node - Node of reference - * - * @return Operator function - */ -export function withElements( - node: ParentNode = document -): OperatorFunction { - return pipe( - map(selector => getElements(selector, node)) - ) + return Array.from(node.querySelectorAll(selector)) } diff --git a/src/assets/javascripts/component/sidebar/index.ts b/src/assets/javascripts/theme/utilities/index.ts similarity index 97% rename from src/assets/javascripts/component/sidebar/index.ts rename to src/assets/javascripts/theme/utilities/index.ts index 634b1a4ff..4e50c4bec 100644 --- a/src/assets/javascripts/component/sidebar/index.ts +++ b/src/assets/javascripts/theme/utilities/index.ts @@ -20,5 +20,5 @@ * IN THE SOFTWARE. */ -export * from "./_" export * from "./element" +export * from "./operator" diff --git a/src/assets/javascripts/utilities/other/index.ts b/src/assets/javascripts/theme/utilities/operator/index.ts similarity index 67% rename from src/assets/javascripts/utilities/other/index.ts rename to src/assets/javascripts/theme/utilities/operator/index.ts index 1bc6f9039..2b1583c9f 100644 --- a/src/assets/javascripts/utilities/other/index.ts +++ b/src/assets/javascripts/theme/utilities/operator/index.ts @@ -20,7 +20,7 @@ * IN THE SOFTWARE. */ -import { NEVER, Observable, OperatorFunction, pipe } from "rxjs" +import { EMPTY, Observable, OperatorFunction, pipe } from "rxjs" import { switchMap } from "rxjs/operators" /* ---------------------------------------------------------------------------- @@ -28,37 +28,35 @@ import { switchMap } from "rxjs/operators" * ------------------------------------------------------------------------- */ /** - * Convert a collection to an array - * - * @template T - Element type - * - * @param collection - Collection or node list - * - * @return Array of elements - */ -export function toArray< - T extends HTMLElement ->(collection: HTMLCollection | NodeListOf): T[] { - return Array.from(collection) as T[] -} - -/* ---------------------------------------------------------------------------- - * Operators - * ------------------------------------------------------------------------- */ - -/** - * Switch to another observable, if toggle is active + * Switch to another observable if source observable emits `true` * * @template T - Observable value type * * @param project - Project function * - * @return Observable, if toggle is active + * @return Operator function */ -export function toggle( - project: () => Observable +export function switchMapIfActive( + project: (value: boolean) => Observable ): OperatorFunction { return pipe( - switchMap(active => active ? project() : NEVER) + switchMap(value => value ? project(value) : EMPTY) + ) +} + +/** + * Switch to another observable if source observable emits `false` + * + * @template T - Observable value type + * + * @param project - Project function + * + * @return Operator function + */ +export function switchMapIfNotActive( + project: (value: boolean) => Observable +): OperatorFunction { + return pipe( + switchMap(value => value ? EMPTY : project(value)) ) } diff --git a/src/assets/javascripts/component/navigation/index.ts b/src/assets/javascripts/ui/document/index.ts similarity index 52% rename from src/assets/javascripts/component/navigation/index.ts rename to src/assets/javascripts/ui/document/index.ts index 71684ed47..99c9f2b4a 100644 --- a/src/assets/javascripts/component/navigation/index.ts +++ b/src/assets/javascripts/ui/document/index.ts @@ -20,64 +20,86 @@ * IN THE SOFTWARE. */ -import { Observable, of } from "rxjs" -import { shareReplay } from "rxjs/operators" - -import { getElement, getElements } from "../../ui" +import { Observable, fromEvent } from "rxjs" +import { ajax } from "rxjs/ajax" +import { + distinctUntilChanged, + map, + mapTo, + pluck, + shareReplay, + skip, + startWith, + switchMap +} from "rxjs/operators" /* ---------------------------------------------------------------------------- - * Types + * Function types * ------------------------------------------------------------------------- */ /** - * Navigation index + * Switch options */ -export type NavigationIndex = Map +interface SwitchOptions { + url$: Observable /* Location observable */ +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Observable for document load events + */ +const load$ = fromEvent(document, "DOMContentLoaded") /* ---------------------------------------------------------------------------- * Functions * ------------------------------------------------------------------------- */ /** - * Set navigation overflow scrolling + * Watch document * - * @param el - Navigation element - * @param active - Whether overflow scrolling is active + * @return Document observable */ -export function setNavigationOverflowScrolling( - el: HTMLElement, active: boolean -): void { - el.style.background = active ? "yellow" : "" // TODO: hack, temporary - el.style.webkitOverflowScrolling = active ? "touch" : "" -} - -/* ------------------------------------------------------------------------- */ - -/** - * Create an observable to index all navigation elements - * - * @param el - Top-level navigation element - * - * @return Navigation index observable - */ -export function watchNavigationIndex( - el: HTMLElement -): Observable { - const list = getElements("nav", el) - - /* Build index to map inputs to navigation lists */ - const index = new Map() - for (const item of list) { - const label = getElement("label", item)! - if (typeof label !== "undefined") { - const input = getElement(`#${label.htmlFor}`)! - index.set(input, item) - } - } - - /* Return navigation index as hot observable */ - return of(index) +export function watchDocument(): Observable { + return load$ .pipe( - shareReplay({ bufferSize: 1, refCount: true }) + 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( + { url$ }: SwitchOptions +): Observable { + return url$ + .pipe( + startWith(location.href), + map(url => url.replace(/#[^#]+$/, "")), + distinctUntilChanged(), + skip(1), + switchMap(url => ajax({ + url, + responseType: "document", + withCredentials: true + }) + .pipe( + pluck("response") + ) + ), + shareReplay(1) ) } diff --git a/src/assets/javascripts/ui/index.ts b/src/assets/javascripts/ui/index.ts index 2ff1e1c10..7ad3ab452 100644 --- a/src/assets/javascripts/ui/index.ts +++ b/src/assets/javascripts/ui/index.ts @@ -20,7 +20,7 @@ * IN THE SOFTWARE. */ -export * from "./element" +export * from "./document" export * from "./location" export * from "./media" export * from "./viewport" diff --git a/src/assets/javascripts/ui/location/index.ts b/src/assets/javascripts/ui/location/index.ts index de3536b6c..834b4344d 100644 --- a/src/assets/javascripts/ui/location/index.ts +++ b/src/assets/javascripts/ui/location/index.ts @@ -21,7 +21,7 @@ */ import { Observable, Subject, fromEvent } from "rxjs" -import { filter, map, share, startWith } from "rxjs/operators" +import { filter, map, share } from "rxjs/operators" /* ---------------------------------------------------------------------------- * Data @@ -42,7 +42,7 @@ const popstate$ = fromEvent(window, "popstate") * ------------------------------------------------------------------------- */ /** - * Create a subject to watch or alter the location + * Watch location * * @return Location subject */ @@ -50,7 +50,8 @@ export function watchLocation(): Subject { const location$ = new Subject() popstate$ .pipe( - map(() => location.href) + map(() => location.href), + share() ) .subscribe(location$) @@ -59,15 +60,14 @@ export function watchLocation(): Subject { } /** - * Create an observable to watch the location hash + * Watch location fragment * - * @return Location hash observable + * @return Location fragment observable */ -export function watchLocationHash(): Observable { +export function watchLocationFragment(): Observable { return hashchange$ .pipe( map(() => location.hash), - startWith(location.hash), filter(hash => hash.length > 0), share() ) diff --git a/src/assets/javascripts/ui/media/index.ts b/src/assets/javascripts/ui/media/index.ts index e01e523ac..d1facf86c 100644 --- a/src/assets/javascripts/ui/media/index.ts +++ b/src/assets/javascripts/ui/media/index.ts @@ -28,19 +28,19 @@ import { shareReplay, startWith } from "rxjs/operators" * ------------------------------------------------------------------------- */ /** - * Create an observable to watch a media query + * Watch media query * * @param query - Media query * * @return Media observable */ export function watchMedia(query: string): Observable { - const media = window.matchMedia(query) + const media = matchMedia(query) return fromEventPattern(next => media.addListener(() => next(media.matches)) ) .pipe( startWith(media.matches), - shareReplay({ bufferSize: 1, refCount: true }) + shareReplay(1) ) } diff --git a/src/assets/javascripts/ui/viewport/index.ts b/src/assets/javascripts/ui/viewport/index.ts index f293622af..a24e2a6f2 100644 --- a/src/assets/javascripts/ui/viewport/index.ts +++ b/src/assets/javascripts/ui/viewport/index.ts @@ -62,33 +62,33 @@ export interface ViewportSize { * ------------------------------------------------------------------------- */ /** - * Retrieve the viewport offset + * Retrieve viewport offset * * @return Viewport offset */ export function getViewportOffset(): ViewportOffset { return { - x: window.pageXOffset, - y: window.pageYOffset + x: pageXOffset, + y: pageYOffset } } /** - * Retrieve the viewport size + * Retrieve viewport size * * @return Viewport size */ export function getViewportSize(): ViewportSize { return { - width: window.innerWidth, - height: window.innerHeight + width: innerWidth, + height: innerHeight } } /* ------------------------------------------------------------------------- */ /** - * Create an observable to watch the viewport offset + * Watch viewport offset * * @return Viewport offset observable */ @@ -97,12 +97,12 @@ export function watchViewportOffset(): Observable { .pipe( map(getViewportOffset), startWith(getViewportOffset()), - shareReplay({ bufferSize: 1, refCount: true }) + shareReplay(1) ) } /** - * Create an observable to watch the viewport size + * Watch viewport size * * @return Viewport size observable */ @@ -111,6 +111,6 @@ export function watchViewportSize(): Observable { .pipe( map(getViewportSize), startWith(getViewportSize()), - shareReplay({ bufferSize: 1, refCount: true }) + shareReplay(1) ) } diff --git a/src/assets/javascripts/utilities/index.ts b/src/assets/javascripts/utilities/index.ts deleted file mode 100644 index c587c7fa9..000000000 --- a/src/assets/javascripts/utilities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./other"