Refactored sidebar and container components

This commit is contained in:
squidfunk 2019-10-28 16:13:13 +01:00
parent 7a3d28b1ff
commit 4e14ff285e
6 changed files with 156 additions and 88 deletions

View File

@ -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<Container> {
/* 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)
)
}

View File

@ -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> /* Container state observable */
toggle$: Observable<boolean> /* Toggle observable */
offset$: Observable<ViewportOffset> /* 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<Sidebar> {
const sidebar$ = toggle$.pipe(
filter(toggle => toggle === true),
switchMap(() => container$.pipe(
finalize(() => {
unsetSidebarHeight(sidebar)
unsetSidebarLock(sidebar)
}),
takeUntil(toggle$.pipe(
filter(toggle => toggle === false)
))
)),
map<Container, Sidebar>(({ 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)
)
}

View File

@ -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...)
}

View File

@ -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<HashChangeEvent>(window, "hashchange")
* @return Location hash observable
*/
export function fromLocationHash(): Observable<string> {
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()
)
}

View File

@ -93,11 +93,12 @@ export function getViewportSize(): ViewportSize {
* @return Viewport offset observable
*/
export function fromViewportOffset(): Observable<ViewportOffset> {
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<ViewportOffset> {
* @return Viewport size observable
*/
export function fromViewportSize(): Observable<ViewportSize> {
return resize$.pipe(
map(getViewportSize),
startWith(getViewportSize()),
shareReplay({ bufferSize: 1, refCount: true })
)
return resize$
.pipe(
map(getViewportSize),
startWith(getViewportSize()),
shareReplay(1)
)
}

View File

@ -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<boolean> {
const media = window.matchMedia(query)
return fromEventPattern<boolean>(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<T>(
query$: Observable<boolean>
): OperatorFunction<T, Observable<T>> {
const start$ = query$.pipe(filter(match => match === true))
const until$ = query$.pipe(filter(match => match === false))
return windowToggle<T, boolean>(start$, () => until$)
}