diff --git a/src/assets/javascripts/component/container/index.ts b/src/assets/javascripts/component/container/index.ts index c1c667e6c..5c7839c15 100644 --- a/src/assets/javascripts/component/container/index.ts +++ b/src/assets/javascripts/component/container/index.ts @@ -20,12 +20,10 @@ * 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 @@ -72,41 +70,40 @@ export function fromContainer( container: HTMLElement, header: HTMLElement, { size$, offset$ }: Options ): Observable { - /* Adjust top offset if header is fixed */ + /* Adjust for header offset if fixed */ const adjust = getComputedStyle(header) .getPropertyValue("position") === "fixed" ? header.offsetHeight : 0 - /* Compute the container's top offset */ - const top$ = 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(offset$, size$, top$).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() - ) + const height$ = combineLatest(offset$, size$) + .pipe( + map(([{ y }, { height }]) => { + const top = container.offsetTop + const bottom = container.offsetHeight + top + return height + - Math.max(0, top - y, adjust) + - Math.max(0, height + y - bottom) + }), + distinctUntilChanged() + ) /* Compute whether the viewport offset is past the container's top */ - const active$ = combineLatest(offset$, top$).pipe( - map(([{ y }, threshold]) => y >= threshold), - distinctUntilChanged() - ) + const active$ = offset$ + .pipe( + map(({ y }) => y >= container.offsetTop - adjust), + distinctUntilChanged() + ) /* Combine into a single hot observable */ - return combineLatest(top$, height$, active$).pipe( - map(([offset, height, active]) => ({ offset, height, active })), - shareReplay({ bufferSize: 1, refCount: true }) - ) + return combineLatest(height$, active$) + .pipe( + map(([height, active]) => ({ + offset: container.offsetTop - adjust, + height, + active + })), + shareReplay(1) + ) } diff --git a/src/assets/javascripts/component/sidebar/index.ts b/src/assets/javascripts/component/sidebar/index.ts index e46b5411a..1e86cf610 100644 --- a/src/assets/javascripts/component/sidebar/index.ts +++ b/src/assets/javascripts/component/sidebar/index.ts @@ -20,16 +20,10 @@ * IN THE SOFTWARE. */ -import { Observable } from "rxjs" -import { - filter, - finalize, - map, - shareReplay, - switchMap, - takeUntil -} from "rxjs/operators" +import { Observable, combineLatest } from "rxjs" +import { map, shareReplay } from "rxjs/operators" +import { ViewportOffset } from "../../ui" import { Container } from "../container" /* ---------------------------------------------------------------------------- @@ -53,7 +47,7 @@ export interface Sidebar { */ interface Options { container$: Observable /* Container state observable */ - toggle$: Observable /* Toggle observable */ + offset$: Observable /* Viewport offset observable */ } /* ---------------------------------------------------------------------------- @@ -111,32 +105,34 @@ export function unsetSidebarLock(sidebar: HTMLElement): void { * @return Sidebar state observable */ export function fromSidebar( - sidebar: HTMLElement, { container$, toggle$ }: Options + sidebar: HTMLElement, { container$, offset$ }: 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 }) + + /* Adjust for internal container offset */ + const adjust = parseFloat( + getComputedStyle(sidebar.parentElement!) + .getPropertyValue("padding-top") ) - /* Subscribe sidebar element */ - sidebar$.subscribe(({ height, lock }) => { - setSidebarHeight(sidebar, height) - setSidebarLock(sidebar, lock) - }) + /* Compute the sidebars's available height */ + const height$ = combineLatest(offset$, container$) + .pipe( + map(([{ y }, { offset, height }]) => { + return height - adjust + + Math.min(adjust, Math.max(0, y - offset)) + }) + ) - /* Return observable */ - return sidebar$ + /* 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 })), + shareReplay(1) + ) } diff --git a/src/assets/javascripts/index.ts b/src/assets/javascripts/index.ts index 82579f40b..94c4847b6 100644 --- a/src/assets/javascripts/index.ts +++ b/src/assets/javascripts/index.ts @@ -20,15 +20,23 @@ * IN THE SOFTWARE. */ +import { animationFrameScheduler, interval, of } from "rxjs" +import { concatMap, concatMapTo, filter, finalize, mapTo, mergeMap, skipUntil, startWith, switchMap, takeUntil, tap, throttleTime, windowToggle } from "rxjs/operators" import { fromContainer, - fromSidebar + fromSidebar, + setSidebarHeight, + setSidebarLock, + unsetSidebarHeight, + unsetSidebarLock, + withToggle } from "./component" import { fromMediaQuery, fromViewportOffset, fromViewportSize, getElement, + withMediaQuery, } from "./ui" // ---------------------------------------------------------------------------- @@ -57,12 +65,49 @@ const container$ = fromContainer(container, header, { size$, offset$ }) // --- const nav = getElement("[data-md-component=navigation")! -const nav$ = fromSidebar(nav, { container$, toggle$: screenAndAbove$ }) + +fromSidebar(nav, { container$, offset$ }) + .pipe( + withMediaQuery(screenAndAbove$), + concatMap(sidebar$ => sidebar$.pipe( + finalize(() => { + unsetSidebarHeight(nav) + unsetSidebarLock(nav) + }) + )) + ) + .subscribe(({ height, lock }) => { + setSidebarHeight(nav, height) + setSidebarLock(nav, lock) + }) const toc = getElement("[data-md-component=toc")! -const toc$ = fromSidebar(toc, { container$, toggle$: tabletAndAbove$ }) + +fromSidebar(toc, { container$, offset$ }) + .pipe( + withMediaQuery(tabletAndAbove$), + concatMap(sidebar$ => sidebar$.pipe( + finalize(() => { + unsetSidebarHeight(toc) + unsetSidebarLock(toc) + }) + )) + ) + .subscribe(({ height, lock }) => { + setSidebarHeight(toc, height) + setSidebarLock(toc, lock) + }) // ---------------------------------------------------------------------------- export function app(config: any) { + // TODO: + let parent = container.parentElement as HTMLElement + const height = 0 + + // TODO: write a fromHeader (?) component observable which + // this fromHeader should take the container and ...? + // container$.subscribe() + + // container padding = "with parent" + 30px (padding of container...) } diff --git a/src/assets/javascripts/ui/location/index.ts b/src/assets/javascripts/ui/location/index.ts index f0690f2c1..28706823c 100644 --- a/src/assets/javascripts/ui/location/index.ts +++ b/src/assets/javascripts/ui/location/index.ts @@ -21,7 +21,7 @@ */ import { Observable, fromEvent } from "rxjs" -import { filter, map, shareReplay, startWith } from "rxjs/operators" +import { filter, map, share, startWith } from "rxjs/operators" /* ---------------------------------------------------------------------------- * Data @@ -42,10 +42,11 @@ const hash$ = fromEvent(window, "hashchange") * @return Location hash observable */ export function fromLocationHash(): Observable { - return hash$.pipe( - map(() => document.location.hash), - startWith(document.location.hash), - filter(hash => hash.length > 0), - shareReplay({ bufferSize: 1, refCount: true }) - ) + return hash$ + .pipe( + map(() => document.location.hash), + startWith(document.location.hash), + filter(hash => hash.length > 0), + share() + ) } diff --git a/src/assets/javascripts/ui/viewport/_/index.ts b/src/assets/javascripts/ui/viewport/_/index.ts index 2d3765e67..7cc2c8af4 100644 --- a/src/assets/javascripts/ui/viewport/_/index.ts +++ b/src/assets/javascripts/ui/viewport/_/index.ts @@ -93,11 +93,12 @@ export function getViewportSize(): ViewportSize { * @return Viewport offset observable */ export function fromViewportOffset(): Observable { - return merge(scroll$, resize$).pipe( - map(getViewportOffset), - startWith(getViewportOffset()), - shareReplay({ bufferSize: 1, refCount: true }) - ) + return merge(scroll$, resize$) + .pipe( + map(getViewportOffset), + startWith(getViewportOffset()), + shareReplay(1) + ) } /** @@ -106,9 +107,10 @@ export function fromViewportOffset(): Observable { * @return Viewport size observable */ export function fromViewportSize(): Observable { - return resize$.pipe( - map(getViewportSize), - startWith(getViewportSize()), - shareReplay({ bufferSize: 1, refCount: true }) - ) + return resize$ + .pipe( + map(getViewportSize), + startWith(getViewportSize()), + shareReplay(1) + ) } diff --git a/src/assets/javascripts/ui/viewport/breakpoint/index.ts b/src/assets/javascripts/ui/viewport/breakpoint/index.ts index 377ca7365..510dd09d1 100644 --- a/src/assets/javascripts/ui/viewport/breakpoint/index.ts +++ b/src/assets/javascripts/ui/viewport/breakpoint/index.ts @@ -20,8 +20,17 @@ * IN THE SOFTWARE. */ -import { Observable, fromEventPattern } from "rxjs" -import { shareReplay, startWith } from "rxjs/operators" +import { + Observable, + OperatorFunction, + fromEventPattern +} from "rxjs" +import { + filter, + shareReplay, + startWith, + windowToggle +} from "rxjs/operators" /* ---------------------------------------------------------------------------- * Functions @@ -38,8 +47,26 @@ export function fromMediaQuery(query: string): Observable { const media = window.matchMedia(query) return fromEventPattern(next => media.addListener(() => next(media.matches)) - ).pipe( - startWith(media.matches), - shareReplay({ bufferSize: 1, refCount: true }) ) + .pipe( + startWith(media.matches), + shareReplay(1) + ) +} + +/** + * Only emit values from the source observable when a media query matches + * + * @template T - Observable value type + * + * @param query - Media query observable + * + * @return Observable of source observable values + */ +export function withMediaQuery( + query$: Observable +): OperatorFunction> { + const start$ = query$.pipe(filter(match => match === true)) + const until$ = query$.pipe(filter(match => match === false)) + return windowToggle(start$, () => until$) }