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

@@ -50,7 +50,7 @@ export function setSearchResultMeta(
export function resetSearchResultMeta(
el: HTMLElement
): void {
el.textContent = translate("search.placeholder")
el.textContent = translate("search.result.placeholder")
}
/* ------------------------------------------------------------------------- */

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

@@ -0,0 +1,71 @@
/*
* 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, pipe } from "rxjs"
import { map, withLatestFrom } from "rxjs/operators"
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 search result metadata from source observable
*
* @param el - Search result metadata element
* @param options - Options
*
* @return Operator function
*/
export function paintSearchResultMeta(
el: HTMLElement, { query$ }: Options
): MonoTypeOperatorFunction<SearchResult[]> {
const meta = getElement(".md-search-result__meta", el)!
return pipe(
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"

View File

@@ -20,16 +20,26 @@
* IN THE SOFTWARE.
*/
// TODO: remove this later on
// tslint:disable
import { identity } from "ramda"
import {
EMPTY,
MonoTypeOperatorFunction,
NEVER,
Observable,
Subject,
defer,
forkJoin,
fromEvent,
merge,
of,
pipe,
} from "rxjs"
import {
combineAll,
delay,
distinctUntilKeyChanged,
filter,
@@ -38,42 +48,44 @@ import {
shareReplay,
switchMap,
switchMapTo,
take,
tap,
} from "rxjs/operators"
import { ajax } from "rxjs/ajax"
import {} from "components"
import { AjaxResponse, ajax } from "rxjs/ajax"
import {
Component,
paintHeaderShadow,
paintHidden,
paintSidebar,
setupHero,
setupMain,
setupNavigation,
setupSearchResult,
switchComponent,
watchBottomOffset,
watchComponentMap,
watchHeader,
watchMain,
watchSearchReset,
watchSidebar,
watchTopOffset
} from "./components"
import {
not,
switchMapIf
} from "./extensions"
import { SearchIndex } from "./modules/search"
import { SearchIndex, SearchResult } from "./modules/search"
import {
getElement,
setupAgent,
watchDocument,
watchDocumentSwitch,
watchLocation,
watchLocationFragment,
watchLocationHash,
watchMedia,
watchToggle,
watchViewportOffset,
watchViewportSize,
watchWorker
} from "./utilities"
import { SearchMessage, SearchMessageType } from "./workers"
import {
SearchMessage,
SearchMessageType,
SearchSetupMessage,
isSearchDumpMessage,
isSearchResultMessage
} from "./workers"
/**
* Configuration
@@ -86,9 +98,13 @@ export interface Config {
}
}
import { PackerMessage, PackerMessageType } from "./workers/packer"
import {
PackerMessage,
PackerMessageType
} from "./workers/packer"
import { renderSearchResult } from "./templates"
import { setupTabs } from "components/tabs"
import { setupTableOfContents } from "components/toc/_"
/* ----------------------------------------------------------------------------
* Functions
@@ -109,7 +125,7 @@ export function isConfig(config: any): config is Config {
// TBD
// TODO: put this somewhere else... (merge with config!) JSON schema!?
const names = [
const names: Component[] = [
"header", /* Header */
"title", /* Header title */
"search", /* Search */
@@ -122,7 +138,7 @@ const names = [
"tabs", /* Tabs */
"navigation", /* Navigation */
"toc" /* Table of contents */
] as const
]
// modernizr for the poor
document.documentElement.classList.remove("no-js")
@@ -132,6 +148,19 @@ document.documentElement.classList.add("js")
* Functions
* ------------------------------------------------------------------------- */
/**
*
* Rogue control characters must be filtered before handing the query to the
* search index, as lunr will throw otherwise.
*/
function prepareQuery(value: string): string {
const newvalue = value
.replace(/(?:^|\s+)[*+-:^~]+(?=\s+|$)/g, "")
.trim()
return newvalue ? newvalue.replace(/\s+|$/g, "* ") : ""
}
/**
* Initialize Material for MkDocs
*
@@ -141,72 +170,113 @@ export function initialize(config: unknown) {
if (!isConfig(config))
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
const agent = setupAgent()
const worker = new Worker(config.worker.search)
const packer = new Worker(config.worker.packer)
// const query = message.data.trim().replace(/\s+|$/g, "* ") // TODO: do this outside of the worker
const packerMessage$ = new Subject<PackerMessage>()
const packer$ = watchWorker(packer, { message$: packerMessage$ })
const packer$ = watchWorker(packer, { send$: packerMessage$ })
// send a message, then switchMapTo worker!
packer$.subscribe(message => {
console.log("PACKER.MSG", message)
console.log("PACKER.MSG", message.data.length)
// is always packed!
console.log(message.data.length)
localStorage.setItem("index", message.data)
if (message.type === PackerMessageType.BINARY && message.data[0] !== "{")
localStorage.setItem("index", message.data)
})
// storing = experimental feature
const searchMessage$ = new Subject<SearchMessage>()
const search$ = watchWorker(worker, { message$: searchMessage$ })
search$.subscribe(message => {
if (message.type === SearchMessageType.DUMP) {
console.log(message.data.length)
packerMessage$.next({
const search$ = watchWorker(worker, { send$: searchMessage$ })
// paintSearchResult <-- must paint META AND LIST!
// list must be painted based on scroll offset...
/* Render search results */
// search$
// .pipe(
// filter(isSearchResultMessage),
// pluck("data")
// )
// .subscribe(result => {
// const list = getElement(".md-search-result__list")!
// list.innerHTML = ""
// for (const el of result.map(renderSearchResult)) // TODO: perform entire lazy render!!!!
// list.appendChild(el)
// })
// scroll!
// watchSearchResult
/* Link search to packer */
search$
.pipe(
filter(isSearchDumpMessage),
map(message => ({
type: PackerMessageType.STRING,
data: message.data
})
} else if (message.type === SearchMessageType.RESULT) {
console.log("RESULT", message)
})),
tap(message => packerMessage$.next(message)) // send message and wait!
// switchMapTo(packer$)
)
.subscribe()
const list = document.querySelector(".md-search-result__list")!
list.innerHTML = ""
for (const el of message.data.map(renderSearchResult)) // TODO: perform entire lazy render!!!!
list.appendChild(el as any) // only render visibile stuff...!
// paint on next animation frame!?
// build a rendering pipeline for search results + scroll bottom!
}
// if (message.type === 0) {
// console.log("Packing...")
// packerMessage$.next(message.toString())
// } else {
// console.log((message as any).term, ":", (message as any).res)
// }
})
// filter singular "+" or "-",as it will result in a lunr.js error
ajax({
const data$ = ajax({
url: `${config.base}/search/search_index.json`,
responseType: "json",
withCredentials: true
})
.pipe(
pluck("response"),
map<SearchIndex, SearchMessage>(data => ({
.pipe<SearchIndex>(
pluck("response")
// take(1)
)
const fromLocal = localStorage.getItem("index")
;
(fromLocal ? of({
type: PackerMessageType.BINARY,
data: localStorage.getItem("index")!
}) : EMPTY)
.subscribe(x => {
// console.log("send message to packer")
packerMessage$.next(x)
})
const index$ = fromLocal ? packer$.pipe(pluck("data"), take(1)) : of(undefined) // of(localStorage.getItem("index"))
// index$.subscribe(xx => console.log("INDEX", xx))
forkJoin([data$, index$])
.pipe<SearchSetupMessage>(
map(([data, index]) => ({
type: SearchMessageType.SETUP,
data
data: { ...data, index }
}))
)
.subscribe(message => {
searchMessage$.next(message) // TODO: this shall not complete
})
// filter singular "+" or "-",as it will result in a lunr.js error
// data$
// .pipe(
// map<SearchIndex, SearchMessage>(data => ({
// type: SearchMessageType.SETUP,
// data
// }))
// )
// .subscribe(message => {
// searchMessage$.next(message) // TODO: this shall not complete
// })
/* ----------------------------------------------------------------------- */
/* Create viewport observables */
@@ -219,10 +289,16 @@ export function initialize(config: unknown) {
/* Create location observables */
const location$ = watchLocation()
const fragment$ = watchLocationFragment()
const fragment$ = watchLocationHash()
/* Create document observables */
const load$ = watchDocument()
// Complete set of AgentObservables...
// component map!
//
// const switch$ = watchDocumentSwitch({ location$ })
/* ----------------------------------------------------------------------- */
@@ -230,44 +306,95 @@ export function initialize(config: unknown) {
/* Create component map observable */
const components$ = watchComponentMap(names, { document$: load$ })
const component = (name: Component): Observable<HTMLElement> => {
const component = <T extends HTMLElement>(name: Component): Observable<T> => {
return components$
.pipe(
switchComponent(name)
switchComponent<T>(name)
)
}
/* Create header observable */
const header$ = component("header")
const header$ = component("header") // TODO:!
.pipe(
switchMap(watchHeader)
)
/* Create main area observable */
// DONE
const main$ = component("main")
.pipe(
switchMap(el => watchMain(el, { size$, offset$, header$ })),
shareReplay(1)
setupMain(agent, { header$ })
)
// setupHeader(agent) ??
// setupSearch
// ----------------------------------------------------------------------------
component("query")
/* Create header shadow toggle */
component("header")
.pipe(
switchMap(el => fromEvent(el, "keyup") // not super nice...
switchMap(el => main$
.pipe(
map<Event, SearchMessage>(() => ({
type: SearchMessageType.QUERY,
data: (el as HTMLInputElement).value
})), // TODO. ugly...
distinctUntilKeyChanged("data")
paintHeaderShadow(el)
)
)
)
.subscribe()
// watchSearchResult // emit, if at bottom...
// receive results as a second observable!? filter stuff, paint
const result$ = search$
.pipe(
filter(isSearchResultMessage),
pluck("data")
)
const query$ = component<HTMLInputElement>("query")
.pipe(
switchMap(el => fromEvent(el, "keyup")
.pipe(
map(() => prepareQuery(el.value))
)
)
)
// DONE
component("result")
.pipe(
setupSearchResult(agent, { result$, query$ })
)
.subscribe()
query$
.pipe(
map(data => ({ // put this into some function...
type: SearchMessageType.QUERY,
data
})), // TODO. ugly...
distinctUntilKeyChanged("data")
)
.subscribe(x => {
searchMessage$.next(x)
searchMessage$.next(x as any) // TODO
})
// Focus on search input
component("query")
.pipe(
switchMap(el => fromEvent(el, "focus")
.pipe(
tap(() => {
if (!search.checked)
search.click() // move this inside the search query stuff? not important...
})
)
) // not super nice...
)
.subscribe()
// // WIP: instant loading
// load$
// .pipe(
@@ -310,62 +437,27 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */
/* Create header shadow toggle */
component("header")
.pipe(
switchMap(el => main$
.pipe(
paintHeaderShadow(el)
)
)
)
.subscribe()
/* Create sidebar with navigation */
component("navigation")
.pipe(
switchMapIf(screen$, el => watchSidebar(el, { offset$, main$ })
.pipe(
paintSidebar(el)
)
),
shareReplay(1)
setupNavigation(agent, { main$ })
)
.subscribe()
/* Create sidebar with table of contents */
component("toc")
.pipe(
switchMapIf(tablet$, el => watchSidebar(el, { offset$, main$ })
.pipe(
paintSidebar(el)
)
),
shareReplay(1)
setupTableOfContents(agent, { header$, main$ })
)
.subscribe()
/* Create tabs visibility toggle */
component("tabs")
.pipe(
switchMapIf(screen$, el => watchTopOffset(el, { size$, offset$, header$ })
.pipe(
paintHidden(el, 8)
)
),
shareReplay(1)
setupTabs(agent, { header$ })
)
.subscribe()
/* Create hero visibility toggle */
component("hero")
.pipe(
switchMap(el => watchTopOffset(el, { size$, offset$, header$ })
.pipe(
paintHidden(el, 20)
)
),
shareReplay(1)
setupHero(agent, { header$ })
)
.subscribe()
@@ -429,6 +521,20 @@ export function initialize(config: unknown) {
)
.subscribe()
/* Wrap all data tables for better overflow scrolling */
// const tables = getElements<HTMLTableElement>("table:not([class])")
// tables.forEach(table => {
// console.log("x", table)
// table.parentNode!.insertBefore(renderTable(table), table)
// table.replaceWith(renderTable(table) as any)
// // table.parentElement!.replaceChild(, table)
// })
return {
// agent, // agent.viewport.offset$
// component, // component.toc$
}
/* Return observable factories */
return {

View File

@@ -0,0 +1,113 @@
/*
* 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, Subject } from "rxjs"
import {
ViewportOffset,
ViewportSize,
watchDocument,
watchLocation,
watchLocationHash,
watchMedia,
watchViewportOffset,
watchViewportSize
} from "utilities"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Agent document
*/
export interface AgentDocument {
load$: Observable<Document> /* Document observable */
}
/**
* Agent location
*/
export interface AgentLocation {
href$: Subject<string> /* Location subject */
hash$: Observable<string> /* Location hash observable */
}
/**
* Agent media
*/
export interface AgentMedia {
screen$: Observable<boolean> /* Media observable for screen */
tablet$: Observable<boolean> /* Media observable for tablet */
}
/**
* Agent viewport
*/
export interface AgentViewport {
offset$: Observable<ViewportOffset> /* Viewport offset observable */
size$: Observable<ViewportSize> /* Viewport size observable */
}
/* ------------------------------------------------------------------------- */
/**
* Agent
*/
export interface Agent {
document: AgentDocument /* Document observables */
location: AgentLocation /* Location observables */
media: AgentMedia /* Media observables */
viewport: AgentViewport /* Viewport observables */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Create the agent
*
* This function returns a data structure that contains all observables that
* are related to the browser and/or environment.
*
* @return Agent
*/
export function setupAgent(): Agent {
return {
document: {
load$: watchDocument()
},
location: {
href$: watchLocation(),
hash$: watchLocationHash()
},
media: {
screen$: watchMedia("(min-width: 1220px)"),
tablet$: watchMedia("(min-width: 960px)")
},
viewport: {
offset$: watchViewportOffset(),
size$: watchViewportSize()
}
}
}

View File

@@ -23,6 +23,7 @@
import { Observable, fromEvent, merge } from "rxjs"
import { map, shareReplay, startWith } from "rxjs/operators"
import { Agent } from "../../_"
import { ViewportSize } from "../../viewport"
/* ----------------------------------------------------------------------------
@@ -71,16 +72,16 @@ export function getElementOffset(el: HTMLElement): ElementOffset {
/**
* Watch element offset
*
* @paramel - Element
* @param options - Options
* @param el - Element
* @param agent - Agent
*
* @return Element offset observable
*/
export function watchElementOffset(
el: HTMLElement, { size$ }: Options
el: HTMLElement, { viewport }: Agent
): Observable<ElementOffset> {
const scroll$ = fromEvent(el, "scroll")
return merge(scroll$, size$)
return merge(scroll$, viewport.size$)
.pipe(
map(() => getElementOffset(el)),
startWith(getElementOffset(el)),

View File

@@ -20,6 +20,7 @@
* IN THE SOFTWARE.
*/
export * from "./_"
export * from "./document"
export * from "./element"
export * from "./location"

View File

@@ -60,11 +60,11 @@ export function watchLocation(): Subject<string> {
}
/**
* Watch location fragment
* Watch location hash
*
* @return Location fragment observable
* @return Location hash observable
*/
export function watchLocationFragment(): Observable<string> {
export function watchLocationHash(): Observable<string> {
return hashchange$
.pipe(
map(() => location.hash),

View File

@@ -30,7 +30,8 @@ import {
} from "rxjs/operators"
import { resetHidden, setHidden } from "actions"
import { ViewportOffset } from "utilities"
import { ViewportOffset } from "../agent"
/* ----------------------------------------------------------------------------
* Functions

View File

@@ -21,5 +21,6 @@
*/
export * from "./agent"
export * from "./hidden"
export * from "./string"
export * from "./toggle"