Fixed Travis and added more components

This commit is contained in:
squidfunk
2019-11-27 19:12:49 +01:00
parent 9cae185391
commit c409fe3953
38 changed files with 937 additions and 244 deletions

View File

@@ -20,17 +20,44 @@
* IN THE SOFTWARE.
*/
import { shareReplay, switchMap } from "rxjs/operators"
import { identity } from "ramda"
import {
EMPTY,
MonoTypeOperatorFunction,
NEVER,
Observable,
fromEvent,
merge,
of,
pipe
} from "rxjs"
import {
delay,
filter,
map,
shareReplay,
switchMap,
switchMapTo,
tap,
withLatestFrom
} from "rxjs/operators"
import { isConfig } from "./config"
import {
setupSidebar,
Component,
paintHidden,
paintSidebar,
switchComponent,
switchMapIfActive,
watchBottomOffset,
watchComponentMap,
watchHeader,
watchMain
watchMain,
watchSearchReset,
watchSidebar,
watchToggle,
watchTopOffset
} from "./theme"
import { paintHeaderShadow } from "./theme/component/header/shadow"
import {
watchDocument,
watchDocumentSwitch,
@@ -40,9 +67,15 @@ import {
watchViewportOffset,
watchViewportSize
} from "./ui"
import {
getElement,
not,
switchMapIf
} from "./utilities"
// TBD
// TODO: put this somewhere else... (merge with config!) JSON schema!?
const names = [
"header", /* Header */
"title", /* Header title */
@@ -56,7 +89,11 @@ const names = [
"tabs", /* Tabs */
"navigation", /* Navigation */
"toc" /* Table of contents */
] as const // TODO: put this somewhere else... (merge with config!) JSON schema!?
] as const
// modernizr for the poor
document.documentElement.classList.remove("no-js")
document.documentElement.classList.add("js")
/* ----------------------------------------------------------------------------
* Functions
@@ -82,66 +119,212 @@ export function initialize(config: unknown) {
const tablet$ = watchMedia("(min-width: 960px)")
/* Create location observables */
const url$ = watchLocation()
const location$ = watchLocation()
const fragment$ = watchLocationFragment()
/* Create document observables */
const load$ = watchDocument()
const switch$ = watchDocumentSwitch({ url$ })
const switch$ = watchDocumentSwitch({ location$ })
/* ----------------------------------------------------------------------- */
/* Create component map observable */
const components$ = watchComponentMap(names, { load$, switch$ })
const component = (name: Component): Observable<HTMLElement> => {
return components$
.pipe(
switchComponent(name)
)
}
/* Create header observable */
const header$ = components$
const header$ = component("header")
.pipe(
switchComponent("header"),
switchMap(watchHeader)
)
/* Create main area observable */
const main$ = components$
const main$ = component("main")
.pipe(
switchComponent("main"),
switchMap(el => watchMain(el, { size$, offset$, header$ })),
shareReplay(1)
)
// ----------------------------------------------------------------------------
// WIP
load$
.pipe(
switchMap(({ body }) => fromEvent(body, "click")),
switchMap(ev => {
if (ev.target instanceof HTMLElement) {
const el = ev.target.closest("a") || undefined
if (el) {
if (!/^(https?:|#)/.test(el.getAttribute("href")!)) {
ev.preventDefault()
}
const href = el.href
history.pushState({}, "", href) // TODO: reference necessary!?
return of(href)
}
}
return EMPTY
}),
// try to reduce the jiggle upon instant page load. ideally, the location
// should directly be resolved and the respective document loaded, but
// we must scroll to the top at first and wait at least 250ms.
//
// Furthermore, this doesn't include the back/next buttons of the browser
// which must be delayed
tap(url => {
if (!/#/.test(url))
scrollTo({ top: 0 })
}), // only when loading something we havent loaded!
delay(250)
)
.subscribe(location$)
location$.subscribe(x => {
console.log("L", x)
})
switch$.subscribe(x => {
console.log("S", x)
})
/* ----------------------------------------------------------------------- */
/* Create sidebar with navigation */
screen$
/* Create header shadow toggle */
component("header")
.pipe(
switchMapIfActive(() => components$ // TODO: write an observable creation function...
switchMap(el => main$
.pipe(
switchComponent("navigation"),
switchMap(el => setupSidebar(el, { offset$, main$ }))
paintHeaderShadow(el)
)
)
)
.subscribe()
/* Create sidebar with table of contents (missing on 404 page) */
tablet$
/* Create sidebar with navigation */
component("navigation")
.pipe(
switchMapIfActive(() => components$
switchMapIf(screen$, el => watchSidebar(el, { offset$, main$ })
.pipe(
switchComponent("toc"),
switchMap(el => setupSidebar(el, { offset$, main$ }))
paintSidebar(el)
)
),
shareReplay(1)
)
.subscribe()
/* Create sidebar with table of contents */
component("toc")
.pipe(
switchMapIf(tablet$, el => watchSidebar(el, { offset$, main$ })
.pipe(
paintSidebar(el)
)
),
shareReplay(1)
)
.subscribe()
/* Create tabs visibility toggle */
component("tabs")
.pipe(
switchMapIf(screen$, el => watchTopOffset(el, { size$, offset$, header$ })
.pipe(
paintHidden(el, 8)
)
),
shareReplay(1)
)
.subscribe()
/* Create hero visibility toggle */
component("hero")
.pipe(
switchMap(el => watchTopOffset(el, { size$, offset$, header$ })
.pipe(
paintHidden(el, 20)
)
),
shareReplay(1)
)
.subscribe()
/* Create header title toggle */
component("main")
.pipe(
delay(1000), // initial delay
switchMap(el => typeof getElement("h1", el) !== "undefined"
? watchBottomOffset(getElement("h1", el)!, { size$, offset$, header$ })
.pipe(
map(({ y }) => y >= 0),
withLatestFrom(component("title")),
tap(([active, title]) => {
title.dataset.mdState = active ? "active" : ""
})
)
: NEVER
)
)
.subscribe(console.log)
.subscribe()
/* Return all observables */
// TODO: replace title as inner text
/* ----------------------------------------------------------------------- */
const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")!
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
// watchToggle
// --> watchSearchQuery?
// watchSearch
// watchSearchReset
// toggles stay the same...
const a$ = watchToggle(search)
.pipe(
filter(identity),
delay(400)
)
// watchSearchReset()
const b$ = component("reset")
.pipe(
switchMap(watchSearchReset)
)
function focusQuery(): MonoTypeOperatorFunction<HTMLElement> {
return pipe(
tap(el => el.focus())
)
}
merge(a$, b$)
.pipe(
switchMapTo(component("query")),
focusQuery()
)
.subscribe()
/* Return observable factories */
return {
ui: {
document: { load$, switch$ },
location: { url$, fragment$ },
media: { screen$, tablet$ },
viewport: { offset$, size$ }
}
/* User interface */
watchDocument: () => load$,
watchDocumentSwitch: () => switch$,
watchLocation: () => location$,
watchLocationFragment: () => fragment$,
watchMediaScreen: () => screen$,
watchMediaTablet: () => tablet$,
watchViewportOffset: () => offset$,
watchViewportSize: () => size$
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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 header shadow
*
* @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 el - Header element
*/
export function resetHeaderShadow(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@@ -0,0 +1,48 @@
/*
* 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 hidden
*
* @param el - Hideable element
* @param value - Whether the element is hidden
*/
export function setHidden(
el: HTMLElement, value: boolean
): void {
el.setAttribute("data-md-state", value ? "hidden" : "")
}
/**
* Reset hidden
*
* @param el - Hideable element
*/
export function resetHidden(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@@ -0,0 +1,25 @@
/*
* 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.
*/
export * from "./header"
export * from "./hidden"
export * from "./sidebar"

View File

@@ -24,14 +24,14 @@ import { keys } from "ramda"
import { NEVER, Observable, OperatorFunction, merge, of, pipe } from "rxjs"
import { map, scan, shareReplay, switchMap } from "rxjs/operators"
import { getElement } from "../../utilities"
import { getElement } from "../../../utilities"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Components
* Component names
*/
export type Component =
| "header" /* Header */
@@ -76,7 +76,7 @@ interface Options {
* This function returns an observable that will maintain bindings to the given
* components in-between document switches and update the document in-place.
*
* @param names - Components
* @param names - Component names
* @param options - Options
*
* @return Component map observable
@@ -133,7 +133,7 @@ export function watchComponentMap(
*
* @template T - Element type
*
* @param name - Component
* @param name - Component name
*
* @return Operator function
*/

View File

@@ -21,3 +21,4 @@
*/
export * from "./_"
export * from "./offset"

View File

@@ -0,0 +1,111 @@
/*
* 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 { Observable, combineLatest } from "rxjs"
import {
distinctUntilChanged,
map,
shareReplay,
switchMapTo
} from "rxjs/operators"
import { ViewportOffset, ViewportSize } from "../../../../ui"
import { Header } from "../_"
/* ----------------------------------------------------------------------------
* Function types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
size$: Observable<ViewportSize> /* Viewport size observable */
offset$: Observable<ViewportOffset> /* Viewport offset observable */
header$: Observable<Header> /* Header observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch viewport offset relative to an element's top
*
* This function returns an observable that computes the relative offset to the
* top of the given element based on the current viewport offset.
*
* @param el - Element
* @param options - Options
*
* @return Viewport offset observable
*/
export function watchTopOffset(
el: HTMLElement, { size$, offset$, header$ }: Options
): Observable<ViewportOffset> {
/* Compute necessary adjustment for offset */
const adjust$ = size$
.pipe(
switchMapTo(header$),
map(({ height }) => el.offsetTop - height),
distinctUntilChanged()
)
/* Compute relative offset and return as hot observable */
return combineLatest([offset$, adjust$])
.pipe(
map(([{ x, y }, adjust]) => ({ x, y: y - adjust })),
shareReplay(1)
)
}
/**
* Watch viewport offset relative to an element's bottom
*
* This function returns an observable that computes the relative offset to the
* bottom of the given element based on the current viewport offset.
*
* @param el - Element
* @param options - Options
*
* @return Viewport offset observable
*/
export function watchBottomOffset(
el: HTMLElement, { size$, offset$, header$ }: Options
): Observable<ViewportOffset> {
/* Compute necessary adjustment for offset */
const adjust$ = size$
.pipe(
switchMapTo(header$),
map(({ height }) => el.offsetTop + el.offsetHeight - height),
distinctUntilChanged()
)
/* Compute relative offset and return as hot observable */
return combineLatest([offset$, adjust$])
.pipe(
map(([{ x, y }, adjust]) => ({ x, y: y - adjust })),
shareReplay(1)
)
}

View File

@@ -20,43 +20,50 @@
* IN THE SOFTWARE.
*/
import { EMPTY, Observable, OperatorFunction, pipe } from "rxjs"
import { switchMap } from "rxjs/operators"
import {
MonoTypeOperatorFunction,
animationFrameScheduler,
pipe
} from "rxjs"
import {
distinctUntilKeyChanged,
finalize,
observeOn,
tap
} from "rxjs/operators"
import {
resetHeaderShadow,
setHeaderShadow
} from "../../../action"
import { Main } from "../../main"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Switch to another observable if source observable emits `true`
* Paint header shadow from source observable
*
* @template T - Observable value type
*
* @param project - Project function
* @param el - Header element
*
* @return Operator function
*/
export function switchMapIfActive<T>(
project: (value: boolean) => Observable<T>
): OperatorFunction<boolean, T> {
export function paintHeaderShadow(
el: HTMLElement
): MonoTypeOperatorFunction<Main> {
return pipe(
switchMap(value => value ? project(value) : EMPTY)
)
}
distinctUntilKeyChanged("active"),
/**
* 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))
/* Defer repaint to next animation frame */
observeOn(animationFrameScheduler),
tap(({ active }) => {
setHeaderShadow(el, active)
}),
/* Reset on complete or error */
finalize(() => {
resetHeaderShadow(el)
})
)
}

View File

@@ -0,0 +1,65 @@
/*
* 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 { OperatorFunction, animationFrameScheduler, pipe } from "rxjs"
import {
distinctUntilChanged,
finalize,
map,
observeOn,
tap
} from "rxjs/operators"
import { ViewportOffset } from "../../../ui"
import { resetHidden, setHidden } from "../../action"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Paint hideable from source observable
*
* @param el - Hideable element
* @param offset - Additional offset
*
* @return Operator function
*/
export function paintHidden(
el: HTMLElement, offset: number = 0
): OperatorFunction<ViewportOffset, boolean> {
return pipe(
map(({ y }) => y >= offset),
distinctUntilChanged(),
/* Defer repaint to next animation frame */
observeOn(animationFrameScheduler),
tap(value => {
setHidden(el, value)
}),
/* Reset on complete or error */
finalize(() => {
resetHidden(el)
})
)
}

View File

@@ -22,5 +22,7 @@
export * from "./_"
export * from "./header"
export * from "./hidden"
export * from "./main"
export * from "./search"
export * from "./sidebar"

View File

@@ -0,0 +1,24 @@
/*
* 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.
*/
// export * from "./query"
export * from "./reset"

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/

View File

@@ -0,0 +1,44 @@
/*
* 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 { Observable, fromEvent } from "rxjs"
import { mapTo } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch search reset
*
* @param el - Search reset element
*
* @return Search reset observable
*/
export function watchSearchReset(
el: HTMLElement
): Observable<boolean> {
return fromEvent(el, "click")
.pipe(
mapTo(true)
)
}

View File

@@ -1,144 +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 { equals } from "ramda"
import { Observable, animationFrameScheduler, combineLatest } from "rxjs"
import {
distinctUntilChanged,
finalize,
map,
observeOn,
shareReplay,
tap
} from "rxjs/operators"
import { ViewportOffset } from "../../../../ui"
import { Main } from "../../main"
import {
resetSidebarHeight,
resetSidebarLock,
setSidebarHeight,
setSidebarLock
} from "../element"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Sidebar
*/
export interface Sidebar {
height: number /* Sidebar height */
lock: boolean /* Sidebar lock */
}
/* ----------------------------------------------------------------------------
* Function types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
offset$: Observable<ViewportOffset> /* Viewport offset observable */
main$: Observable<Main> /* Main area observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* 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
*
* @return Sidebar observable
*/
export function watchSidebar(
el: HTMLElement, { offset$, main$ }: Options
): Observable<Sidebar> {
/* Adjust for internal main area offset */
const adjust = parseFloat(
getComputedStyle(el.parentElement!)
.getPropertyValue("padding-top")
)
/* Compute the sidebar's available height */
const height$ = combineLatest([offset$, main$])
.pipe(
map(([{ y }, { offset, height }]) => {
return height - adjust + Math.min(adjust, Math.max(0, y - offset))
})
)
/* Compute whether the sidebar should be locked */
const lock$ = combineLatest([offset$, main$])
.pipe(
map(([{ y }, { offset }]) => y >= offset + adjust)
)
/* Combine into single hot observable */
return combineLatest([height$, lock$])
.pipe(
map(([height, lock]) => ({ height, lock })),
distinctUntilChanged<Sidebar>(equals),
shareReplay(1)
)
}
/**
* Setup sidebar
*
* @param el - Sidebar element
* @param options - Options
*
* @return Sidebar observable
*/
export function setupSidebar(
el: HTMLElement, options: Options
): Observable<Sidebar> {
return watchSidebar(el, options)
.pipe(
observeOn(animationFrameScheduler),
/* Apply mutations (side effects) */
tap(({ height, lock }) => {
setSidebarHeight(el, height)
setSidebarLock(el, lock)
}),
/* Reset on complete or error */
finalize(() => {
resetSidebarHeight(el)
resetSidebarLock(el)
})
)
}

View File

@@ -20,5 +20,131 @@
* IN THE SOFTWARE.
*/
export * from "./_"
export * from "./element"
import { equals } from "ramda"
import {
MonoTypeOperatorFunction,
Observable,
animationFrameScheduler,
combineLatest,
pipe
} from "rxjs"
import {
distinctUntilChanged,
finalize,
map,
observeOn,
shareReplay,
tap
} from "rxjs/operators"
import { ViewportOffset } from "../../../ui"
import {
resetSidebarHeight,
resetSidebarLock,
setSidebarHeight,
setSidebarLock
} from "../../action"
import { Main } from "../main"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Sidebar
*/
export interface Sidebar {
height: number /* Sidebar height */
lock: boolean /* Sidebar lock */
}
/* ----------------------------------------------------------------------------
* Function types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
offset$: Observable<ViewportOffset> /* Viewport offset observable */
main$: Observable<Main> /* Main area observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* 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
*
* @return Sidebar observable
*/
export function watchSidebar(
el: HTMLElement, { offset$, main$ }: Options
): Observable<Sidebar> {
/* Adjust for internal main area offset */
const adjust = parseFloat(
getComputedStyle(el.parentElement!)
.getPropertyValue("padding-top")
)
/* Compute the sidebar's available height */
const height$ = combineLatest([offset$, main$])
.pipe(
map(([{ y }, { offset, height }]) => {
return height - adjust + Math.min(adjust, Math.max(0, y - offset))
})
)
/* Compute whether the sidebar should be locked */
const lock$ = combineLatest([offset$, main$])
.pipe(
map(([{ y }, { offset }]) => y >= offset + adjust)
)
/* Combine into single hot observable */
return combineLatest([height$, lock$])
.pipe(
map(([height, lock]) => ({ height, lock })),
distinctUntilChanged<Sidebar>(equals),
shareReplay(1)
)
}
/* ------------------------------------------------------------------------- */
/**
* Paint sidebar from source observable
*
* @param el - Sidebar element
*
* @return Operator function
*/
export function paintSidebar(
el: HTMLElement
): MonoTypeOperatorFunction<Sidebar> {
return pipe(
/* Defer repaint to next animation frame */
observeOn(animationFrameScheduler),
tap(({ height, lock }) => {
setSidebarHeight(el, height)
setSidebarLock(el, lock)
}),
/* Reset on complete or error */
finalize(() => {
resetSidebarHeight(el)
resetSidebarLock(el)
})
)
}

View File

@@ -20,6 +20,7 @@
* IN THE SOFTWARE.
*/
export * from "./action"
export * from "./component"
export * from "./utilities"
export * from "./toggle"
// export * from "./worker"

View File

@@ -0,0 +1,44 @@
/*
* 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 { Observable, fromEvent } from "rxjs"
import { pluck } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch toggle
*
* @param el - Toggle element
*
* @return Toggle observable
*/
export function watchToggle(
el: HTMLInputElement
): Observable<boolean> {
return fromEvent(el, "change")
.pipe(
pluck("checked")
)
}

View File

@@ -41,7 +41,7 @@ import {
* Switch options
*/
interface SwitchOptions {
url$: Observable<string> /* Location observable */
location$: Observable<string> /* Location observable */
}
/* ----------------------------------------------------------------------------
@@ -83,14 +83,16 @@ export function watchDocument(): Observable<Document> {
* @return Document switch observable
*/
export function watchDocumentSwitch(
{ url$ }: SwitchOptions
{ location$ }: SwitchOptions
): Observable<Document> {
return url$
return location$
.pipe(
startWith(location.href),
map(url => url.replace(/#[^#]+$/, "")),
distinctUntilChanged(),
skip(1),
/* Fetch document */
switchMap(url => ajax({
url,
responseType: "document",

View File

@@ -0,0 +1,86 @@
/*
* 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 {
EMPTY,
Observable,
OperatorFunction,
combineLatest,
of,
pipe
} from "rxjs"
import {
filter,
map,
switchMap,
takeUntil
} from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Invert boolean value of source observable
*
* @param toggle$ - Toggle observable
*
* @return Inverted toggle observable
*/
export function not(
toggle$: Observable<boolean>
): Observable<boolean> {
return toggle$
.pipe(
map(active => !active)
)
}
/* ------------------------------------------------------------------------- */
/**
* Toggle switch map with another observable
*
* @template T - Source value type
* @template U - Target value type
*
* @param toggle$ - Toggle observable
* @param project - Projection
*
* @return Operator function
*/
export function switchMapIf<T, U>(
toggle$: Observable<boolean>, project: (value: T) => Observable<U>
): OperatorFunction<T, U> {
const begin$ = toggle$.pipe(filter(value => value))
const end$ = toggle$.pipe(filter(value => !value))
return pipe(
switchMap(value => combineLatest([of(value), begin$])),
switchMap(([value, active]) => active
? project(value)
.pipe(
takeUntil(end$)
)
: EMPTY
)
)
}