From b9be2ee8752f2006075ed4727786595f83cccf3b Mon Sep 17 00:00:00 2001 From: squidfunk Date: Sun, 27 Oct 2019 16:21:49 +0100 Subject: [PATCH] Added container component implementation --- .../javascripts/component/container/index.ts | 112 ++++++++++++++++++ src/assets/javascripts/ui/location/index.ts | 7 +- 2 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 src/assets/javascripts/component/container/index.ts diff --git a/src/assets/javascripts/component/container/index.ts b/src/assets/javascripts/component/container/index.ts new file mode 100644 index 000000000..4147caa72 --- /dev/null +++ b/src/assets/javascripts/component/container/index.ts @@ -0,0 +1,112 @@ +/* + * 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 { reduce } from "ramda" +import { Observable, combineLatest } from "rxjs" +import { distinctUntilChanged, map, shareReplay } from "rxjs/operators" + +import { ViewportOffset, ViewportSize } from "../../ui" +import { toArray } from "../../utilities" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Container state + */ +export interface ContainerState { + offset: number /* Container top offset */ + height: number /* Container height */ + active: boolean /* Scrolled past top */ +} + +/* ---------------------------------------------------------------------------- + * Function types + * ------------------------------------------------------------------------- */ + +/** + * Options + */ +interface Options { + size$: Observable /* Viewport size observable */ + offset$: Observable /* Viewport offset observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create an observable for the container component + * + * The container represents the main content area including the sidebars (table + * of contents and navigation), as well as the actual page content. + * + * @param container - Container element + * @param header - Header element + * @param options - Options + * + * @return Container state observable + */ +export function fromContainer( + container: HTMLElement, header: HTMLElement, options: Options +): Observable { + + /* Adjust top offset if header is fixed */ + const adjust = getComputedStyle(header) + .getPropertyValue("position") === "fixed" + ? header.offsetHeight + : 0 + + /* Compute the container's top offset */ + const offset$ = options.size$.pipe( + map(() => reduce((offset, child) => { + return Math.max(offset, child.offsetTop) + }, 0, toArray(container.children)) - adjust), + distinctUntilChanged(), + shareReplay({ bufferSize: 1, refCount: true }) + ) + + /* Compute the container's available height */ + const height$ = combineLatest(options.offset$, options.size$, offset$).pipe( + map(([{ y }, { height }, offset]) => { + const bottom = container.offsetTop + container.offsetHeight + return height - adjust + - Math.max(0, offset - y) + - Math.max(0, height + y - bottom) + }), + distinctUntilChanged() + ) + + /* Compute whether the viewport offset is past the container's top */ + const active$ = combineLatest(options.offset$, offset$).pipe( + map(([{ y }, threshold]) => y >= threshold), + distinctUntilChanged() + ) + + /* Combine into a single hot observable */ + return combineLatest(offset$, height$, active$).pipe( + map(([offset, height, active]) => ({ offset, height, active })), + shareReplay({ bufferSize: 1, refCount: true }) + ) +} diff --git a/src/assets/javascripts/ui/location/index.ts b/src/assets/javascripts/ui/location/index.ts index 18521479b..f0690f2c1 100644 --- a/src/assets/javascripts/ui/location/index.ts +++ b/src/assets/javascripts/ui/location/index.ts @@ -21,12 +21,7 @@ */ import { Observable, fromEvent } from "rxjs" -import { - filter, - map, - shareReplay, - startWith -} from "rxjs/operators" +import { filter, map, shareReplay, startWith } from "rxjs/operators" /* ---------------------------------------------------------------------------- * Data