mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-28 09:20:52 +01:00
Refactored entrypoint and observable setup
This commit is contained in:
parent
62ead4092f
commit
a70a42cdd1
@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2019 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* 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 { difference, reverse } from "ramda"
|
||||
import {
|
||||
MonoTypeOperatorFunction,
|
||||
Observable,
|
||||
animationFrameScheduler,
|
||||
combineLatest,
|
||||
pipe
|
||||
} from "rxjs"
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
finalize,
|
||||
map,
|
||||
observeOn,
|
||||
scan,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { ViewportOffset, ViewportSize, getElement } from "../../../ui"
|
||||
import { Header } from "../../header"
|
||||
import {
|
||||
resetAnchorActive,
|
||||
resetAnchorBlur,
|
||||
setAnchorActive,
|
||||
setAnchorBlur
|
||||
} from "../element"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Anchor list
|
||||
*/
|
||||
export interface AnchorList {
|
||||
done: HTMLAnchorElement[][] /* Done anchors */
|
||||
next: HTMLAnchorElement[][] /* Next anchors */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Function types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
size$: Observable<ViewportSize> /* Viewport size observable */
|
||||
offset$: Observable<ViewportOffset> /* Viewport offset observable */
|
||||
header$: Observable<Header> /* Header observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create an observable to watch an anchor list
|
||||
*
|
||||
* This is effectively a scroll-spy implementation which will account for the
|
||||
* fixed header and automatically re-calculate anchor offsets when the viewport
|
||||
* is resized. The returned observable will only emit if the anchor list needs
|
||||
* to be repainted.
|
||||
*
|
||||
* This implementation tracks an anchor element's entire path starting from its
|
||||
* level up to the top-most anchor element, e.g. `[h3, h2, h1]`. Although the
|
||||
* Material theme currently doesn't make use of this information, it enables
|
||||
* the styling of the entire hierarchy through customization.
|
||||
*
|
||||
* @param els - Anchor elements
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Anchor list observable
|
||||
*/
|
||||
export function watchAnchorList(
|
||||
els: HTMLAnchorElement[], { size$, offset$, header$ }: WatchOptions
|
||||
): Observable<AnchorList> {
|
||||
const table = new Map<HTMLAnchorElement, HTMLElement>()
|
||||
for (const el of els) {
|
||||
const id = decodeURIComponent(el.hash.substring(1))
|
||||
const target = getElement(`[id="${id}"]`)
|
||||
if (typeof target !== "undefined")
|
||||
table.set(el, target)
|
||||
}
|
||||
|
||||
/* Compute necessary adjustment for header */
|
||||
const adjust$ = header$
|
||||
.pipe(
|
||||
map(header => 18 + header.height)
|
||||
)
|
||||
|
||||
/* Compute partition of done and next anchors */
|
||||
const partition$ = size$.pipe(
|
||||
|
||||
/* Build index to map anchor paths to vertical offsets */
|
||||
map(() => {
|
||||
let path: HTMLAnchorElement[] = []
|
||||
return [...table].reduce((index, [anchor, target]) => {
|
||||
while (path.length) {
|
||||
const last = table.get(path[path.length - 1])!
|
||||
if (last.tagName >= target.tagName) {
|
||||
path.pop()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return index.set(
|
||||
reverse(path = [...path, anchor]),
|
||||
target.offsetTop
|
||||
)
|
||||
}, new Map<HTMLAnchorElement[], number>())
|
||||
}),
|
||||
|
||||
/* Re-compute partition when viewport offset changes */
|
||||
switchMap(index => combineLatest(offset$, adjust$)
|
||||
.pipe(
|
||||
scan(([done, next], [{ y }, adjust]) => {
|
||||
|
||||
/* Look forward */
|
||||
while (next.length) {
|
||||
const [, offset] = next[0]
|
||||
if (offset - adjust < y) {
|
||||
done = [...done, next.shift()!]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/* Look backward */
|
||||
while (done.length) {
|
||||
const [, offset] = done[done.length - 1]
|
||||
if (offset - adjust >= y) {
|
||||
next = [done.pop()!, ...next]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/* Return partition */
|
||||
return [done, next]
|
||||
}, [[], [...index]]),
|
||||
distinctUntilChanged((a, b) => {
|
||||
return a[0] === b[0]
|
||||
&& a[1] === b[1]
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
/* Extract anchor list and return hot observable */
|
||||
return partition$
|
||||
.pipe(
|
||||
map(([done, next]) => ({
|
||||
done: done.map(([path]) => path),
|
||||
next: next.map(([path]) => path)
|
||||
})),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Paint anchor list from source observable
|
||||
*
|
||||
* This operator function will keep track of the anchor list in-between emits
|
||||
* in order to optimize rendering by only repainting anchor list migrations.
|
||||
* After determining which anchors need to be repainted, the actual rendering
|
||||
* is deferred to the next animation frame.
|
||||
*
|
||||
* @param els - Anchor elements
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function paintAnchorList(
|
||||
els: HTMLAnchorElement[]
|
||||
): MonoTypeOperatorFunction<AnchorList> {
|
||||
return pipe(
|
||||
|
||||
/* Extract anchor list migrations only */
|
||||
scan<AnchorList>((a, b) => {
|
||||
const begin = Math.max(0, Math.min(b.done.length, a.done.length) - 1)
|
||||
const end = Math.max(b.done.length, a.done.length)
|
||||
return {
|
||||
done: b.done.slice(begin, end + 1),
|
||||
next: difference(b.next, a.next)
|
||||
}
|
||||
}, { done: [], next: [] }),
|
||||
|
||||
/* Defer repaint to next animation frame */
|
||||
observeOn(animationFrameScheduler),
|
||||
tap(({ done, next }) => {
|
||||
|
||||
/* Look forward */
|
||||
for (const [el] of next) {
|
||||
resetAnchorActive(el)
|
||||
resetAnchorBlur(el)
|
||||
}
|
||||
|
||||
/* Look backward */
|
||||
for (const [index, [el]] of done.entries()) {
|
||||
setAnchorActive(el, index === done.length - 1)
|
||||
setAnchorBlur(el, true)
|
||||
}
|
||||
}),
|
||||
|
||||
/* Reset on complete or error */
|
||||
finalize(() => {
|
||||
for (const el of els) {
|
||||
resetAnchorActive(el)
|
||||
resetAnchorBlur(el)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2019 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Set anchor blur
|
||||
*
|
||||
* @param el - Anchor element
|
||||
* @param value - Whether the anchor is blurred
|
||||
*/
|
||||
export function setAnchorBlur(
|
||||
el: HTMLElement, value: boolean
|
||||
): void {
|
||||
el.setAttribute("data-md-state", value ? "blur" : "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset anchor blur
|
||||
*
|
||||
* @param el - Anchor element
|
||||
*/
|
||||
export function resetAnchorBlur(
|
||||
el: HTMLElement
|
||||
): void {
|
||||
el.removeAttribute("data-md-state")
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Set anchor active
|
||||
*
|
||||
* @param el - Anchor element
|
||||
* @param value - Whether the anchor is active
|
||||
*/
|
||||
export function setAnchorActive(
|
||||
el: HTMLElement, value: boolean
|
||||
): void {
|
||||
el.classList.toggle("md-nav__link--active", value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset anchor active
|
||||
*
|
||||
* @param el - Anchor element
|
||||
*/
|
||||
export function resetAnchorActive(
|
||||
el: HTMLElement
|
||||
): void {
|
||||
el.classList.remove("md-nav__link--active")
|
||||
}
|
@ -20,29 +20,29 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Configuration
|
||||
*/
|
||||
export interface Config {
|
||||
base: string /* Base URL */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Set header shadow
|
||||
* Ensure that the given value is a valid configuration
|
||||
*
|
||||
* @param el - Header element
|
||||
* @param value - Whether the shadow is shown
|
||||
*/
|
||||
export function setHeaderShadow(
|
||||
el: HTMLElement, value: boolean
|
||||
): void {
|
||||
el.setAttribute("data-md-state", value ? "shadow" : "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset header shadow
|
||||
* @param config - Configuration
|
||||
*
|
||||
* @param el - Header element
|
||||
* @return Test result
|
||||
*/
|
||||
export function resetHeaderShadow(
|
||||
el: HTMLElement
|
||||
): void {
|
||||
el.removeAttribute("data-md-state")
|
||||
export function isConfig(config: any): config is Config {
|
||||
return typeof config === "object"
|
||||
&& typeof config.base === "string"
|
||||
}
|
@ -20,412 +20,128 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { findLast } from "ramda"
|
||||
import {
|
||||
NEVER,
|
||||
animationFrameScheduler,
|
||||
fromEvent,
|
||||
merge,
|
||||
of
|
||||
} from "rxjs"
|
||||
import { AjaxRequest, ajax } from "rxjs/ajax"
|
||||
import {
|
||||
delay,
|
||||
filter,
|
||||
map,
|
||||
mapTo,
|
||||
observeOn,
|
||||
scan,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import "./polyfill"
|
||||
import { shareReplay, switchMap } from "rxjs/operators"
|
||||
|
||||
import { isConfig } from "./config"
|
||||
import {
|
||||
paintAnchorList,
|
||||
paintComponentMap,
|
||||
paintHeaderShadow,
|
||||
paintSidebar,
|
||||
pluckComponent,
|
||||
setNavigationOverflowScrolling,
|
||||
watchAnchorList,
|
||||
setupSidebar,
|
||||
switchComponent,
|
||||
switchMapIfActive,
|
||||
watchComponentMap,
|
||||
watchHeader,
|
||||
watchMain,
|
||||
watchNavigationIndex,
|
||||
watchSidebar
|
||||
} from "./component"
|
||||
watchMain
|
||||
} from "./theme"
|
||||
import {
|
||||
getElement,
|
||||
getElements,
|
||||
watchDocument,
|
||||
watchDocumentSwitch,
|
||||
watchLocation,
|
||||
watchLocationHash,
|
||||
watchLocationFragment,
|
||||
watchMedia,
|
||||
watchViewportOffset,
|
||||
watchViewportSize,
|
||||
withElement
|
||||
watchViewportSize
|
||||
} from "./ui"
|
||||
import { toggle } from "./utilities"
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Disclaimer: this file is currently heavy WIP
|
||||
// ----------------------------------------------------------------------------
|
||||
// TBD
|
||||
|
||||
const offset$ = watchViewportOffset()
|
||||
const size$ = watchViewportSize()
|
||||
const names = [
|
||||
"header", /* Header */
|
||||
"title", /* Header title */
|
||||
"search", /* Search */
|
||||
"query", /* Search input */
|
||||
"reset", /* Search reset */
|
||||
"result", /* Search results */
|
||||
"container", /* Container */
|
||||
"main", /* Main area */
|
||||
"hero", /* Hero */
|
||||
"tabs", /* Tabs */
|
||||
"navigation", /* Navigation */
|
||||
"toc" /* Table of contents */
|
||||
] as const // TODO: put this somewhere else... (merge with config!) JSON schema!?
|
||||
|
||||
const aboveScreen$ = watchMedia("(min-width: 1220px)")
|
||||
const belowScreen$ = watchMedia("(max-width: 1219px)")
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const aboveTablet$ = watchMedia("(min-width: 960px)")
|
||||
const belowTablet$ = watchMedia("(max-width: 959px)")
|
||||
/**
|
||||
* Initialize Material for MkDocs
|
||||
*
|
||||
* @param config - Configuration
|
||||
*/
|
||||
export function initialize(config: unknown) {
|
||||
if (!isConfig(config))
|
||||
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
// modernizr for the poor
|
||||
document.documentElement.classList.remove("no-js")
|
||||
document.documentElement.classList.add("js")
|
||||
/* Create viewport observables */
|
||||
const offset$ = watchViewportOffset()
|
||||
const size$ = watchViewportSize()
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
/* Create media observables */
|
||||
const screen$ = watchMedia("(min-width: 1220px)")
|
||||
const tablet$ = watchMedia("(min-width: 960px)")
|
||||
|
||||
// Observable that resolves with document when loaded
|
||||
const init$ = fromEvent(document, "DOMContentLoaded")
|
||||
.pipe(
|
||||
mapTo(document),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
)
|
||||
/* Create location observables */
|
||||
const url$ = watchLocation()
|
||||
const fragment$ = watchLocationFragment()
|
||||
|
||||
// Location subject
|
||||
const location$ = watchLocation()
|
||||
/* Create document observables */
|
||||
const load$ = watchDocument()
|
||||
const switch$ = watchDocumentSwitch({ url$ })
|
||||
|
||||
// Observable that resolves with document on XHR load
|
||||
const reload$ = location$
|
||||
.pipe(
|
||||
switchMap(url => load(url))
|
||||
)
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
// Extract and (re-)paint components
|
||||
const components$ = merge(init$, reload$)
|
||||
.pipe(
|
||||
switchMap(watchComponentMap),
|
||||
paintComponentMap(),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
)
|
||||
/* Create component map observable */
|
||||
const components$ = watchComponentMap(names, { load$, switch$ })
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const header$ = components$
|
||||
.pipe(
|
||||
pluckComponent("header"),
|
||||
switchMap(watchHeader)
|
||||
)
|
||||
|
||||
const main$ = components$
|
||||
.pipe(
|
||||
pluckComponent("main"),
|
||||
switchMap(el => watchMain(el, { size$, offset$, header$ })),
|
||||
shareReplay({ bufferSize: 1, refCount: true})
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/* Component: sidebar with navigation */
|
||||
components$
|
||||
.pipe(
|
||||
pluckComponent("navigation"),
|
||||
switchMap(el => aboveScreen$
|
||||
.pipe(
|
||||
toggle(() => watchSidebar(el, { offset$, main$ })
|
||||
.pipe(
|
||||
paintSidebar(el)
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
/* Component: sidebar with table of contents (missing on 404 page) */
|
||||
components$
|
||||
.pipe(
|
||||
pluckComponent("toc"),
|
||||
switchMap(el => aboveTablet$
|
||||
.pipe(
|
||||
toggle(() => watchSidebar(el, { offset$, main$ })
|
||||
.pipe(
|
||||
paintSidebar(el)
|
||||
)
|
||||
)
|
||||
))
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
/* Component: link blurring for table of contents */
|
||||
components$
|
||||
.pipe(
|
||||
pluckComponent("toc"),
|
||||
map(el => getElements<HTMLAnchorElement>(".md-nav__link", el)),
|
||||
switchMap(els => aboveTablet$
|
||||
.pipe(
|
||||
toggle(() => watchAnchorList(els, { size$, offset$, header$ })
|
||||
.pipe(
|
||||
paintAnchorList(els)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
/* Component: header shadow toggle */
|
||||
components$
|
||||
.pipe(
|
||||
pluckComponent("header"),
|
||||
switchMap(el => main$.pipe(
|
||||
paintHeaderShadow(el)
|
||||
))
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Refactor:
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Observable that catches all internal links without the necessity of rebinding
|
||||
// as events are bubbled up through the DOM.
|
||||
init$
|
||||
.pipe(
|
||||
switchMap(({ body }) => fromEvent(body, "click")),
|
||||
switchMap(ev => {
|
||||
|
||||
/* Walk up as long as we're not in a details tag */
|
||||
let parent = ev.target as Node | undefined
|
||||
while (parent && !(parent instanceof HTMLAnchorElement))
|
||||
parent = parent.parentNode // TODO: fix errors...
|
||||
|
||||
if (parent) { // this one OR (!) one of
|
||||
// its parents...
|
||||
if (!/(:\/\/|^#[^\/]+$)/.test(parent.getAttribute("href")!)) {
|
||||
ev.preventDefault()
|
||||
console.log("> ", parent.href)
|
||||
|
||||
// Extract URL; push to state, then emit new URL
|
||||
const href = parent.href
|
||||
history.pushState({}, "", href) // move this somewhere else!???
|
||||
return of(href)
|
||||
}
|
||||
}
|
||||
return NEVER
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
)
|
||||
.subscribe(location$)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const nav2 = getElement("[data-md-component=navigation]")!
|
||||
const index$ = watchNavigationIndex(nav2) // TODO: maybe rename into setup!? merge with sidebar?
|
||||
belowScreen$
|
||||
.pipe(
|
||||
toggle(() => index$
|
||||
.pipe(
|
||||
switchMap(index => merge(...[...index.keys()]
|
||||
.map(input => fromEvent(input, "change"))
|
||||
)
|
||||
.pipe(
|
||||
mapTo(index)
|
||||
)
|
||||
),
|
||||
map(index => getElement("ul", index.get(
|
||||
findLast(input => input.checked, [...index.keys()])!)
|
||||
)!), // find the TOP MOST! <-- this is the actively displayed on mobile
|
||||
|
||||
// this is the paint job...
|
||||
|
||||
// dispatch action - TODO: document why this crap is even necessary
|
||||
scan((prev, next) => {
|
||||
if (prev)
|
||||
setNavigationOverflowScrolling(prev, false) // TODO: resetOverflowScrolling ....
|
||||
return next
|
||||
}),
|
||||
delay(250),
|
||||
tap(next => {
|
||||
setNavigationOverflowScrolling(next, true) // setNavigationScrollfix
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function isNavigationCollapsible(el: HTMLElement): boolean {
|
||||
return el.getAttribute("data-md-component") === "collapsible" // TODO: maybe better remove again
|
||||
}
|
||||
|
||||
aboveScreen$
|
||||
.pipe(
|
||||
toggle(() => index$
|
||||
.pipe(
|
||||
// map(index => )
|
||||
// filter shit from index...
|
||||
switchMap(index => [...index.keys()]
|
||||
.filter(input => isNavigationCollapsible(index.get(input)!))
|
||||
.map(input => {
|
||||
const el = index.get(input)!
|
||||
// this doesnt work...
|
||||
el.setAttribute("data-md-height", `${el.offsetHeight}`) // TODO: this is a hack
|
||||
return input
|
||||
})
|
||||
.map(input => fromEvent(input, "change")
|
||||
.pipe(
|
||||
map(() => {
|
||||
const el = index.get(input)!
|
||||
let height = parseInt(el.getAttribute("data-md-height")!, 10)
|
||||
// always goes from data-md-height... wrong...
|
||||
if (!input.checked) {
|
||||
el.style.maxHeight = `${height}px`
|
||||
|
||||
/* Set target height */
|
||||
height = 0
|
||||
|
||||
} else {
|
||||
el.style.maxHeight = "initial" // 100%!?
|
||||
el.style.transitionDuration = "initial"
|
||||
|
||||
/* Retrieve target height */
|
||||
height = el.offsetHeight
|
||||
console.log("expand to height")
|
||||
|
||||
/* Reset state and set start height */
|
||||
// el.removeAttribute("data-md-state")
|
||||
el.style.maxHeight = "0px"
|
||||
}
|
||||
|
||||
/* Force style recalculation */
|
||||
el.offsetHeight // tslint:disable-line
|
||||
el.style.transitionDuration = ""
|
||||
return height
|
||||
}), // max height is set... just read it.
|
||||
observeOn(animationFrameScheduler),
|
||||
tap(height => {
|
||||
const el = index.get(input)!
|
||||
// el.setAttribute("data-md-state", "animate")
|
||||
el.style.maxHeight = `${height}px`
|
||||
console.log("setting shit...")
|
||||
|
||||
el.setAttribute("data-md-height", `${height}`)
|
||||
}),
|
||||
delay(250),
|
||||
tap(() => {
|
||||
const el = index.get(input)!
|
||||
console.log("DONE")
|
||||
// el.removeAttribute("data-md-state")
|
||||
el.style.maxHeight = ""
|
||||
})
|
||||
)
|
||||
.subscribe() // merge shit and return it...
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/* Open details after anchor jump */
|
||||
const hash$ = watchLocationHash()
|
||||
hash$
|
||||
.pipe(
|
||||
withElement(), // TODO: somehow ugly... not so nice and cool
|
||||
tap(el => {
|
||||
let parent = el.parentNode
|
||||
while (parent && !(parent instanceof HTMLDetailsElement)) // TODO: put this into a FUNCTION!
|
||||
parent = parent.parentNode
|
||||
|
||||
/* If there's a details tag, open it */
|
||||
if (parent && !parent.open) {
|
||||
parent.open = true
|
||||
|
||||
/* Hack: force reload for repositioning */ // TODO. what happens here!?
|
||||
const hash = location.hash
|
||||
location.hash = " "
|
||||
location.hash = hash // tslint:disable-line
|
||||
// TODO: setLocationHash() + forceLocationHashChange
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// setupAnchorToggle?
|
||||
const drawerToggle = getElement<HTMLInputElement>("[data-md-toggle=drawer]")!
|
||||
const searchToggle = getElement<HTMLInputElement>("[data-md-toggle=search]")!
|
||||
|
||||
/* Listener: close drawer when anchor links are clicked */
|
||||
hash$
|
||||
.pipe(
|
||||
tap(() => setToggle(drawerToggle, false))
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
/* Listener: open search on focus */
|
||||
const query = getElement("[data-md-component=query]")!
|
||||
if (query) {
|
||||
fromEvent(query, "focus")
|
||||
/* Create header observable */
|
||||
const header$ = components$
|
||||
.pipe(
|
||||
tap(() => setToggle(searchToggle, true))
|
||||
switchComponent("header"),
|
||||
switchMap(watchHeader)
|
||||
)
|
||||
|
||||
/* Create main area observable */
|
||||
const main$ = components$
|
||||
.pipe(
|
||||
switchComponent("main"),
|
||||
switchMap(el => watchMain(el, { size$, offset$, header$ })),
|
||||
shareReplay(1)
|
||||
)
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
/* Create sidebar with navigation */
|
||||
screen$
|
||||
.pipe(
|
||||
switchMapIfActive(() => components$ // TODO: write an observable creation function...
|
||||
.pipe(
|
||||
switchComponent("navigation"),
|
||||
switchMap(el => setupSidebar(el, { offset$, main$ }))
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/* Listener: focus input after opening search */
|
||||
fromEvent(searchToggle, "change")
|
||||
.pipe(
|
||||
filter(() => searchToggle.checked),
|
||||
delay(400),
|
||||
tap(() => query.focus())
|
||||
)
|
||||
.subscribe()
|
||||
/* Create sidebar with table of contents (missing on 404 page) */
|
||||
tablet$
|
||||
.pipe(
|
||||
switchMapIfActive(() => components$
|
||||
.pipe(
|
||||
switchComponent("toc"),
|
||||
switchMap(el => setupSidebar(el, { offset$, main$ }))
|
||||
)
|
||||
)
|
||||
)
|
||||
.subscribe(console.log)
|
||||
|
||||
// data-md-toggle!
|
||||
function setToggle(toggle: HTMLInputElement, active: boolean): void {
|
||||
if (toggle.checked !== active) {
|
||||
toggle.checked = active
|
||||
toggle.dispatchEvent(new CustomEvent("change"))
|
||||
/* Return all observables */
|
||||
return {
|
||||
ui: {
|
||||
document: { load$, switch$ },
|
||||
location: { url$, fragment$ },
|
||||
media: { screen$, tablet$ },
|
||||
viewport: { offset$, size$ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Asynchronously load a document
|
||||
function load(url: string) {
|
||||
|
||||
const options: AjaxRequest = {
|
||||
responseType: "document",
|
||||
withCredentials: true
|
||||
} // TODO: remove favicon from source!? patch...
|
||||
|
||||
return ajax({ url, ...options })
|
||||
.pipe(
|
||||
map(({ response }) => {
|
||||
if (!(response instanceof Document)) // TODO: what to do in case of error?
|
||||
throw Error("Unknown error...")
|
||||
|
||||
return response
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// function isLocal(el: HTMLAnchorElement): boolean {
|
||||
// return /(:\/\/|^#[^\/]+$)/.test(el.getAttribute("href")!)
|
||||
// }
|
||||
|
||||
export function app(config: any) {
|
||||
console.log("called app with", config)
|
||||
}
|
||||
|
@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2019 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Check if browser supports the `<details>` tag
|
||||
*
|
||||
* As this polyfill is executed at the end of `<body>`, not all checks from the
|
||||
* original source were necessary, so the script was stripped down a little.
|
||||
*
|
||||
* @see https://bit.ly/2O1teyP - Original source
|
||||
*
|
||||
* @return Test result
|
||||
*/
|
||||
function isSupported(): boolean {
|
||||
const details = document.createElement("details")
|
||||
if (!("open" in details))
|
||||
return false
|
||||
|
||||
/* Insert summary and append element */
|
||||
details.innerHTML = "<summary>_</summary>_"
|
||||
details.style.display = "block"
|
||||
document.body.appendChild(details)
|
||||
|
||||
/* Measure height difference */
|
||||
const h0 = details.offsetHeight
|
||||
details.open = true
|
||||
const h1 = details.offsetHeight
|
||||
|
||||
/* Remove element and return test result */
|
||||
document.body.removeChild(details)
|
||||
return h1 - h0 !== 0
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Polyfill
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Execute polyfill when DOM is available */
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (isSupported())
|
||||
return
|
||||
|
||||
/* Indicate presence of details polyfill */
|
||||
document.documentElement.classList.add("no-details")
|
||||
|
||||
/* Retrieve all summaries and polyfill open/close functionality */
|
||||
const summaries = document.querySelectorAll("details > summary")
|
||||
summaries.forEach(summary => {
|
||||
summary.addEventListener("click", () => {
|
||||
const details = summary.parentNode as HTMLElement
|
||||
if (details.hasAttribute("open")) {
|
||||
details.removeAttribute("open")
|
||||
} else {
|
||||
details.setAttribute("open", "")
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2019 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* 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 "./custom-event"
|
||||
import "./details"
|
@ -21,17 +21,10 @@
|
||||
*/
|
||||
|
||||
import { keys } from "ramda"
|
||||
import {
|
||||
MonoTypeOperatorFunction,
|
||||
NEVER,
|
||||
Observable,
|
||||
OperatorFunction,
|
||||
of,
|
||||
pipe
|
||||
} from "rxjs"
|
||||
import { scan, shareReplay, switchMap } from "rxjs/operators"
|
||||
import { NEVER, Observable, OperatorFunction, merge, of, pipe } from "rxjs"
|
||||
import { map, scan, shareReplay, switchMap } from "rxjs/operators"
|
||||
|
||||
import { getElement } from "../../ui"
|
||||
import { getElement } from "../../utilities"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@ -61,92 +54,82 @@ export type ComponentMap = {
|
||||
[P in Component]?: HTMLElement
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Function types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
load$: Observable<Document> /* Document observable */
|
||||
switch$: Observable<Document> /* Document switch observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve the component to element mapping
|
||||
* Watch component map
|
||||
*
|
||||
* The document must be passed as a parameter to support retrieving elements
|
||||
* from the document object returned through asynchronous loading.
|
||||
* This function returns an observable that will maintain bindings to the given
|
||||
* components in-between document switches and update the document in-place.
|
||||
*
|
||||
* @param document - Document of reference
|
||||
* @param names - Components
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Component map observable
|
||||
*/
|
||||
export function watchComponentMap(
|
||||
document: Document
|
||||
names: Component[], { load$, switch$ }: Options
|
||||
): Observable<ComponentMap> {
|
||||
const components$ = merge(load$, switch$)
|
||||
.pipe(
|
||||
|
||||
/* Build component map */
|
||||
const map$ = of([
|
||||
"header", /* Header */
|
||||
"title", /* Header title */
|
||||
"search", /* Search */
|
||||
"query", /* Search input */
|
||||
"reset", /* Search reset */
|
||||
"result", /* Search results */
|
||||
"container", /* Container */
|
||||
"main", /* Main area */
|
||||
"hero", /* Hero */
|
||||
"tabs", /* Tabs */
|
||||
"navigation", /* Navigation */
|
||||
"toc" /* Table of contents */
|
||||
].reduce<ComponentMap>((map, name) => {
|
||||
const el = getElement(`[data-md-component=${name}]`, document)
|
||||
return {
|
||||
...map,
|
||||
...typeof el !== "undefined" ? { [name]: el } : {}
|
||||
}
|
||||
}, {}))
|
||||
/* Build component map */
|
||||
map(document => names.reduce<ComponentMap>((components, name) => {
|
||||
const el = getElement(`[data-md-component=${name}]`, document)
|
||||
return {
|
||||
...components,
|
||||
...typeof el !== "undefined" ? { [name]: el } : {}
|
||||
}
|
||||
}, {})),
|
||||
|
||||
/* Re-compute component map on document switch */
|
||||
scan((prev, next) => {
|
||||
for (const name of keys(prev)) {
|
||||
switch (name) {
|
||||
|
||||
/* Top-level components: update */
|
||||
case "title":
|
||||
case "container":
|
||||
if (name in prev && typeof prev[name] !== "undefined") {
|
||||
prev[name]!.replaceWith(next[name]!)
|
||||
prev[name] = next[name]
|
||||
}
|
||||
break
|
||||
|
||||
/* All other components: rebind */
|
||||
default:
|
||||
prev[name] = getElement(`[data-md-component=${name}]`)
|
||||
}
|
||||
}
|
||||
return prev
|
||||
})
|
||||
)
|
||||
|
||||
/* Return component map as hot observable */
|
||||
return map$
|
||||
return components$
|
||||
.pipe(
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Paint component map from source observable
|
||||
*
|
||||
* This operator function will swap the components in the previous component
|
||||
* map with the new components identified by the given names and rebind all
|
||||
* remaining components, as they may be children of swapped components.
|
||||
*
|
||||
* @param names - Components to paint
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function paintComponentMap(
|
||||
names: Component[] = ["title", "container"]
|
||||
): MonoTypeOperatorFunction<ComponentMap> {
|
||||
return pipe(
|
||||
scan<ComponentMap>((prev, next) => {
|
||||
for (const name of keys(prev)) {
|
||||
|
||||
/* Swap component */
|
||||
if (names.includes(name)) {
|
||||
if (name in prev && typeof prev[name] !== "undefined") {
|
||||
prev[name]!.replaceWith(next[name]!)
|
||||
prev[name] = next[name]
|
||||
}
|
||||
|
||||
/* Bind component */
|
||||
} else {
|
||||
prev[name] = getElement(`[data-md-component=${name}]`)
|
||||
}
|
||||
}
|
||||
return prev
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pluck a component from the component map
|
||||
* Switch to component
|
||||
*
|
||||
* @template T - Element type
|
||||
*
|
||||
@ -154,13 +137,13 @@ export function paintComponentMap(
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function pluckComponent(
|
||||
export function switchComponent<T extends HTMLElement>(
|
||||
name: Component
|
||||
): OperatorFunction<ComponentMap, HTMLElement> {
|
||||
): OperatorFunction<ComponentMap, T> {
|
||||
return pipe(
|
||||
switchMap(map => {
|
||||
return typeof map[name] !== "undefined"
|
||||
? of(map[name]!)
|
||||
switchMap(components => {
|
||||
return typeof components[name] !== "undefined"
|
||||
? of(components[name] as T)
|
||||
: NEVER
|
||||
})
|
||||
)
|
@ -20,19 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { MonoTypeOperatorFunction, Observable, of, pipe } from "rxjs"
|
||||
import {
|
||||
distinctUntilKeyChanged,
|
||||
finalize,
|
||||
shareReplay,
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { Main } from "../../main"
|
||||
import {
|
||||
resetHeaderShadow,
|
||||
setHeaderShadow
|
||||
} from "../element"
|
||||
import { Observable, defer, of } from "rxjs"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@ -51,7 +39,7 @@ export interface Header {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create an observable to watch the header
|
||||
* Watch header
|
||||
*
|
||||
* The header is wrapped in an observable to pave the way for auto-hiding or
|
||||
* other dynamic behaviors that may be implemented later on.
|
||||
@ -63,38 +51,14 @@ export interface Header {
|
||||
export function watchHeader(
|
||||
el: HTMLElement
|
||||
): Observable<Header> {
|
||||
const sticky = getComputedStyle(el)
|
||||
.getPropertyValue("position") === "fixed"
|
||||
return defer(() => {
|
||||
const sticky = getComputedStyle(el)
|
||||
.getPropertyValue("position") === "fixed"
|
||||
|
||||
/* Return header as hot observable */
|
||||
return of({
|
||||
sticky,
|
||||
height: sticky ? el.offsetHeight : 0
|
||||
})
|
||||
.pipe(
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Paint header shadow from source observable
|
||||
*
|
||||
* @param el - Header element
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function paintHeaderShadow(
|
||||
el: HTMLElement
|
||||
): MonoTypeOperatorFunction<Main> {
|
||||
return pipe(
|
||||
distinctUntilKeyChanged("active"),
|
||||
tap(({ active }) => {
|
||||
setHeaderShadow(el, active)
|
||||
}),
|
||||
finalize(() => {
|
||||
resetHeaderShadow(el)
|
||||
/* Return header as hot observable */
|
||||
return of({
|
||||
sticky,
|
||||
height: sticky ? el.offsetHeight : 0
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
@ -21,4 +21,3 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./element"
|
@ -21,8 +21,6 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./anchor"
|
||||
export * from "./header"
|
||||
export * from "./main"
|
||||
export * from "./navigation"
|
||||
export * from "./sidebar"
|
@ -28,7 +28,7 @@ import {
|
||||
shareReplay
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { ViewportOffset, ViewportSize } from "../../ui"
|
||||
import { ViewportOffset, ViewportSize } from "../../../ui"
|
||||
import { Header } from "../header"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -49,9 +49,9 @@ export interface Main {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch options
|
||||
* Options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
interface Options {
|
||||
size$: Observable<ViewportSize> /* Viewport size observable */
|
||||
offset$: Observable<ViewportOffset> /* Viewport offset observable */
|
||||
header$: Observable<Header> /* Header observable */
|
||||
@ -62,7 +62,12 @@ interface WatchOptions {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create an observable to watch the main area
|
||||
* Watch main area
|
||||
*
|
||||
* This function returns an observable that computes the visual parameters of
|
||||
* the main area from the viewport height and vertical offset, as well as the
|
||||
* height of the header element. The height of the main area is corrected by
|
||||
* the height of the header (if fixed) and footer element.
|
||||
*
|
||||
* @param el - Main area element
|
||||
* @param options - Options
|
||||
@ -70,7 +75,7 @@ interface WatchOptions {
|
||||
* @return Main area observable
|
||||
*/
|
||||
export function watchMain(
|
||||
el: HTMLElement, { size$, offset$, header$ }: WatchOptions
|
||||
el: HTMLElement, { size$, offset$, header$ }: Options
|
||||
): Observable<Main> {
|
||||
|
||||
/* Compute necessary adjustment for header */
|
||||
@ -80,7 +85,7 @@ export function watchMain(
|
||||
)
|
||||
|
||||
/* Compute the main area's visible height */
|
||||
const height$ = combineLatest(offset$, size$, adjust$)
|
||||
const height$ = combineLatest([offset$, size$, adjust$])
|
||||
.pipe(
|
||||
map(([{ y }, { height }, adjust]) => {
|
||||
const top = el.offsetTop
|
||||
@ -93,20 +98,20 @@ export function watchMain(
|
||||
)
|
||||
|
||||
/* Compute whether the viewport offset is past the main area's top */
|
||||
const active$ = combineLatest(offset$, adjust$)
|
||||
const active$ = combineLatest([offset$, adjust$])
|
||||
.pipe(
|
||||
map(([{ y }, adjust]) => y >= el.offsetTop - adjust),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return combineLatest(height$, adjust$, active$)
|
||||
return combineLatest([height$, adjust$, active$])
|
||||
.pipe(
|
||||
map(([height, adjust, active]) => ({
|
||||
offset: el.offsetTop - adjust,
|
||||
height,
|
||||
active
|
||||
})),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
@ -21,13 +21,7 @@
|
||||
*/
|
||||
|
||||
import { equals } from "ramda"
|
||||
import {
|
||||
MonoTypeOperatorFunction,
|
||||
Observable,
|
||||
animationFrameScheduler,
|
||||
combineLatest,
|
||||
pipe
|
||||
} from "rxjs"
|
||||
import { Observable, animationFrameScheduler, combineLatest } from "rxjs"
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
finalize,
|
||||
@ -37,7 +31,7 @@ import {
|
||||
tap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { ViewportOffset } from "../../../ui"
|
||||
import { ViewportOffset } from "../../../../ui"
|
||||
import { Main } from "../../main"
|
||||
import {
|
||||
resetSidebarHeight,
|
||||
@ -63,9 +57,9 @@ export interface Sidebar {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch options
|
||||
* Options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
interface Options {
|
||||
offset$: Observable<ViewportOffset> /* Viewport offset observable */
|
||||
main$: Observable<Main> /* Main area observable */
|
||||
}
|
||||
@ -75,7 +69,12 @@ interface WatchOptions {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create an observable to watch a sidebar
|
||||
* Watch sidebar
|
||||
*
|
||||
* This function returns an observable that computes the visual parameters of
|
||||
* the given element (a sidebar) from the vertical viewport offset, as well as
|
||||
* the height of the main area. When the page is scrolled beyond the header,
|
||||
* the sidebar is locked and fills the remaining space.
|
||||
*
|
||||
* @param el - Sidebar element
|
||||
* @param options - Options
|
||||
@ -83,7 +82,7 @@ interface WatchOptions {
|
||||
* @return Sidebar observable
|
||||
*/
|
||||
export function watchSidebar(
|
||||
el: HTMLElement, { offset$, main$ }: WatchOptions
|
||||
el: HTMLElement, { offset$, main$ }: Options
|
||||
): Observable<Sidebar> {
|
||||
|
||||
/* Adjust for internal main area offset */
|
||||
@ -93,7 +92,7 @@ export function watchSidebar(
|
||||
)
|
||||
|
||||
/* Compute the sidebar's available height */
|
||||
const height$ = combineLatest(offset$, main$)
|
||||
const height$ = combineLatest([offset$, main$])
|
||||
.pipe(
|
||||
map(([{ y }, { offset, height }]) => {
|
||||
return height - adjust + Math.min(adjust, Math.max(0, y - offset))
|
||||
@ -101,45 +100,45 @@ export function watchSidebar(
|
||||
)
|
||||
|
||||
/* Compute whether the sidebar should be locked */
|
||||
const lock$ = combineLatest(offset$, main$)
|
||||
const lock$ = combineLatest([offset$, main$])
|
||||
.pipe(
|
||||
map(([{ y }, { offset }]) => y >= offset + adjust)
|
||||
)
|
||||
|
||||
/* Combine into single hot observable */
|
||||
return combineLatest(height$, lock$)
|
||||
return combineLatest([height$, lock$])
|
||||
.pipe(
|
||||
map(([height, lock]) => ({ height, lock })),
|
||||
distinctUntilChanged<Sidebar>(equals),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Paint sidebar from source observable
|
||||
* Setup sidebar
|
||||
*
|
||||
* @param el - Sidebar element
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Operator function
|
||||
* @return Sidebar observable
|
||||
*/
|
||||
export function paintSidebar(
|
||||
el: HTMLElement
|
||||
): MonoTypeOperatorFunction<Sidebar> {
|
||||
return pipe(
|
||||
export function setupSidebar(
|
||||
el: HTMLElement, options: Options
|
||||
): Observable<Sidebar> {
|
||||
return watchSidebar(el, options)
|
||||
.pipe(
|
||||
observeOn(animationFrameScheduler),
|
||||
|
||||
/* Defer repaint to next animation frame */
|
||||
observeOn(animationFrameScheduler),
|
||||
tap(({ height, lock }) => {
|
||||
setSidebarHeight(el, height)
|
||||
setSidebarLock(el, lock)
|
||||
}),
|
||||
/* Apply mutations (side effects) */
|
||||
tap(({ height, lock }) => {
|
||||
setSidebarHeight(el, height)
|
||||
setSidebarLock(el, lock)
|
||||
}),
|
||||
|
||||
/* Reset on complete or error */
|
||||
finalize(() => {
|
||||
resetSidebarHeight(el)
|
||||
resetSidebarLock(el)
|
||||
})
|
||||
)
|
||||
/* Reset on complete or error */
|
||||
finalize(() => {
|
||||
resetSidebarHeight(el)
|
||||
resetSidebarLock(el)
|
||||
})
|
||||
)
|
||||
}
|
@ -20,4 +20,6 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import "custom-event-polyfill"
|
||||
export * from "./component"
|
||||
export * from "./utilities"
|
||||
// export * from "./worker"
|
@ -20,11 +20,6 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { OperatorFunction, pipe } from "rxjs"
|
||||
import { filter, map } from "rxjs/operators"
|
||||
|
||||
import { toArray } from "../../utilities"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@ -58,44 +53,5 @@ export function getElement<T extends HTMLElement>(
|
||||
export function getElements<T extends HTMLElement>(
|
||||
selector: string, node: ParentNode = document
|
||||
): T[] {
|
||||
return toArray(node.querySelectorAll<T>(selector))
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Operators
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve an element matching the query selector
|
||||
*
|
||||
* @template T - Element type
|
||||
*
|
||||
* @param node - Node of reference
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function withElement<T extends HTMLElement>(
|
||||
node: ParentNode = document
|
||||
): OperatorFunction<string, T> {
|
||||
return pipe(
|
||||
map(selector => getElement<T>(selector, node)!),
|
||||
filter<T>(Boolean)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all elements matching the query selector
|
||||
*
|
||||
* @template T - Element type
|
||||
*
|
||||
* @param node - Node of reference
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function withElements<T extends HTMLElement>(
|
||||
node: ParentNode = document
|
||||
): OperatorFunction<string, T[]> {
|
||||
return pipe(
|
||||
map(selector => getElements<T>(selector, node))
|
||||
)
|
||||
return Array.from(node.querySelectorAll<T>(selector))
|
||||
}
|
@ -20,5 +20,5 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./element"
|
||||
export * from "./operator"
|
@ -20,7 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { NEVER, Observable, OperatorFunction, pipe } from "rxjs"
|
||||
import { EMPTY, Observable, OperatorFunction, pipe } from "rxjs"
|
||||
import { switchMap } from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -28,37 +28,35 @@ import { switchMap } from "rxjs/operators"
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Convert a collection to an array
|
||||
*
|
||||
* @template T - Element type
|
||||
*
|
||||
* @param collection - Collection or node list
|
||||
*
|
||||
* @return Array of elements
|
||||
*/
|
||||
export function toArray<
|
||||
T extends HTMLElement
|
||||
>(collection: HTMLCollection | NodeListOf<T>): T[] {
|
||||
return Array.from(collection) as T[]
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Operators
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Switch to another observable, if toggle is active
|
||||
* Switch to another observable if source observable emits `true`
|
||||
*
|
||||
* @template T - Observable value type
|
||||
*
|
||||
* @param project - Project function
|
||||
*
|
||||
* @return Observable, if toggle is active
|
||||
* @return Operator function
|
||||
*/
|
||||
export function toggle<T>(
|
||||
project: () => Observable<T>
|
||||
export function switchMapIfActive<T>(
|
||||
project: (value: boolean) => Observable<T>
|
||||
): OperatorFunction<boolean, T> {
|
||||
return pipe(
|
||||
switchMap(active => active ? project() : NEVER)
|
||||
switchMap(value => value ? project(value) : EMPTY)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to another observable if source observable emits `false`
|
||||
*
|
||||
* @template T - Observable value type
|
||||
*
|
||||
* @param project - Project function
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function switchMapIfNotActive<T>(
|
||||
project: (value: boolean) => Observable<T>
|
||||
): OperatorFunction<boolean, T> {
|
||||
return pipe(
|
||||
switchMap(value => value ? EMPTY : project(value))
|
||||
)
|
||||
}
|
@ -20,64 +20,86 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable, of } from "rxjs"
|
||||
import { shareReplay } from "rxjs/operators"
|
||||
|
||||
import { getElement, getElements } from "../../ui"
|
||||
import { Observable, fromEvent } from "rxjs"
|
||||
import { ajax } from "rxjs/ajax"
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
mapTo,
|
||||
pluck,
|
||||
shareReplay,
|
||||
skip,
|
||||
startWith,
|
||||
switchMap
|
||||
} from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* Function types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Navigation index
|
||||
* Switch options
|
||||
*/
|
||||
export type NavigationIndex = Map<HTMLInputElement, HTMLElement>
|
||||
interface SwitchOptions {
|
||||
url$: Observable<string> /* Location observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Data
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Observable for document load events
|
||||
*/
|
||||
const load$ = fromEvent(document, "DOMContentLoaded")
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Set navigation overflow scrolling
|
||||
* Watch document
|
||||
*
|
||||
* @param el - Navigation element
|
||||
* @param active - Whether overflow scrolling is active
|
||||
* @return Document observable
|
||||
*/
|
||||
export function setNavigationOverflowScrolling(
|
||||
el: HTMLElement, active: boolean
|
||||
): void {
|
||||
el.style.background = active ? "yellow" : "" // TODO: hack, temporary
|
||||
el.style.webkitOverflowScrolling = active ? "touch" : ""
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create an observable to index all navigation elements
|
||||
*
|
||||
* @param el - Top-level navigation element
|
||||
*
|
||||
* @return Navigation index observable
|
||||
*/
|
||||
export function watchNavigationIndex(
|
||||
el: HTMLElement
|
||||
): Observable<NavigationIndex> {
|
||||
const list = getElements("nav", el)
|
||||
|
||||
/* Build index to map inputs to navigation lists */
|
||||
const index = new Map<HTMLInputElement, HTMLElement>()
|
||||
for (const item of list) {
|
||||
const label = getElement<HTMLLabelElement>("label", item)!
|
||||
if (typeof label !== "undefined") {
|
||||
const input = getElement<HTMLInputElement>(`#${label.htmlFor}`)!
|
||||
index.set(input, item)
|
||||
}
|
||||
}
|
||||
|
||||
/* Return navigation index as hot observable */
|
||||
return of(index)
|
||||
export function watchDocument(): Observable<Document> {
|
||||
return load$
|
||||
.pipe(
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
mapTo(document),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch document switch
|
||||
*
|
||||
* This function returns an observables that fetches a document if the provided
|
||||
* location observable emits a new value (i.e. URL). If the emitted URL points
|
||||
* to the same page, the request is effectively ignored (e.g. when only the
|
||||
* fragment identifier changes)
|
||||
*
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Document switch observable
|
||||
*/
|
||||
export function watchDocumentSwitch(
|
||||
{ url$ }: SwitchOptions
|
||||
): Observable<Document> {
|
||||
return url$
|
||||
.pipe(
|
||||
startWith(location.href),
|
||||
map(url => url.replace(/#[^#]+$/, "")),
|
||||
distinctUntilChanged(),
|
||||
skip(1),
|
||||
switchMap(url => ajax({
|
||||
url,
|
||||
responseType: "document",
|
||||
withCredentials: true
|
||||
})
|
||||
.pipe<Document>(
|
||||
pluck("response")
|
||||
)
|
||||
),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
@ -20,7 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./element"
|
||||
export * from "./document"
|
||||
export * from "./location"
|
||||
export * from "./media"
|
||||
export * from "./viewport"
|
||||
|
@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
import { Observable, Subject, fromEvent } from "rxjs"
|
||||
import { filter, map, share, startWith } from "rxjs/operators"
|
||||
import { filter, map, share } from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Data
|
||||
@ -42,7 +42,7 @@ const popstate$ = fromEvent<PopStateEvent>(window, "popstate")
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a subject to watch or alter the location
|
||||
* Watch location
|
||||
*
|
||||
* @return Location subject
|
||||
*/
|
||||
@ -50,7 +50,8 @@ export function watchLocation(): Subject<string> {
|
||||
const location$ = new Subject<string>()
|
||||
popstate$
|
||||
.pipe(
|
||||
map(() => location.href)
|
||||
map(() => location.href),
|
||||
share()
|
||||
)
|
||||
.subscribe(location$)
|
||||
|
||||
@ -59,15 +60,14 @@ export function watchLocation(): Subject<string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an observable to watch the location hash
|
||||
* Watch location fragment
|
||||
*
|
||||
* @return Location hash observable
|
||||
* @return Location fragment observable
|
||||
*/
|
||||
export function watchLocationHash(): Observable<string> {
|
||||
export function watchLocationFragment(): Observable<string> {
|
||||
return hashchange$
|
||||
.pipe(
|
||||
map(() => location.hash),
|
||||
startWith(location.hash),
|
||||
filter(hash => hash.length > 0),
|
||||
share()
|
||||
)
|
||||
|
@ -28,19 +28,19 @@ import { shareReplay, startWith } from "rxjs/operators"
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create an observable to watch a media query
|
||||
* Watch media query
|
||||
*
|
||||
* @param query - Media query
|
||||
*
|
||||
* @return Media observable
|
||||
*/
|
||||
export function watchMedia(query: string): Observable<boolean> {
|
||||
const media = window.matchMedia(query)
|
||||
const media = matchMedia(query)
|
||||
return fromEventPattern<boolean>(next =>
|
||||
media.addListener(() => next(media.matches))
|
||||
)
|
||||
.pipe(
|
||||
startWith(media.matches),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
@ -62,33 +62,33 @@ export interface ViewportSize {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Retrieve the viewport offset
|
||||
* Retrieve viewport offset
|
||||
*
|
||||
* @return Viewport offset
|
||||
*/
|
||||
export function getViewportOffset(): ViewportOffset {
|
||||
return {
|
||||
x: window.pageXOffset,
|
||||
y: window.pageYOffset
|
||||
x: pageXOffset,
|
||||
y: pageYOffset
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the viewport size
|
||||
* Retrieve viewport size
|
||||
*
|
||||
* @return Viewport size
|
||||
*/
|
||||
export function getViewportSize(): ViewportSize {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
width: innerWidth,
|
||||
height: innerHeight
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create an observable to watch the viewport offset
|
||||
* Watch viewport offset
|
||||
*
|
||||
* @return Viewport offset observable
|
||||
*/
|
||||
@ -97,12 +97,12 @@ export function watchViewportOffset(): Observable<ViewportOffset> {
|
||||
.pipe(
|
||||
map(getViewportOffset),
|
||||
startWith(getViewportOffset()),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an observable to watch the viewport size
|
||||
* Watch viewport size
|
||||
*
|
||||
* @return Viewport size observable
|
||||
*/
|
||||
@ -111,6 +111,6 @@ export function watchViewportSize(): Observable<ViewportSize> {
|
||||
.pipe(
|
||||
map(getViewportSize),
|
||||
startWith(getViewportSize()),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export * from "./other"
|
Loading…
Reference in New Issue
Block a user