mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-24 15:40:15 +01:00
Refactored sidebar and container components
This commit is contained in:
parent
7a3d28b1ff
commit
4e14ff285e
@ -20,12 +20,10 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { reduce } from "ramda"
|
|
||||||
import { Observable, combineLatest } from "rxjs"
|
import { Observable, combineLatest } from "rxjs"
|
||||||
import { distinctUntilChanged, map, shareReplay } from "rxjs/operators"
|
import { distinctUntilChanged, map, shareReplay } from "rxjs/operators"
|
||||||
|
|
||||||
import { ViewportOffset, ViewportSize } from "../../ui"
|
import { ViewportOffset, ViewportSize } from "../../ui"
|
||||||
import { toArray } from "../../utilities"
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Types
|
* Types
|
||||||
@ -72,41 +70,40 @@ export function fromContainer(
|
|||||||
container: HTMLElement, header: HTMLElement, { size$, offset$ }: Options
|
container: HTMLElement, header: HTMLElement, { size$, offset$ }: Options
|
||||||
): Observable<Container> {
|
): Observable<Container> {
|
||||||
|
|
||||||
/* Adjust top offset if header is fixed */
|
/* Adjust for header offset if fixed */
|
||||||
const adjust = getComputedStyle(header)
|
const adjust = getComputedStyle(header)
|
||||||
.getPropertyValue("position") === "fixed"
|
.getPropertyValue("position") === "fixed"
|
||||||
? header.offsetHeight
|
? header.offsetHeight
|
||||||
: 0
|
: 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 */
|
/* Compute the container's available height */
|
||||||
const height$ = combineLatest(offset$, size$, top$).pipe(
|
const height$ = combineLatest(offset$, size$)
|
||||||
map(([{ y }, { height }, offset]) => {
|
.pipe(
|
||||||
const bottom = container.offsetTop + container.offsetHeight
|
map(([{ y }, { height }]) => {
|
||||||
return height - adjust
|
const top = container.offsetTop
|
||||||
- Math.max(0, offset - y)
|
const bottom = container.offsetHeight + top
|
||||||
|
return height
|
||||||
|
- Math.max(0, top - y, adjust)
|
||||||
- Math.max(0, height + y - bottom)
|
- Math.max(0, height + y - bottom)
|
||||||
}),
|
}),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Compute whether the viewport offset is past the container's top */
|
/* Compute whether the viewport offset is past the container's top */
|
||||||
const active$ = combineLatest(offset$, top$).pipe(
|
const active$ = offset$
|
||||||
map(([{ y }, threshold]) => y >= threshold),
|
.pipe(
|
||||||
|
map(({ y }) => y >= container.offsetTop - adjust),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Combine into a single hot observable */
|
/* Combine into a single hot observable */
|
||||||
return combineLatest(top$, height$, active$).pipe(
|
return combineLatest(height$, active$)
|
||||||
map(([offset, height, active]) => ({ offset, height, active })),
|
.pipe(
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
map(([height, active]) => ({
|
||||||
|
offset: container.offsetTop - adjust,
|
||||||
|
height,
|
||||||
|
active
|
||||||
|
})),
|
||||||
|
shareReplay(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,16 +20,10 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Observable } from "rxjs"
|
import { Observable, combineLatest } from "rxjs"
|
||||||
import {
|
import { map, shareReplay } from "rxjs/operators"
|
||||||
filter,
|
|
||||||
finalize,
|
|
||||||
map,
|
|
||||||
shareReplay,
|
|
||||||
switchMap,
|
|
||||||
takeUntil
|
|
||||||
} from "rxjs/operators"
|
|
||||||
|
|
||||||
|
import { ViewportOffset } from "../../ui"
|
||||||
import { Container } from "../container"
|
import { Container } from "../container"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
@ -53,7 +47,7 @@ export interface Sidebar {
|
|||||||
*/
|
*/
|
||||||
interface Options {
|
interface Options {
|
||||||
container$: Observable<Container> /* Container state observable */
|
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
|
* @return Sidebar state observable
|
||||||
*/
|
*/
|
||||||
export function fromSidebar(
|
export function fromSidebar(
|
||||||
sidebar: HTMLElement, { container$, toggle$ }: Options
|
sidebar: HTMLElement, { container$, offset$ }: Options
|
||||||
): Observable<Sidebar> {
|
): Observable<Sidebar> {
|
||||||
const sidebar$ = toggle$.pipe(
|
|
||||||
filter(toggle => toggle === true),
|
/* Adjust for internal container offset */
|
||||||
switchMap(() => container$.pipe(
|
const adjust = parseFloat(
|
||||||
finalize(() => {
|
getComputedStyle(sidebar.parentElement!)
|
||||||
unsetSidebarHeight(sidebar)
|
.getPropertyValue("padding-top")
|
||||||
unsetSidebarLock(sidebar)
|
|
||||||
}),
|
|
||||||
takeUntil(toggle$.pipe(
|
|
||||||
filter(toggle => toggle === false)
|
|
||||||
))
|
|
||||||
)),
|
|
||||||
map<Container, Sidebar>(({ height, active }) => ({
|
|
||||||
height,
|
|
||||||
lock: active
|
|
||||||
})),
|
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Subscribe sidebar element */
|
/* Compute the sidebars's available height */
|
||||||
sidebar$.subscribe(({ height, lock }) => {
|
const height$ = combineLatest(offset$, container$)
|
||||||
setSidebarHeight(sidebar, height)
|
.pipe(
|
||||||
setSidebarLock(sidebar, lock)
|
map(([{ y }, { offset, height }]) => {
|
||||||
|
return height - adjust
|
||||||
|
+ Math.min(adjust, Math.max(0, y - offset))
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
/* Return observable */
|
/* Compute whether the sidebar should be locked */
|
||||||
return sidebar$
|
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.
|
* 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 {
|
import {
|
||||||
fromContainer,
|
fromContainer,
|
||||||
fromSidebar
|
fromSidebar,
|
||||||
|
setSidebarHeight,
|
||||||
|
setSidebarLock,
|
||||||
|
unsetSidebarHeight,
|
||||||
|
unsetSidebarLock,
|
||||||
|
withToggle
|
||||||
} from "./component"
|
} from "./component"
|
||||||
import {
|
import {
|
||||||
fromMediaQuery,
|
fromMediaQuery,
|
||||||
fromViewportOffset,
|
fromViewportOffset,
|
||||||
fromViewportSize,
|
fromViewportSize,
|
||||||
getElement,
|
getElement,
|
||||||
|
withMediaQuery,
|
||||||
} from "./ui"
|
} from "./ui"
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
@ -57,12 +65,49 @@ const container$ = fromContainer(container, header, { size$, offset$ })
|
|||||||
// ---
|
// ---
|
||||||
|
|
||||||
const nav = getElement("[data-md-component=navigation")!
|
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 = 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) {
|
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 { Observable, fromEvent } from "rxjs"
|
||||||
import { filter, map, shareReplay, startWith } from "rxjs/operators"
|
import { filter, map, share, startWith } from "rxjs/operators"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Data
|
* Data
|
||||||
@ -42,10 +42,11 @@ const hash$ = fromEvent<HashChangeEvent>(window, "hashchange")
|
|||||||
* @return Location hash observable
|
* @return Location hash observable
|
||||||
*/
|
*/
|
||||||
export function fromLocationHash(): Observable<string> {
|
export function fromLocationHash(): Observable<string> {
|
||||||
return hash$.pipe(
|
return hash$
|
||||||
|
.pipe(
|
||||||
map(() => document.location.hash),
|
map(() => document.location.hash),
|
||||||
startWith(document.location.hash),
|
startWith(document.location.hash),
|
||||||
filter(hash => hash.length > 0),
|
filter(hash => hash.length > 0),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
share()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -93,10 +93,11 @@ export function getViewportSize(): ViewportSize {
|
|||||||
* @return Viewport offset observable
|
* @return Viewport offset observable
|
||||||
*/
|
*/
|
||||||
export function fromViewportOffset(): Observable<ViewportOffset> {
|
export function fromViewportOffset(): Observable<ViewportOffset> {
|
||||||
return merge(scroll$, resize$).pipe(
|
return merge(scroll$, resize$)
|
||||||
|
.pipe(
|
||||||
map(getViewportOffset),
|
map(getViewportOffset),
|
||||||
startWith(getViewportOffset()),
|
startWith(getViewportOffset()),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,9 +107,10 @@ export function fromViewportOffset(): Observable<ViewportOffset> {
|
|||||||
* @return Viewport size observable
|
* @return Viewport size observable
|
||||||
*/
|
*/
|
||||||
export function fromViewportSize(): Observable<ViewportSize> {
|
export function fromViewportSize(): Observable<ViewportSize> {
|
||||||
return resize$.pipe(
|
return resize$
|
||||||
|
.pipe(
|
||||||
map(getViewportSize),
|
map(getViewportSize),
|
||||||
startWith(getViewportSize()),
|
startWith(getViewportSize()),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,17 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Observable, fromEventPattern } from "rxjs"
|
import {
|
||||||
import { shareReplay, startWith } from "rxjs/operators"
|
Observable,
|
||||||
|
OperatorFunction,
|
||||||
|
fromEventPattern
|
||||||
|
} from "rxjs"
|
||||||
|
import {
|
||||||
|
filter,
|
||||||
|
shareReplay,
|
||||||
|
startWith,
|
||||||
|
windowToggle
|
||||||
|
} from "rxjs/operators"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
@ -38,8 +47,26 @@ export function fromMediaQuery(query: string): Observable<boolean> {
|
|||||||
const media = window.matchMedia(query)
|
const media = window.matchMedia(query)
|
||||||
return fromEventPattern<boolean>(next =>
|
return fromEventPattern<boolean>(next =>
|
||||||
media.addListener(() => next(media.matches))
|
media.addListener(() => next(media.matches))
|
||||||
).pipe(
|
)
|
||||||
|
.pipe(
|
||||||
startWith(media.matches),
|
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