mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-28 09:20:52 +01:00
Refactored sidebar and container components
This commit is contained in:
parent
7a3d28b1ff
commit
4e14ff285e
@ -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)
|
||||
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),
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
@ -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...)
|
||||
}
|
||||
|
@ -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(
|
||||
return hash$
|
||||
.pipe(
|
||||
map(() => document.location.hash),
|
||||
startWith(document.location.hash),
|
||||
filter(hash => hash.length > 0),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
share()
|
||||
)
|
||||
}
|
||||
|
@ -93,10 +93,11 @@ export function getViewportSize(): ViewportSize {
|
||||
* @return Viewport offset observable
|
||||
*/
|
||||
export function fromViewportOffset(): Observable<ViewportOffset> {
|
||||
return merge(scroll$, resize$).pipe(
|
||||
return merge(scroll$, resize$)
|
||||
.pipe(
|
||||
map(getViewportOffset),
|
||||
startWith(getViewportOffset()),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@ -106,9 +107,10 @@ export function fromViewportOffset(): Observable<ViewportOffset> {
|
||||
* @return Viewport size observable
|
||||
*/
|
||||
export function fromViewportSize(): Observable<ViewportSize> {
|
||||
return resize$.pipe(
|
||||
return resize$
|
||||
.pipe(
|
||||
map(getViewportSize),
|
||||
startWith(getViewportSize()),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
@ -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(
|
||||
)
|
||||
.pipe(
|
||||
startWith(media.matches),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
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$)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user