mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-06-14 11:52:32 +03:00
Refactored entrypoint and observable setup
This commit is contained in:
@@ -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.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
* Types
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration
|
||||||
|
*/
|
||||||
|
export interface Config {
|
||||||
|
base: string /* Base URL */
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set header shadow
|
* Ensure that the given value is a valid configuration
|
||||||
*
|
*
|
||||||
* @param el - Header element
|
* @param config - Configuration
|
||||||
* @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 el - Header element
|
* @return Test result
|
||||||
*/
|
*/
|
||||||
export function resetHeaderShadow(
|
export function isConfig(config: any): config is Config {
|
||||||
el: HTMLElement
|
return typeof config === "object"
|
||||||
): void {
|
&& typeof config.base === "string"
|
||||||
el.removeAttribute("data-md-state")
|
|
||||||
}
|
}
|
||||||
@@ -20,412 +20,128 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { findLast } from "ramda"
|
import { shareReplay, switchMap } from "rxjs/operators"
|
||||||
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 { isConfig } from "./config"
|
||||||
import {
|
import {
|
||||||
paintAnchorList,
|
setupSidebar,
|
||||||
paintComponentMap,
|
switchComponent,
|
||||||
paintHeaderShadow,
|
switchMapIfActive,
|
||||||
paintSidebar,
|
|
||||||
pluckComponent,
|
|
||||||
setNavigationOverflowScrolling,
|
|
||||||
watchAnchorList,
|
|
||||||
watchComponentMap,
|
watchComponentMap,
|
||||||
watchHeader,
|
watchHeader,
|
||||||
watchMain,
|
watchMain
|
||||||
watchNavigationIndex,
|
} from "./theme"
|
||||||
watchSidebar
|
|
||||||
} from "./component"
|
|
||||||
import {
|
import {
|
||||||
getElement,
|
watchDocument,
|
||||||
getElements,
|
watchDocumentSwitch,
|
||||||
watchLocation,
|
watchLocation,
|
||||||
watchLocationHash,
|
watchLocationFragment,
|
||||||
watchMedia,
|
watchMedia,
|
||||||
watchViewportOffset,
|
watchViewportOffset,
|
||||||
watchViewportSize,
|
watchViewportSize
|
||||||
withElement
|
|
||||||
} from "./ui"
|
} from "./ui"
|
||||||
import { toggle } from "./utilities"
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// TBD
|
||||||
// Disclaimer: this file is currently heavy WIP
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const offset$ = watchViewportOffset()
|
const names = [
|
||||||
const size$ = watchViewportSize()
|
"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
|
/* Create viewport observables */
|
||||||
document.documentElement.classList.remove("no-js")
|
const offset$ = watchViewportOffset()
|
||||||
document.documentElement.classList.add("js")
|
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
|
/* Create location observables */
|
||||||
const init$ = fromEvent(document, "DOMContentLoaded")
|
const url$ = watchLocation()
|
||||||
.pipe(
|
const fragment$ = watchLocationFragment()
|
||||||
mapTo(document),
|
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
|
||||||
)
|
|
||||||
|
|
||||||
// Location subject
|
/* Create document observables */
|
||||||
const location$ = watchLocation()
|
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
|
/* Create component map observable */
|
||||||
const components$ = merge(init$, reload$)
|
const components$ = watchComponentMap(names, { load$, switch$ })
|
||||||
.pipe(
|
|
||||||
switchMap(watchComponentMap),
|
|
||||||
paintComponentMap(),
|
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
|
||||||
)
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
/* Create header observable */
|
||||||
|
const header$ = components$
|
||||||
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")
|
|
||||||
.pipe(
|
.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()
|
.subscribe()
|
||||||
}
|
|
||||||
|
|
||||||
/* Listener: focus input after opening search */
|
/* Create sidebar with table of contents (missing on 404 page) */
|
||||||
fromEvent(searchToggle, "change")
|
tablet$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(() => searchToggle.checked),
|
switchMapIfActive(() => components$
|
||||||
delay(400),
|
.pipe(
|
||||||
tap(() => query.focus())
|
switchComponent("toc"),
|
||||||
)
|
switchMap(el => setupSidebar(el, { offset$, main$ }))
|
||||||
.subscribe()
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(console.log)
|
||||||
|
|
||||||
// data-md-toggle!
|
/* Return all observables */
|
||||||
function setToggle(toggle: HTMLInputElement, active: boolean): void {
|
return {
|
||||||
if (toggle.checked !== active) {
|
ui: {
|
||||||
toggle.checked = active
|
document: { load$, switch$ },
|
||||||
toggle.dispatchEvent(new CustomEvent("change"))
|
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 { keys } from "ramda"
|
||||||
import {
|
import { NEVER, Observable, OperatorFunction, merge, of, pipe } from "rxjs"
|
||||||
MonoTypeOperatorFunction,
|
import { map, scan, shareReplay, switchMap } from "rxjs/operators"
|
||||||
NEVER,
|
|
||||||
Observable,
|
|
||||||
OperatorFunction,
|
|
||||||
of,
|
|
||||||
pipe
|
|
||||||
} from "rxjs"
|
|
||||||
import { scan, shareReplay, switchMap } from "rxjs/operators"
|
|
||||||
|
|
||||||
import { getElement } from "../../ui"
|
import { getElement } from "../../utilities"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Types
|
* Types
|
||||||
@@ -61,92 +54,82 @@ export type ComponentMap = {
|
|||||||
[P in Component]?: HTMLElement
|
[P in Component]?: HTMLElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------------------
|
||||||
|
* Function types
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options
|
||||||
|
*/
|
||||||
|
interface Options {
|
||||||
|
load$: Observable<Document> /* Document observable */
|
||||||
|
switch$: Observable<Document> /* Document switch observable */
|
||||||
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the component to element mapping
|
* Watch component map
|
||||||
*
|
*
|
||||||
* The document must be passed as a parameter to support retrieving elements
|
* This function returns an observable that will maintain bindings to the given
|
||||||
* from the document object returned through asynchronous loading.
|
* 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
|
* @return Component map observable
|
||||||
*/
|
*/
|
||||||
export function watchComponentMap(
|
export function watchComponentMap(
|
||||||
document: Document
|
names: Component[], { load$, switch$ }: Options
|
||||||
): Observable<ComponentMap> {
|
): Observable<ComponentMap> {
|
||||||
|
const components$ = merge(load$, switch$)
|
||||||
|
.pipe(
|
||||||
|
|
||||||
/* Build component map */
|
/* Build component map */
|
||||||
const map$ = of([
|
map(document => names.reduce<ComponentMap>((components, name) => {
|
||||||
"header", /* Header */
|
const el = getElement(`[data-md-component=${name}]`, document)
|
||||||
"title", /* Header title */
|
return {
|
||||||
"search", /* Search */
|
...components,
|
||||||
"query", /* Search input */
|
...typeof el !== "undefined" ? { [name]: el } : {}
|
||||||
"reset", /* Search reset */
|
}
|
||||||
"result", /* Search results */
|
}, {})),
|
||||||
"container", /* Container */
|
|
||||||
"main", /* Main area */
|
/* Re-compute component map on document switch */
|
||||||
"hero", /* Hero */
|
scan((prev, next) => {
|
||||||
"tabs", /* Tabs */
|
for (const name of keys(prev)) {
|
||||||
"navigation", /* Navigation */
|
switch (name) {
|
||||||
"toc" /* Table of contents */
|
|
||||||
].reduce<ComponentMap>((map, name) => {
|
/* Top-level components: update */
|
||||||
const el = getElement(`[data-md-component=${name}]`, document)
|
case "title":
|
||||||
return {
|
case "container":
|
||||||
...map,
|
if (name in prev && typeof prev[name] !== "undefined") {
|
||||||
...typeof el !== "undefined" ? { [name]: el } : {}
|
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 component map as hot observable */
|
||||||
return map$
|
return components$
|
||||||
.pipe(
|
.pipe(
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paint component map from source observable
|
* Switch to component
|
||||||
*
|
|
||||||
* 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
|
|
||||||
*
|
*
|
||||||
* @template T - Element type
|
* @template T - Element type
|
||||||
*
|
*
|
||||||
@@ -154,13 +137,13 @@ export function paintComponentMap(
|
|||||||
*
|
*
|
||||||
* @return Operator function
|
* @return Operator function
|
||||||
*/
|
*/
|
||||||
export function pluckComponent(
|
export function switchComponent<T extends HTMLElement>(
|
||||||
name: Component
|
name: Component
|
||||||
): OperatorFunction<ComponentMap, HTMLElement> {
|
): OperatorFunction<ComponentMap, T> {
|
||||||
return pipe(
|
return pipe(
|
||||||
switchMap(map => {
|
switchMap(components => {
|
||||||
return typeof map[name] !== "undefined"
|
return typeof components[name] !== "undefined"
|
||||||
? of(map[name]!)
|
? of(components[name] as T)
|
||||||
: NEVER
|
: NEVER
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -20,19 +20,7 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MonoTypeOperatorFunction, Observable, of, pipe } from "rxjs"
|
import { Observable, defer, of } from "rxjs"
|
||||||
import {
|
|
||||||
distinctUntilKeyChanged,
|
|
||||||
finalize,
|
|
||||||
shareReplay,
|
|
||||||
tap
|
|
||||||
} from "rxjs/operators"
|
|
||||||
|
|
||||||
import { Main } from "../../main"
|
|
||||||
import {
|
|
||||||
resetHeaderShadow,
|
|
||||||
setHeaderShadow
|
|
||||||
} from "../element"
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Types
|
* 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
|
* The header is wrapped in an observable to pave the way for auto-hiding or
|
||||||
* other dynamic behaviors that may be implemented later on.
|
* other dynamic behaviors that may be implemented later on.
|
||||||
@@ -63,38 +51,14 @@ export interface Header {
|
|||||||
export function watchHeader(
|
export function watchHeader(
|
||||||
el: HTMLElement
|
el: HTMLElement
|
||||||
): Observable<Header> {
|
): Observable<Header> {
|
||||||
const sticky = getComputedStyle(el)
|
return defer(() => {
|
||||||
.getPropertyValue("position") === "fixed"
|
const sticky = getComputedStyle(el)
|
||||||
|
.getPropertyValue("position") === "fixed"
|
||||||
|
|
||||||
/* Return header as hot observable */
|
/* Return header as hot observable */
|
||||||
return of({
|
return of({
|
||||||
sticky,
|
sticky,
|
||||||
height: sticky ? el.offsetHeight : 0
|
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)
|
|
||||||
})
|
})
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
@@ -21,4 +21,3 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./_"
|
export * from "./_"
|
||||||
export * from "./element"
|
|
||||||
@@ -21,8 +21,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./_"
|
export * from "./_"
|
||||||
export * from "./anchor"
|
|
||||||
export * from "./header"
|
export * from "./header"
|
||||||
export * from "./main"
|
export * from "./main"
|
||||||
export * from "./navigation"
|
|
||||||
export * from "./sidebar"
|
export * from "./sidebar"
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
shareReplay
|
shareReplay
|
||||||
} from "rxjs/operators"
|
} from "rxjs/operators"
|
||||||
|
|
||||||
import { ViewportOffset, ViewportSize } from "../../ui"
|
import { ViewportOffset, ViewportSize } from "../../../ui"
|
||||||
import { Header } from "../header"
|
import { Header } from "../header"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
@@ -49,9 +49,9 @@ export interface Main {
|
|||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watch options
|
* Options
|
||||||
*/
|
*/
|
||||||
interface WatchOptions {
|
interface Options {
|
||||||
size$: Observable<ViewportSize> /* Viewport size observable */
|
size$: Observable<ViewportSize> /* Viewport size observable */
|
||||||
offset$: Observable<ViewportOffset> /* Viewport offset observable */
|
offset$: Observable<ViewportOffset> /* Viewport offset observable */
|
||||||
header$: Observable<Header> /* Header 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 el - Main area element
|
||||||
* @param options - Options
|
* @param options - Options
|
||||||
@@ -70,7 +75,7 @@ interface WatchOptions {
|
|||||||
* @return Main area observable
|
* @return Main area observable
|
||||||
*/
|
*/
|
||||||
export function watchMain(
|
export function watchMain(
|
||||||
el: HTMLElement, { size$, offset$, header$ }: WatchOptions
|
el: HTMLElement, { size$, offset$, header$ }: Options
|
||||||
): Observable<Main> {
|
): Observable<Main> {
|
||||||
|
|
||||||
/* Compute necessary adjustment for header */
|
/* Compute necessary adjustment for header */
|
||||||
@@ -80,7 +85,7 @@ export function watchMain(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/* Compute the main area's visible height */
|
/* Compute the main area's visible height */
|
||||||
const height$ = combineLatest(offset$, size$, adjust$)
|
const height$ = combineLatest([offset$, size$, adjust$])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([{ y }, { height }, adjust]) => {
|
map(([{ y }, { height }, adjust]) => {
|
||||||
const top = el.offsetTop
|
const top = el.offsetTop
|
||||||
@@ -93,20 +98,20 @@ export function watchMain(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/* Compute whether the viewport offset is past the main area's top */
|
/* Compute whether the viewport offset is past the main area's top */
|
||||||
const active$ = combineLatest(offset$, adjust$)
|
const active$ = combineLatest([offset$, adjust$])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([{ y }, adjust]) => y >= el.offsetTop - adjust),
|
map(([{ y }, adjust]) => y >= el.offsetTop - adjust),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Combine into a single hot observable */
|
/* Combine into a single hot observable */
|
||||||
return combineLatest(height$, adjust$, active$)
|
return combineLatest([height$, adjust$, active$])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([height, adjust, active]) => ({
|
map(([height, adjust, active]) => ({
|
||||||
offset: el.offsetTop - adjust,
|
offset: el.offsetTop - adjust,
|
||||||
height,
|
height,
|
||||||
active
|
active
|
||||||
})),
|
})),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -21,13 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { equals } from "ramda"
|
import { equals } from "ramda"
|
||||||
import {
|
import { Observable, animationFrameScheduler, combineLatest } from "rxjs"
|
||||||
MonoTypeOperatorFunction,
|
|
||||||
Observable,
|
|
||||||
animationFrameScheduler,
|
|
||||||
combineLatest,
|
|
||||||
pipe
|
|
||||||
} from "rxjs"
|
|
||||||
import {
|
import {
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
finalize,
|
finalize,
|
||||||
@@ -37,7 +31,7 @@ import {
|
|||||||
tap
|
tap
|
||||||
} from "rxjs/operators"
|
} from "rxjs/operators"
|
||||||
|
|
||||||
import { ViewportOffset } from "../../../ui"
|
import { ViewportOffset } from "../../../../ui"
|
||||||
import { Main } from "../../main"
|
import { Main } from "../../main"
|
||||||
import {
|
import {
|
||||||
resetSidebarHeight,
|
resetSidebarHeight,
|
||||||
@@ -63,9 +57,9 @@ export interface Sidebar {
|
|||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watch options
|
* Options
|
||||||
*/
|
*/
|
||||||
interface WatchOptions {
|
interface Options {
|
||||||
offset$: Observable<ViewportOffset> /* Viewport offset observable */
|
offset$: Observable<ViewportOffset> /* Viewport offset observable */
|
||||||
main$: Observable<Main> /* Main area 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 el - Sidebar element
|
||||||
* @param options - Options
|
* @param options - Options
|
||||||
@@ -83,7 +82,7 @@ interface WatchOptions {
|
|||||||
* @return Sidebar observable
|
* @return Sidebar observable
|
||||||
*/
|
*/
|
||||||
export function watchSidebar(
|
export function watchSidebar(
|
||||||
el: HTMLElement, { offset$, main$ }: WatchOptions
|
el: HTMLElement, { offset$, main$ }: Options
|
||||||
): Observable<Sidebar> {
|
): Observable<Sidebar> {
|
||||||
|
|
||||||
/* Adjust for internal main area offset */
|
/* Adjust for internal main area offset */
|
||||||
@@ -93,7 +92,7 @@ export function watchSidebar(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/* Compute the sidebar's available height */
|
/* Compute the sidebar's available height */
|
||||||
const height$ = combineLatest(offset$, main$)
|
const height$ = combineLatest([offset$, main$])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([{ y }, { offset, height }]) => {
|
map(([{ y }, { offset, height }]) => {
|
||||||
return height - adjust + Math.min(adjust, Math.max(0, y - offset))
|
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 */
|
/* Compute whether the sidebar should be locked */
|
||||||
const lock$ = combineLatest(offset$, main$)
|
const lock$ = combineLatest([offset$, main$])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([{ y }, { offset }]) => y >= offset + adjust)
|
map(([{ y }, { offset }]) => y >= offset + adjust)
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Combine into single hot observable */
|
/* Combine into single hot observable */
|
||||||
return combineLatest(height$, lock$)
|
return combineLatest([height$, lock$])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([height, lock]) => ({ height, lock })),
|
map(([height, lock]) => ({ height, lock })),
|
||||||
distinctUntilChanged<Sidebar>(equals),
|
distinctUntilChanged<Sidebar>(equals),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Paint sidebar from source observable
|
* Setup sidebar
|
||||||
*
|
*
|
||||||
* @param el - Sidebar element
|
* @param el - Sidebar element
|
||||||
|
* @param options - Options
|
||||||
*
|
*
|
||||||
* @return Operator function
|
* @return Sidebar observable
|
||||||
*/
|
*/
|
||||||
export function paintSidebar(
|
export function setupSidebar(
|
||||||
el: HTMLElement
|
el: HTMLElement, options: Options
|
||||||
): MonoTypeOperatorFunction<Sidebar> {
|
): Observable<Sidebar> {
|
||||||
return pipe(
|
return watchSidebar(el, options)
|
||||||
|
.pipe(
|
||||||
|
observeOn(animationFrameScheduler),
|
||||||
|
|
||||||
/* Defer repaint to next animation frame */
|
/* Apply mutations (side effects) */
|
||||||
observeOn(animationFrameScheduler),
|
tap(({ height, lock }) => {
|
||||||
tap(({ height, lock }) => {
|
setSidebarHeight(el, height)
|
||||||
setSidebarHeight(el, height)
|
setSidebarLock(el, lock)
|
||||||
setSidebarLock(el, lock)
|
}),
|
||||||
}),
|
|
||||||
|
|
||||||
/* Reset on complete or error */
|
/* Reset on complete or error */
|
||||||
finalize(() => {
|
finalize(() => {
|
||||||
resetSidebarHeight(el)
|
resetSidebarHeight(el)
|
||||||
resetSidebarLock(el)
|
resetSidebarLock(el)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -20,4 +20,6 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "custom-event-polyfill"
|
export * from "./component"
|
||||||
|
export * from "./utilities"
|
||||||
|
// export * from "./worker"
|
||||||
@@ -20,11 +20,6 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { OperatorFunction, pipe } from "rxjs"
|
|
||||||
import { filter, map } from "rxjs/operators"
|
|
||||||
|
|
||||||
import { toArray } from "../../utilities"
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Functions
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
@@ -58,44 +53,5 @@ export function getElement<T extends HTMLElement>(
|
|||||||
export function getElements<T extends HTMLElement>(
|
export function getElements<T extends HTMLElement>(
|
||||||
selector: string, node: ParentNode = document
|
selector: string, node: ParentNode = document
|
||||||
): T[] {
|
): T[] {
|
||||||
return toArray(node.querySelectorAll<T>(selector))
|
return Array.from(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))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -20,5 +20,5 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./_"
|
|
||||||
export * from "./element"
|
export * from "./element"
|
||||||
|
export * from "./operator"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NEVER, Observable, OperatorFunction, pipe } from "rxjs"
|
import { EMPTY, Observable, OperatorFunction, pipe } from "rxjs"
|
||||||
import { switchMap } from "rxjs/operators"
|
import { switchMap } from "rxjs/operators"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
@@ -28,37 +28,35 @@ import { switchMap } from "rxjs/operators"
|
|||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a collection to an array
|
* Switch to another observable if source observable emits `true`
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*
|
*
|
||||||
* @template T - Observable value type
|
* @template T - Observable value type
|
||||||
*
|
*
|
||||||
* @param project - Project function
|
* @param project - Project function
|
||||||
*
|
*
|
||||||
* @return Observable, if toggle is active
|
* @return Operator function
|
||||||
*/
|
*/
|
||||||
export function toggle<T>(
|
export function switchMapIfActive<T>(
|
||||||
project: () => Observable<T>
|
project: (value: boolean) => Observable<T>
|
||||||
): OperatorFunction<boolean, T> {
|
): OperatorFunction<boolean, T> {
|
||||||
return pipe(
|
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.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Observable, of } from "rxjs"
|
import { Observable, fromEvent } from "rxjs"
|
||||||
import { shareReplay } from "rxjs/operators"
|
import { ajax } from "rxjs/ajax"
|
||||||
|
import {
|
||||||
import { getElement, getElements } from "../../ui"
|
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
|
* Functions
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set navigation overflow scrolling
|
* Watch document
|
||||||
*
|
*
|
||||||
* @param el - Navigation element
|
* @return Document observable
|
||||||
* @param active - Whether overflow scrolling is active
|
|
||||||
*/
|
*/
|
||||||
export function setNavigationOverflowScrolling(
|
export function watchDocument(): Observable<Document> {
|
||||||
el: HTMLElement, active: boolean
|
return load$
|
||||||
): 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)
|
|
||||||
.pipe(
|
.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.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./element"
|
export * from "./document"
|
||||||
export * from "./location"
|
export * from "./location"
|
||||||
export * from "./media"
|
export * from "./media"
|
||||||
export * from "./viewport"
|
export * from "./viewport"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Observable, Subject, fromEvent } from "rxjs"
|
import { Observable, Subject, fromEvent } from "rxjs"
|
||||||
import { filter, map, share, startWith } from "rxjs/operators"
|
import { filter, map, share } from "rxjs/operators"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
* Data
|
* 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
|
* @return Location subject
|
||||||
*/
|
*/
|
||||||
@@ -50,7 +50,8 @@ export function watchLocation(): Subject<string> {
|
|||||||
const location$ = new Subject<string>()
|
const location$ = new Subject<string>()
|
||||||
popstate$
|
popstate$
|
||||||
.pipe(
|
.pipe(
|
||||||
map(() => location.href)
|
map(() => location.href),
|
||||||
|
share()
|
||||||
)
|
)
|
||||||
.subscribe(location$)
|
.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$
|
return hashchange$
|
||||||
.pipe(
|
.pipe(
|
||||||
map(() => location.hash),
|
map(() => location.hash),
|
||||||
startWith(location.hash),
|
|
||||||
filter(hash => hash.length > 0),
|
filter(hash => hash.length > 0),
|
||||||
share()
|
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
|
* @param query - Media query
|
||||||
*
|
*
|
||||||
* @return Media observable
|
* @return Media observable
|
||||||
*/
|
*/
|
||||||
export function watchMedia(query: string): Observable<boolean> {
|
export function watchMedia(query: string): Observable<boolean> {
|
||||||
const media = window.matchMedia(query)
|
const media = 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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,33 +62,33 @@ export interface ViewportSize {
|
|||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the viewport offset
|
* Retrieve viewport offset
|
||||||
*
|
*
|
||||||
* @return Viewport offset
|
* @return Viewport offset
|
||||||
*/
|
*/
|
||||||
export function getViewportOffset(): ViewportOffset {
|
export function getViewportOffset(): ViewportOffset {
|
||||||
return {
|
return {
|
||||||
x: window.pageXOffset,
|
x: pageXOffset,
|
||||||
y: window.pageYOffset
|
y: pageYOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the viewport size
|
* Retrieve viewport size
|
||||||
*
|
*
|
||||||
* @return Viewport size
|
* @return Viewport size
|
||||||
*/
|
*/
|
||||||
export function getViewportSize(): ViewportSize {
|
export function getViewportSize(): ViewportSize {
|
||||||
return {
|
return {
|
||||||
width: window.innerWidth,
|
width: innerWidth,
|
||||||
height: window.innerHeight
|
height: innerHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------------- */
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an observable to watch the viewport offset
|
* Watch viewport offset
|
||||||
*
|
*
|
||||||
* @return Viewport offset observable
|
* @return Viewport offset observable
|
||||||
*/
|
*/
|
||||||
@@ -97,12 +97,12 @@ export function watchViewportOffset(): Observable<ViewportOffset> {
|
|||||||
.pipe(
|
.pipe(
|
||||||
map(getViewportOffset),
|
map(getViewportOffset),
|
||||||
startWith(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
|
* @return Viewport size observable
|
||||||
*/
|
*/
|
||||||
@@ -111,6 +111,6 @@ export function watchViewportSize(): Observable<ViewportSize> {
|
|||||||
.pipe(
|
.pipe(
|
||||||
map(getViewportSize),
|
map(getViewportSize),
|
||||||
startWith(getViewportSize()),
|
startWith(getViewportSize()),
|
||||||
shareReplay({ bufferSize: 1, refCount: true })
|
shareReplay(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./other"
|
|
||||||
Reference in New Issue
Block a user