Finished search and moved components to setup functions

This commit is contained in:
squidfunk
2019-12-20 18:03:31 +01:00
parent c7dfcb87cc
commit 6863aeb4a4
36 changed files with 1184 additions and 354 deletions

View File

@@ -70,7 +70,7 @@ interface Options {
* ------------------------------------------------------------------------- */
/**
* Watch component map
* Watch component mapping
*
* This function returns an observable that will maintain bindings to the given
* components in-between document switches and update the document in-place.
@@ -78,7 +78,7 @@ interface Options {
* @param names - Component names
* @param options - Options
*
* @return Component map observable
* @return Component mapping observable
*/
export function watchComponentMap(
names: Component[], { document$ }: Options

View File

@@ -28,7 +28,8 @@ import {
switchMapTo
} from "rxjs/operators"
import { ViewportOffset, ViewportSize } from "../../../utilities"
import { Agent, ViewportOffset } from "utilities"
import { Header } from "../_"
/* ----------------------------------------------------------------------------
@@ -39,8 +40,6 @@ import { Header } from "../_"
* Options
*/
interface Options {
size$: Observable<ViewportSize> /* Viewport size observable */
offset$: Observable<ViewportOffset> /* Viewport offset observable */
header$: Observable<Header> /* Header observable */
}
@@ -55,16 +54,17 @@ interface Options {
* top of the given element based on the current viewport offset.
*
* @param el - HTML element
* @param agent - Agent
* @param options - Options
*
* @return Viewport offset observable
*/
export function watchHeaderOffsetToTopOf(
el: HTMLElement, { size$, offset$, header$ }: Options
el: HTMLElement, { viewport }: Agent, { header$ }: Options
): Observable<ViewportOffset> {
/* Compute necessary adjustment for offset */
const adjust$ = size$
const adjust$ = viewport.size$
.pipe(
switchMapTo(header$),
map(({ height }) => el.offsetTop - height),
@@ -72,7 +72,7 @@ export function watchHeaderOffsetToTopOf(
)
/* Compute relative offset and return as hot observable */
return combineLatest([offset$, adjust$])
return combineLatest([viewport.offset$, adjust$])
.pipe(
map(([{ x, y }, adjust]) => ({ x, y: y - adjust })),
shareReplay(1)
@@ -86,16 +86,17 @@ export function watchHeaderOffsetToTopOf(
* bottom of the given element based on the current viewport offset.
*
* @param el - HTML element
* @param agent - Agent
* @param options - Options
*
* @return Viewport offset observable
*/
export function watchHeaderOffsetToBottomOf(
el: HTMLElement, { size$, offset$, header$ }: Options
el: HTMLElement, { viewport }: Agent, { header$ }: Options
): Observable<ViewportOffset> {
/* Compute necessary adjustment for offset */
const adjust$ = size$
const adjust$ = viewport.size$
.pipe(
switchMapTo(header$),
map(({ height }) => el.offsetTop + el.offsetHeight - height),
@@ -103,7 +104,7 @@ export function watchHeaderOffsetToBottomOf(
)
/* Compute relative offset and return as hot observable */
return combineLatest([offset$, adjust$])
return combineLatest([viewport.offset$, adjust$])
.pipe(
map(([{ x, y }, adjust]) => ({ x, y: y - adjust })),
shareReplay(1)

View File

@@ -32,7 +32,8 @@ import {
tap
} from "rxjs/operators"
import { resetHeaderShadow, setHeaderShadow } from "../../../actions"
import { resetHeaderShadow, setHeaderShadow } from "actions"
import { Main } from "../../main"
/* ----------------------------------------------------------------------------

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 { Observable, OperatorFunction, pipe } from "rxjs"
import { map, shareReplay } from "rxjs/operators"
import { switchMapIf } from "extensions"
import { Agent, paintHidden } from "utilities"
import { Header, watchHeaderOffsetToTopOf } from "../header"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Hero
*/
export interface Hero {
hidden: boolean /* Whether the hero is hidden */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
header$: Observable<Header> /* Header observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Setup hero from source observable
*
* @param agent - Agent
* @param options - Options
*
* @return Operator function
*/
export function setupHero(
agent: Agent, { header$ }: Options
): OperatorFunction<HTMLElement, Hero> {
const { media } = agent
return pipe(
switchMapIf(media.screen$, el => {
/* Watch and paint visibility */
const hidden$ = watchHeaderOffsetToTopOf(el, agent, { header$ })
.pipe(
paintHidden(el, 20)
)
/* Combine into a single hot observable */
return hidden$
.pipe(
map(hidden => ({ hidden }))
)
}),
shareReplay(1)
)
}

View File

@@ -21,9 +21,10 @@
*/
export * from "./_"
export * from "./anchor"
export * from "./header"
export * from "./hidden"
export * from "./hero"
export * from "./navigation"
export * from "./main"
export * from "./search"
export * from "./sidebar"
export * from "./tabs"
export * from "./toc"

View File

@@ -0,0 +1,125 @@
/*
* 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, OperatorFunction, combineLatest, pipe } from "rxjs"
import {
distinctUntilChanged,
map,
pluck,
shareReplay,
switchMap
} from "rxjs/operators"
import { Agent } from "utilities"
import { Header } from "../../header"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Main area
*/
export interface Main {
offset: number /* Main area top offset */
height: number /* Main area visible height */
active: boolean /* Scrolled past top offset */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
header$: Observable<Header> /* Header observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Setup main area from source observable
*
* 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 agent - Agent
* @param options - Options
*
* @return Operator function
*/
export function setupMain(
{ viewport }: Agent, { header$ }: Options
): OperatorFunction<HTMLElement, Main> {
return pipe(
switchMap(el => {
/* Compute necessary adjustment for header */
const adjust$ = header$
.pipe(
pluck("height")
)
/* Compute the main area's visible height */
const height$ = combineLatest([
viewport.offset$,
viewport.size$,
adjust$
])
.pipe(
map(([{ y }, { height }, adjust]) => {
const top = el.offsetTop
const bottom = el.offsetHeight + top
return height
- Math.max(0, top - y, adjust)
- Math.max(0, height + y - bottom)
}),
distinctUntilChanged()
)
/* Compute whether the viewport offset is past the main area's top */
const active$ = combineLatest([viewport.offset$, adjust$])
.pipe(
map(([{ y }, adjust]) => y >= el.offsetTop - adjust),
distinctUntilChanged()
)
/* Combine into a single hot observable */
return combineLatest([height$, adjust$, active$])
.pipe(
map(([height, adjust, active]) => ({
offset: el.offsetTop - adjust,
height,
active
}))
)
}),
shareReplay(1)
)
}

View File

@@ -20,99 +20,5 @@
* IN THE SOFTWARE.
*/
import { Observable, combineLatest } from "rxjs"
import {
distinctUntilChanged,
map,
pluck,
shareReplay
} from "rxjs/operators"
import { ViewportOffset, ViewportSize } from "utilities"
import { Header } from "../header"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Main area
*/
export interface Main {
offset: number /* Main area top offset */
height: number /* Main area visible height */
active: boolean /* Scrolled past top offset */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
size$: Observable<ViewportSize> /* Viewport size observable */
offset$: Observable<ViewportOffset> /* Viewport offset observable */
header$: Observable<Header> /* Header observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch main area
*
* This function returns an observable that computes the visual parameters of
* the main area from the viewport height and vertical offset, as well as the
* height of the header element. The height of the main area is corrected by
* the height of the header (if fixed) and footer element.
*
* @param el - Main area element
* @param options - Options
*
* @return Main area observable
*/
export function watchMain(
el: HTMLElement, { size$, offset$, header$ }: Options
): Observable<Main> {
/* Compute necessary adjustment for header */
const adjust$ = header$
.pipe(
pluck("height")
)
/* Compute the main area's visible height */
const height$ = combineLatest([offset$, size$, adjust$])
.pipe(
map(([{ y }, { height }, adjust]) => {
const top = el.offsetTop
const bottom = el.offsetHeight + top
return height
- Math.max(0, top - y, adjust)
- Math.max(0, height + y - bottom)
}),
distinctUntilChanged()
)
/* Compute whether the viewport offset is past the main area's top */
const active$ = combineLatest([offset$, adjust$])
.pipe(
map(([{ y }, adjust]) => y >= el.offsetTop - adjust),
distinctUntilChanged()
)
/* Combine into a single hot observable */
return combineLatest([height$, adjust$, active$])
.pipe(
map(([height, adjust, active]) => ({
offset: el.offsetTop - adjust,
height,
active
})),
shareReplay(1)
)
}
export * from "./_"
export * from "./sidebar"

View File

@@ -43,7 +43,7 @@ import {
setSidebarHeight,
setSidebarLock
} from "actions"
import { ViewportOffset } from "utilities"
import { Agent } from "utilities"
import { Main } from "../main"
@@ -67,7 +67,6 @@ export interface Sidebar {
* Options
*/
interface Options {
offset$: Observable<ViewportOffset> /* Viewport offset observable */
main$: Observable<Main> /* Main area observable */
}
@@ -84,12 +83,13 @@ interface Options {
* the sidebar is locked and fills the remaining space.
*
* @param el - Sidebar element
* @param agent - Agent
* @param options - Options
*
* @return Sidebar observable
*/
export function watchSidebar(
el: HTMLElement, { offset$, main$ }: Options
el: HTMLElement, { viewport }: Agent, { main$ }: Options
): Observable<Sidebar> {
/* Adjust for internal main area offset */
@@ -99,7 +99,7 @@ export function watchSidebar(
)
/* Compute the sidebar's available height */
const height$ = combineLatest([offset$, main$])
const height$ = combineLatest([viewport.offset$, main$])
.pipe(
map(([{ y }, { offset, height }]) => {
return height - adjust + Math.min(adjust, Math.max(0, y - offset))
@@ -107,7 +107,7 @@ export function watchSidebar(
)
/* Compute whether the sidebar should be locked */
const lock$ = combineLatest([offset$, main$])
const lock$ = combineLatest([viewport.offset$, main$])
.pipe(
map(([{ y }, { offset }]) => y >= offset + adjust)
)

View File

@@ -0,0 +1,91 @@
/*
* 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, OperatorFunction, pipe } from "rxjs"
import { map, shareReplay } from "rxjs/operators"
import { switchMapIf } from "extensions"
import { Agent } from "utilities"
import {
Main,
Sidebar,
paintSidebar,
watchSidebar
} from "../../main"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Navigation
*/
export interface Navigation {
sidebar: Sidebar /* Sidebar */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
main$: Observable<Main> /* Main observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Setup navigation from source observable
*
* @param agent - Agent
* @param options - Options
*
* @return Operator function
*/
export function setupNavigation(
agent: Agent, { main$ }: Options
): OperatorFunction<HTMLElement, Navigation> {
const { media } = agent
return pipe(
switchMapIf(media.screen$, el => {
/* Watch and paint sidebar */
const sidebar$ = watchSidebar(el, agent, { main$ })
.pipe(
paintSidebar(el)
)
/* Combine into a single hot observable */
return sidebar$
.pipe(
map(sidebar => ({ sidebar }))
)
}),
shareReplay(1)
)
}

View File

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

View File

@@ -0,0 +1,85 @@
/*
* 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 { identity } from "ramda"
import { Observable, OperatorFunction, pipe } from "rxjs"
import {
distinctUntilChanged,
filter,
map,
switchMap,
} from "rxjs/operators"
import { SearchResult } from "modules"
import { Agent, watchElementOffset } from "utilities"
import { paintSearchResultList } from "../list"
import { paintSearchResultMeta } from "../meta"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
result$: Observable<SearchResult[]> /* Search result observable */
query$: Observable<string> /* Search query observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Setup search result from source observable
*
* @param el - Search result element
* @param agent - Agent
*
* @return Operator function
*/
export function setupSearchResult(
agent: Agent, { result$, query$ }: Options
): OperatorFunction<HTMLElement, SearchResult[]> {
return pipe(
switchMap(el => {
const parent = el.parentElement!
/* Compute whether more elements need to be rendered */
const render$ = watchElementOffset(parent, agent)
.pipe(
map(({ y }) => y >= parent.scrollHeight - parent.offsetHeight - 16),
distinctUntilChanged(),
filter(identity)
)
/* Paint search results */
return result$
.pipe(
paintSearchResultMeta(el, { query$ }),
paintSearchResultList(el, { render$ })
)
})
)
}

View File

@@ -20,99 +20,6 @@
* IN THE SOFTWARE.
*/
import { identity } from "ramda"
import {
MonoTypeOperatorFunction,
Observable,
animationFrameScheduler,
pipe
} from "rxjs"
import {
distinctUntilChanged,
filter,
finalize,
map,
mapTo,
observeOn,
scan,
switchMap,
tap
} from "rxjs/operators"
import {
addToSearchResultList,
resetSearchResultList,
setSearchResultMeta
} from "actions"
import { SearchResult } from "modules"
import { renderSearchResult } from "templates"
import { ViewportSize, watchElementOffset } from "utilities"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
size$: Observable<ViewportSize> /* Viewport size observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Paint search result from source observable
*
* @param el - Search result element
*
* @return Operator function
*/
export function paintSearchResult(
el: HTMLElement, { size$ }: Options
): MonoTypeOperatorFunction<SearchResult[]> {
const container = el.parentElement!
/* Compute whether the container is near the bottom offset */
const render$ = watchElementOffset(container, { size$ })
.pipe(
map(({ y }) => y >= container.scrollHeight - container.offsetHeight - 16),
distinctUntilChanged(),
filter(identity)
)
/* Paint search results lazily */
const [meta, list] = Array.from(el.children) as HTMLElement[]
return pipe(
/* Paint search result metadata */
tap(result => {
setSearchResultMeta(meta, result.length)
}),
/* Paint search result list */
switchMap(result => render$
.pipe(
/* Defer repaint to next animation frame */
observeOn(animationFrameScheduler),
scan(index => {
while (index < result.length) {
addToSearchResultList(list, renderSearchResult(result[index++]))
if (container.scrollHeight - container.offsetHeight > 16)
break
}
return index
}, 0),
mapTo(result),
/* Reset on complete or error */
finalize(() => {
resetSearchResultList(list)
})
)
)
)
}
export * from "./_"
export * from "./list"
export * from "./meta"

View File

@@ -0,0 +1,98 @@
/*
* 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 {
MonoTypeOperatorFunction,
Observable,
animationFrameScheduler,
pipe
} from "rxjs"
import {
finalize,
mapTo,
observeOn,
scan,
switchMap
} from "rxjs/operators"
import {
addToSearchResultList,
resetSearchResultList
} from "actions"
import { SearchResult } from "modules"
import { renderSearchResult } from "templates"
import { getElement } from "utilities"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
render$: Observable<boolean> /* Render trigger observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Paint search result list from source observable
*
* @param el - Search result element
* @param options - Options
*
* @return Operator function
*/
export function paintSearchResultList(
el: HTMLElement, { render$ }: Options
): MonoTypeOperatorFunction<SearchResult[]> {
const parent = el.parentElement!
const list = getElement(".md-search-result__list", el)!
return pipe(
switchMap(result => render$
.pipe(
/* Defer repaint to next animation frame */
observeOn(animationFrameScheduler),
scan(index => {
while (index < result.length) {
addToSearchResultList(list, renderSearchResult(result[index++]))
if (parent.scrollHeight - parent.offsetHeight > 16)
break
}
return index
}, 0),
/* Re-map to search result */
mapTo(result),
/* Reset on complete or error */
finalize(() => {
resetSearchResultList(list)
})
)
)
)
}

View File

@@ -20,46 +20,52 @@
* IN THE SOFTWARE.
*/
import { OperatorFunction, animationFrameScheduler, pipe } from "rxjs"
import {
distinctUntilChanged,
finalize,
map,
observeOn,
tap
} from "rxjs/operators"
import { MonoTypeOperatorFunction, Observable, pipe } from "rxjs"
import { map, withLatestFrom } from "rxjs/operators"
import { resetHidden, setHidden } from "actions"
import { ViewportOffset } from "utilities"
import {
resetSearchResultMeta,
setSearchResultMeta
} from "actions"
import { SearchResult } from "modules"
import { getElement } from "utilities"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
query$: Observable<string> /* Search query observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Paint hideable from source observable
* Paint search result metadata from source observable
*
* @param el - Hideable element
* @param offset - Additional offset
* @param el - Search result metadata element
* @param options - Options
*
* @return Operator function
*/
export function paintHidden(
el: HTMLElement, offset: number = 0
): OperatorFunction<ViewportOffset, boolean> {
export function paintSearchResultMeta(
el: HTMLElement, { query$ }: Options
): MonoTypeOperatorFunction<SearchResult[]> {
const meta = getElement(".md-search-result__meta", el)!
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)
withLatestFrom(query$),
map(([result, query]) => {
if (query) {
setSearchResultMeta(meta, result.length)
} else {
resetSearchResultMeta(meta)
}
return result
})
)
}

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 { Observable, OperatorFunction, pipe } from "rxjs"
import { map, shareReplay } from "rxjs/operators"
import { switchMapIf } from "extensions"
import { Agent, paintHidden } from "utilities"
import { Header, watchHeaderOffsetToTopOf } from "../header"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Tabs
*/
export interface Tabs {
hidden: boolean /* Whether the tabs are hidden */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
header$: Observable<Header> /* Header observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Setup tabs from source observable
*
* @param agent - Agent
* @param options - Options
*
* @return Operator function
*/
export function setupTabs(
agent: Agent, { header$ }: Options
): OperatorFunction<HTMLElement, Tabs> {
const { media } = agent
return pipe(
switchMapIf(media.screen$, el => {
/* Watch and paint visibility */
const hidden$ = watchHeaderOffsetToTopOf(el, agent, { header$ })
.pipe(
paintHidden(el, 8)
)
/* Combine into a single hot observable */
return hidden$
.pipe(
map(hidden => ({ hidden }))
)
}),
shareReplay(1)
)
}

View File

@@ -0,0 +1,106 @@
/*
* 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, OperatorFunction, combineLatest, pipe } from "rxjs"
import { map, shareReplay } from "rxjs/operators"
import { switchMapIf } from "extensions"
import { Agent, getElements } from "utilities"
import { Header } from "../../header"
import {
Main,
Sidebar,
paintSidebar,
watchSidebar
} from "../../main"
import {
AnchorList,
paintAnchorList,
watchAnchorList
} from "../anchor"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Table of contents
*/
export interface TableOfContents {
sidebar: Sidebar /* Sidebar */
anchors: AnchorList /* Anchor list */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
header$: Observable<Header> /* Header observable */
main$: Observable<Main> /* Main observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Setup table of contents from source observable
*
* @param agent - Agent
* @param options - Options
*
* @return Operator function
*/
export function setupTableOfContents(
agent: Agent, { header$, main$ }: Options
): OperatorFunction<HTMLElement, TableOfContents> {
const { media } = agent
return pipe(
switchMapIf(media.tablet$, el => {
/* Watch and paint sidebar */
const sidebar$ = watchSidebar(el, agent, { main$ })
.pipe(
paintSidebar(el)
)
/* Watch and paint anchor list (scroll spy) */
const els = getElements<HTMLAnchorElement>(".md-nav__link", el)
const anchors$ = watchAnchorList(els, agent, { header$ })
.pipe(
paintAnchorList(els)
)
/* Combine into a single hot observable */
return combineLatest([sidebar$, anchors$])
.pipe(
map(([sidebar, anchors]) => ({ sidebar, anchors }))
)
}),
shareReplay(1)
)
}

View File

@@ -45,13 +45,9 @@ import {
setAnchorActive,
setAnchorBlur
} from "actions"
import {
ViewportOffset,
ViewportSize,
getElement
} from "utilities"
import { Agent, getElement } from "utilities"
import { Header } from "../header"
import { Header } from "../../header"
/* ----------------------------------------------------------------------------
* Types
@@ -73,8 +69,6 @@ export interface AnchorList {
* Options
*/
interface Options {
size$: Observable<ViewportSize> /* Viewport size observable */
offset$: Observable<ViewportOffset> /* Viewport offset observable */
header$: Observable<Header> /* Header observable */
}
@@ -98,12 +92,13 @@ interface Options {
* Note that the current anchor is the last item of the `prev` anchor list.
*
* @param els - Anchor elements
* @param agent - Agent
* @param options - Options
*
* @return Anchor list observable
*/
export function watchAnchorList(
els: HTMLAnchorElement[], { size$, offset$, header$ }: Options
els: HTMLAnchorElement[], { viewport }: Agent, { header$ }: Options
): Observable<AnchorList> {
const table = new Map<HTMLAnchorElement, HTMLElement>()
for (const el of els) {
@@ -120,7 +115,7 @@ export function watchAnchorList(
)
/* Compute partition of previous and next anchors */
const partition$ = size$
const partition$ = viewport.size$
.pipe(
/* Build index to map anchor paths to vertical offsets */
@@ -143,7 +138,7 @@ export function watchAnchorList(
}),
/* Re-compute partition when viewport offset changes */
switchMap(index => combineLatest(offset$, adjust$)
switchMap(index => combineLatest(viewport.offset$, adjust$)
.pipe(
scan(([prev, next], [{ y }, adjust]) => {

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 "./_"
export * from "./anchor"