diff --git a/src/assets/javascripts/component/anchor/_/index.ts b/src/assets/javascripts/component/anchor/_/index.ts index 79f53a643..cd2f2ddc1 100644 --- a/src/assets/javascripts/component/anchor/_/index.ts +++ b/src/assets/javascripts/component/anchor/_/index.ts @@ -30,6 +30,7 @@ import { } from "rxjs" import { distinctUntilChanged, + finalize, map, observeOn, scan, @@ -41,7 +42,8 @@ import { import { ViewportOffset, ViewportSize, getElement } from "../../../ui" import { Header } from "../../header" import { - resetAnchor, + resetAnchorActive, + resetAnchorBlur, setAnchorActive, setAnchorBlur } from "../element" @@ -81,7 +83,7 @@ interface WatchOptions { * 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 re-painted. + * 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 @@ -112,7 +114,7 @@ export function watchAnchorList( /* Compute partition of done and next anchors */ const partition$ = size$.pipe( - /* Build table to map anchor paths to vertical offsets */ + /* Build index to map anchor paths to vertical offsets */ map(() => { let path: HTMLAnchorElement[] = [] return [...table].reduce((index, [anchor, target]) => { @@ -184,13 +186,17 @@ export function watchAnchorList( * 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 re-painting anchor list migrations. - * After determining which anchors need to be re-painted, the actual rendering + * 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(): MonoTypeOperatorFunction { +export function paintAnchorList( + els: HTMLAnchorElement[] +): MonoTypeOperatorFunction { return pipe( /* Extract anchor list migrations only */ @@ -203,18 +209,28 @@ export function paintAnchorList(): MonoTypeOperatorFunction { } }, { done: [], next: [] }), - /* Defer re-paint to next animation frame */ + /* Defer repaint to next animation frame */ observeOn(animationFrameScheduler), tap(({ done, next }) => { /* Look forward */ - for (const [el] of next) - resetAnchor(el) + for (const [el] of next) { + resetAnchorActive(el) + resetAnchorBlur(el) + } /* Look backward */ for (const [index, [el]] of done.entries()) { - setAnchorBlur(el, true) 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 index 4a362f9bb..7f6d4eb92 100644 --- a/src/assets/javascripts/component/anchor/element/index.ts +++ b/src/assets/javascripts/component/anchor/element/index.ts @@ -36,6 +36,19 @@ export function setAnchorBlur( 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 * @@ -48,16 +61,13 @@ export function setAnchorActive( el.classList.toggle("md-nav__link--active", value) } -/* ------------------------------------------------------------------------- */ - /** - * Reset anchor + * Reset anchor active * * @param el - Anchor element */ -export function resetAnchor( +export function resetAnchorActive( el: HTMLElement ): void { - el.removeAttribute("data-md-state") el.classList.remove("md-nav__link--active") } diff --git a/src/assets/javascripts/component/container/index.ts b/src/assets/javascripts/component/container/index.ts index 989b6f7b7..63c7751d1 100644 --- a/src/assets/javascripts/component/container/index.ts +++ b/src/assets/javascripts/component/container/index.ts @@ -21,7 +21,13 @@ */ import { Observable, combineLatest } from "rxjs" -import { distinctUntilChanged, map, pluck, shareReplay } from "rxjs/operators" +import { + distinctUntilChanged, + map, + pluck, + shareReplay, + tap +} from "rxjs/operators" import { ViewportOffset, ViewportSize } from "../../ui" import { Header } from "../header" @@ -57,7 +63,7 @@ interface WatchOptions { * ------------------------------------------------------------------------- */ /** - * Create an observable to monitor the container + * Create an observable to watch the container * * The container represents the main content area including the sidebars (table * of contents and navigation), as well as the actual page content. diff --git a/src/assets/javascripts/component/header/_/index.ts b/src/assets/javascripts/component/header/_/index.ts new file mode 100644 index 000000000..a6b242e78 --- /dev/null +++ b/src/assets/javascripts/component/header/_/index.ts @@ -0,0 +1,97 @@ +/* + * 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 { + distinctUntilKeyChanged, + finalize, + shareReplay, + tap +} from "rxjs/operators" + +import { Container } from "../../container" +import { resetHeaderShadow, setHeaderShadow } from "../element" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Header + */ +export interface Header { + sticky: boolean /* Header stickyness */ + height: number /* Header visible height */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create an observable to watch the 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. + * + * @param el - Header element + * + * @return Header observable + */ +export function watchHeader( + el: HTMLElement +): Observable
{ + 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) + }) + ) +} diff --git a/src/assets/javascripts/component/header/element/index.ts b/src/assets/javascripts/component/header/element/index.ts new file mode 100644 index 000000000..16921e03e --- /dev/null +++ b/src/assets/javascripts/component/header/element/index.ts @@ -0,0 +1,48 @@ +/* + * 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 header shadow + * + * @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 el - Header element + */ +export function resetHeaderShadow( + el: HTMLElement +): void { + el.removeAttribute("data-md-state") +} diff --git a/src/assets/javascripts/component/header/index.ts b/src/assets/javascripts/component/header/index.ts index 7001e33af..634b1a4ff 100644 --- a/src/assets/javascripts/component/header/index.ts +++ b/src/assets/javascripts/component/header/index.ts @@ -20,67 +20,5 @@ * IN THE SOFTWARE. */ -import { Observable, of } from "rxjs" -import { shareReplay } from "rxjs/operators" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Header - */ -export interface Header { - sticky: boolean /* Header stickyness */ - height: number /* Header visible height */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set header shadow - * - * @param el - Header element - * @param shadow - Shadow - */ -export function setHeaderShadow( - el: HTMLElement, shadow: boolean -): void { - el.setAttribute("data-md-state", shadow ? "shadow" : "") -} - -/** - * Reset header - * - * @param el - Header element - */ -export function resetHeader(el: HTMLElement): void { - el.removeAttribute("data-md-state") -} - -/* ------------------------------------------------------------------------- */ - -/** - * Create an observable to monitor the header - * - * @param header - Header element - * - * @return Header observable - */ -export function watchHeader( - header: HTMLElement -): Observable
{ - const sticky = getComputedStyle(header) - .getPropertyValue("position") === "fixed" - - /* Return header as hot observable */ - return of({ - sticky, - height: sticky ? header.offsetHeight : 0 - }) - .pipe( - shareReplay({ bufferSize: 1, refCount: true }) - ) -} +export * from "./_" +export * from "./element" diff --git a/src/assets/javascripts/component/sidebar/_/index.ts b/src/assets/javascripts/component/sidebar/_/index.ts new file mode 100644 index 000000000..d8df809de --- /dev/null +++ b/src/assets/javascripts/component/sidebar/_/index.ts @@ -0,0 +1,145 @@ +/* + * 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 { equals } from "ramda" +import { + MonoTypeOperatorFunction, + Observable, + animationFrameScheduler, + combineLatest, + pipe +} from "rxjs" +import { + distinctUntilChanged, + finalize, + map, + observeOn, + shareReplay, + tap +} from "rxjs/operators" + +import { ViewportOffset } from "../../../ui" +import { Container } from "../../container" +import { + resetSidebarHeight, + resetSidebarLock, + setSidebarHeight, + setSidebarLock +} from "../element" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Sidebar + */ +export interface Sidebar { + height: number /* Sidebar height */ + lock: boolean /* Sidebar lock */ +} + +/* ---------------------------------------------------------------------------- + * Function types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + offset$: Observable /* Viewport offset observable */ + container$: Observable /* Container observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create an observable to watch a sidebar + * + * @param el - Sidebar element + * @param options - Options + * + * @return Sidebar observable + */ +export function watchSidebar( + el: HTMLElement, { offset$, container$ }: WatchOptions +): Observable { + + /* Adjust for internal container offset */ + const adjust = parseFloat( + getComputedStyle(el.parentElement!) + .getPropertyValue("padding-top") + ) + + /* Compute the sidebar's available height */ + const height$ = combineLatest(offset$, container$) + .pipe( + map(([{ y }, { offset, height }]) => { + return height - adjust + Math.min(adjust, Math.max(0, y - offset)) + }) + ) + + /* Compute whether the sidebar should be locked */ + const lock$ = combineLatest(offset$, container$) + .pipe( + map(([{ y }, { offset }]) => y >= offset + adjust) + ) + + /* Combine into single hot observable */ + return combineLatest(height$, lock$) + .pipe( + map(([height, lock]) => ({ height, lock })), + distinctUntilChanged(equals), + shareReplay({ bufferSize: 1, refCount: true }) + ) +} + +/* ------------------------------------------------------------------------- */ + +/** + * Paint sidebar from source observable + * + * @param el - Sidebar element + * + * @return Operator function + */ +export function paintSidebar( + el: HTMLElement +): MonoTypeOperatorFunction { + return pipe( + + /* Defer repaint to next animation frame */ + observeOn(animationFrameScheduler), + tap(({ height, lock }) => { + setSidebarHeight(el, height) + setSidebarLock(el, lock) + }), + + /* 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/component/sidebar/element/index.ts new file mode 100644 index 000000000..2f67225a3 --- /dev/null +++ b/src/assets/javascripts/component/sidebar/element/index.ts @@ -0,0 +1,73 @@ +/* + * 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 sidebar height + * + * @param el - Sidebar element + * @param value - Sidebar height + */ +export function setSidebarHeight( + el: HTMLElement, value: number +): void { + el.style.height = `${value}px` +} + +/** + * Reset sidebar height + * + * @param el - Sidebar element + */ +export function resetSidebarHeight( + el: HTMLElement +): void { + el.style.height = "" +} + +/* ------------------------------------------------------------------------- */ + +/** + * Set sidebar lock + * + * @param el - Sidebar element + * @param value - Whether the sidebar is locked + */ +export function setSidebarLock( + el: HTMLElement, value: boolean +): void { + el.setAttribute("data-md-state", value ? "lock" : "") +} + +/** + * Reset sidebar lock + * + * @param el - Sidebar element + */ +export function resetSidebarLock( + el: HTMLElement +): void { + el.removeAttribute("data-md-state") +} diff --git a/src/assets/javascripts/component/sidebar/index.ts b/src/assets/javascripts/component/sidebar/index.ts index e518b3d76..634b1a4ff 100644 --- a/src/assets/javascripts/component/sidebar/index.ts +++ b/src/assets/javascripts/component/sidebar/index.ts @@ -20,114 +20,5 @@ * IN THE SOFTWARE. */ -import { equals } from "ramda" -import { Observable, combineLatest } from "rxjs" -import { distinctUntilChanged, map, shareReplay } from "rxjs/operators" - -import { ViewportOffset } from "../../ui" -import { Container } from "../container" - -/* ---------------------------------------------------------------------------- - * Types - * ------------------------------------------------------------------------- */ - -/** - * Sidebar - */ -export interface Sidebar { - height: number /* Sidebar height */ - lock: boolean /* Sidebar lock */ -} - -/* ---------------------------------------------------------------------------- - * Function types - * ------------------------------------------------------------------------- */ - -/** - * Watch options - */ -interface WatchOptions { - container$: Observable /* Container observable */ - offset$: Observable /* Viewport offset observable */ -} - -/* ---------------------------------------------------------------------------- - * Functions - * ------------------------------------------------------------------------- */ - -/** - * Set sidebar height - * - * @param el - Sidebar element - * @param height - Sidebar height - */ -export function setSidebarHeight( - el: HTMLElement, height: number -): void { - el.style.height = `${height}px` -} - -/** - * Set sidebar lock - * - * @param el - Sidebar element - * @param lock - Whether the sidebar is locked - */ -export function setSidebarLock( - el: HTMLElement, lock: boolean -): void { - el.setAttribute("data-md-state", lock ? "lock" : "") -} - -/** - * Reset sidebar - * - * @param el - Sidebar element - */ -export function resetSidebar(el: HTMLElement): void { - el.removeAttribute("data-md-state") - el.style.height = "" -} - -/* ------------------------------------------------------------------------- */ - -/** - * Create an observable to monitor the sidebar - * - * @param el - Sidebar element - * @param options - Options - * - * @return Sidebar observable - */ -export function watchSidebar( - el: HTMLElement, { container$, offset$ }: WatchOptions -): Observable { - - /* Adjust for internal container offset */ - const adjust = parseFloat( - getComputedStyle(el.parentElement!) - .getPropertyValue("padding-top") - ) - - /* Compute the sidebar's available height */ - const height$ = combineLatest(offset$, container$) - .pipe( - map(([{ y }, { offset, height }]) => { - return height - adjust + Math.min(adjust, Math.max(0, y - offset)) - }) - ) - - /* Compute whether the sidebar should be locked */ - const lock$ = combineLatest(offset$, container$) - .pipe( - map(([{ y }, { offset }]) => y >= offset + adjust) - ) - - /* Combine into single hot observable */ - return combineLatest(height$, lock$) - .pipe( - map(([height, lock]) => ({ height, lock })), - distinctUntilChanged(equals), - shareReplay({ bufferSize: 1, refCount: true }) - ) -} +export * from "./_" +export * from "./element"