diff --git a/src/assets/javascripts/component/container/index.ts b/src/assets/javascripts/component/container/index.ts index 4147caa72..c1c667e6c 100644 --- a/src/assets/javascripts/component/container/index.ts +++ b/src/assets/javascripts/component/container/index.ts @@ -34,10 +34,10 @@ import { toArray } from "../../utilities" /** * Container state */ -export interface ContainerState { +export interface Container { offset: number /* Container top offset */ height: number /* Container height */ - active: boolean /* Scrolled past top */ + active: boolean /* Scrolled past top offset */ } /* ---------------------------------------------------------------------------- @@ -69,8 +69,8 @@ interface Options { * @return Container state observable */ export function fromContainer( - container: HTMLElement, header: HTMLElement, options: Options -): Observable { + container: HTMLElement, header: HTMLElement, { size$, offset$ }: Options +): Observable { /* Adjust top offset if header is fixed */ const adjust = getComputedStyle(header) @@ -79,7 +79,7 @@ export function fromContainer( : 0 /* Compute the container's top offset */ - const offset$ = options.size$.pipe( + const top$ = size$.pipe( map(() => reduce((offset, child) => { return Math.max(offset, child.offsetTop) }, 0, toArray(container.children)) - adjust), @@ -88,7 +88,7 @@ export function fromContainer( ) /* Compute the container's available height */ - const height$ = combineLatest(options.offset$, options.size$, offset$).pipe( + const height$ = combineLatest(offset$, size$, top$).pipe( map(([{ y }, { height }, offset]) => { const bottom = container.offsetTop + container.offsetHeight return height - adjust @@ -99,13 +99,13 @@ export function fromContainer( ) /* Compute whether the viewport offset is past the container's top */ - const active$ = combineLatest(options.offset$, offset$).pipe( + const active$ = combineLatest(offset$, top$).pipe( map(([{ y }, threshold]) => y >= threshold), distinctUntilChanged() ) /* Combine into a single hot observable */ - return combineLatest(offset$, height$, active$).pipe( + return combineLatest(top$, height$, active$).pipe( map(([offset, height, active]) => ({ offset, height, active })), shareReplay({ bufferSize: 1, refCount: true }) ) diff --git a/src/assets/javascripts/component/index.ts b/src/assets/javascripts/component/index.ts new file mode 100644 index 000000000..57a09ff05 --- /dev/null +++ b/src/assets/javascripts/component/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ + +export * from "./container" +export * from "./sidebar" diff --git a/src/assets/javascripts/component/sidebar/index.ts b/src/assets/javascripts/component/sidebar/index.ts new file mode 100644 index 000000000..28a08ba2f --- /dev/null +++ b/src/assets/javascripts/component/sidebar/index.ts @@ -0,0 +1,142 @@ +/* + * 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 { Observable } from "rxjs" +import { + filter, + finalize, + map, + shareReplay, + switchMap, + takeUntil +} from "rxjs/operators" + +import { Container } from "../container" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Sidebar state + */ +interface Sidebar { + height: number /* Sidebar height */ + lock: boolean /* Sidebar lock */ +} + +/* ---------------------------------------------------------------------------- + * Function types + * ------------------------------------------------------------------------- */ + +/** + * Options + */ +interface Options { + container$: Observable /* Container state observable */ + toggle$: Observable /* Toggle observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Set sidebar height + * + * @param sidebar - Sidebar HTML element + * @param height - Sidebar height + */ +export function setSidebarHeight(sidebar: HTMLElement, height: number): void { + sidebar.style.height = `${height}px` +} + +/** + * Unset sidebar height + * + * @param sidebar - Sidebar HTML element + */ +export function unsetSidebarHeight(sidebar: HTMLElement): void { + sidebar.style.height = "" +} + +/* ------------------------------------------------------------------------- */ + +/** + * Set sidebar lock + * + * @param sidebar - Sidebar HTML element + * @param lock - Sidebar lock + */ +export function setSidebarLock(sidebar: HTMLElement, lock: boolean): void { + sidebar.setAttribute("data-md-state", lock ? "lock" : "") +} + +/** + * Unset sidebar lock + * + * @param sidebar - Sidebar HTML element + */ +export function unsetSidebarLock(sidebar: HTMLElement): void { + sidebar.removeAttribute("data-md-state") +} + +/* ------------------------------------------------------------------------- */ + +/** + * Create a observable for a sidebar component + * + * @param sidebar - Sidebar element + * @param options - Options + * + * @return Sidebar state observable + */ +export function fromSidebar( + sidebar: HTMLElement, { container$, toggle$ }: Options +): Observable { + const sidebar$ = toggle$.pipe( + filter(toggle => toggle === true), + switchMap(() => container$.pipe( + finalize(() => { + unsetSidebarHeight(sidebar) + unsetSidebarLock(sidebar) + }), + takeUntil(toggle$.pipe( + filter(toggle => toggle === false) + )) + )), + map(({ height, active }) => ({ + height, + lock: active + })), + shareReplay({ bufferSize: 1, refCount: true }) + ) + + /* Subscribe sidebar element */ + sidebar$.subscribe(({ height, lock }) => { + setSidebarHeight(sidebar, height) + setSidebarLock(sidebar, lock) + }) + + /* Return sidebar observable */ + return sidebar$ +}