Refactored main and header observables

This commit is contained in:
squidfunk 2020-02-13 18:29:44 +01:00
parent f4de690712
commit ca27f23674
9 changed files with 170 additions and 448 deletions

View File

@ -1,149 +0,0 @@
/*
* Copyright (c) 2016-2020 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 { keys } from "ramda"
import { NEVER, Observable, OperatorFunction, of, pipe } from "rxjs"
import { map, scan, shareReplay, switchMap } from "rxjs/operators"
import { getElement } from "observables"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Component names
*/
export type Component =
| "container" /* Container */
| "header" /* Header */
| "header-title" /* Header title */
| "hero" /* Hero */
| "main" /* Main area */
| "navigation" /* Navigation */
| "search" /* Search */
| "search-query" /* Search input */
| "search-reset" /* Search reset */
| "search-result" /* Search results */
| "tabs" /* Tabs */
| "toc" /* Table of contents */
/**
* Component map
*/
export type ComponentMap = {
[P in Component]?: HTMLElement
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Options
*/
interface Options {
document$: Observable<Document> /* Document observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* 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.
*
* @param names - Component names
* @param options - Options
*
* @return Component mapping observable
*/
export function watchComponentMap(
names: Component[], { document$ }: Options
): Observable<ComponentMap> {
const components$ = document$
.pipe(
/* Build component map */
map(document => names.reduce<ComponentMap>((components, name) => {
const el = getElement(`[data-md-component=${name}]`, document)
return {
...components,
...typeof el !== "undefined" ? { [name]: el } : {}
}
}, {})),
/* Re-compute component map on document switch */
scan((prev, next) => {
for (const name of keys(prev)) {
switch (name) {
/* Top-level components: update */
case "header-title":
case "container":
if (name in prev && typeof prev[name] !== "undefined") {
prev[name]!.replaceWith(next[name]!)
prev[name] = next[name]
}
break
/* All other components: rebind */
default:
prev[name] = getElement(`[data-md-component=${name}]`)
}
}
return prev
})
)
/* Return component map as hot observable */
return components$
.pipe(
shareReplay(1)
)
}
/* ------------------------------------------------------------------------- */
/**
* Switch to component
*
* @template T - Element type
*
* @param name - Component name
*
* @return Operator function
*/
export function switchComponent<T extends HTMLElement>(
name: Component
): OperatorFunction<ComponentMap, T> {
return pipe(
switchMap(components => {
return typeof components[name] !== "undefined"
? of(components[name] as T)
: NEVER
})
)
}

View File

@ -21,4 +21,5 @@
*/
export * from "./_"
export * from "./main"
export * from "./search"

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2016-2020 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 { shareReplay, switchMap } from "rxjs/operators"
import {
Header,
Main,
Viewport,
paintHeaderShadow,
watchMain
} from "observables"
import { useComponent } from "../_"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
header$: Observable<Header> /* Header observable */
viewport$: Observable<Viewport> /* Viewport observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Mount main area from source observable
*
* @param options - Options
*
* @return Main area observable
*/
export function mountMain(
{ header$, viewport$ }: MountOptions
): OperatorFunction<HTMLElement, Main> {
return pipe(
switchMap(el => useComponent("header")
.pipe(
switchMap(header => watchMain(el, { header$, viewport$ })
.pipe(
paintHeaderShadow(header)
)
)
)
),
shareReplay(1)
)
}

View File

@ -1,128 +0,0 @@
// /*
// * Copyright (c) 2016-2020 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, switchMap } from "rxjs/operators"
// import { switchMapIf } from "extensions"
// import { Agent, getElements } from "utilities"
// import { HeaderState } from "../../header"
// import {
// MainState,
// SidebarState,
// paintSidebar,
// watchSidebar
// } from "../../main"
// import {
// AnchorList,
// paintAnchorList,
// watchAnchorList
// } from "../anchor"
// /* ----------------------------------------------------------------------------
// * Types
// * ------------------------------------------------------------------------- */
// /**
// * Table of contents
// */
// export interface TableOfContents {
// sidebar: SidebarState /* Sidebar state */
// anchors: AnchorList /* Anchor list */
// }
// /* ----------------------------------------------------------------------------
// * Helper types
// * ------------------------------------------------------------------------- */
// /**
// * Options
// */
// interface Options {
// header$: Observable<Header> /* Header observable */
// main$: Observable<Main> /* Main area observable */
// }
// /* ----------------------------------------------------------------------------
// * Functions
// * ------------------------------------------------------------------------- */
// /**
// * Watch table of contents
// *
// * @param el - Table of contents element
// * @param agent - Agent
// * @param options - Options
// *
// * @return Table of contents observable
// */
// export function watchTableOfContents(
// el: HTMLElement, agent: Agent, { header$, main$ }: Options
// ): Observable<TableOfContents> {
// /* 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 }))
// )
// }
// /* ------------------------------------------------------------------------- */
// /**
// * Mount table of contents from source observable
// *
// * @param agent - Agent
// * @param options - Options
// *
// * @return Operator function
// */
// export function mountTableOfContents(
// agent: Agent, options: Options
// ): OperatorFunction<HTMLElement, TableOfContents> {
// const { media } = agent
// return pipe(
// switchMap(el => media.tablet$
// .pipe(
// switchMap(tablet => {
// return watchTableOfContents(el, agent, options)
// })
// )
// ),
// switchMapIf(media.tablet$, el => watchTableOfContents(el, agent, options)),
// shareReplay(1)
// )
// }

View File

@ -46,44 +46,42 @@ import {
switchMapTo,
tap,
distinctUntilKeyChanged,
shareReplay
shareReplay,
withLatestFrom
} from "rxjs/operators"
import {
Component,
paintHeaderShadow,
mountHero,
mountTableOfContents,
mountTabs,
switchComponent,
watchComponentMap,
} from "./components"
import {
watchHeader,
watchSearchQuery,
watchSearchReset,
getElement,
watchToggle,
setToggle,
getElements,
watchMedia,
watchDocument,
watchLocationHash,
watchMain,
watchViewport,
watchKeyboard
watchKeyboard,
watchToggleMap,
useToggle
} from "./observables"
import {
isSearchResultMessage,
setupSearchWorker
} from "./workers"
import { setupSearchWorker } from "./workers"
import { renderSource } from "templates"
import { not, takeIf } from "utilities"
import { renderClipboard } from "templates/clipboard"
import { fetchGitHubStats } from "modules/source/github"
import { mountNavigation } from "components2/navigation"
import { mountSearchResult } from "components2/search"
import { watchComponentMap, useComponent } from "components2/_"
import { renderTable } from "templates/table"
import { setToggle } from "actions"
import {
Component,
mountMain,
mountSearch
} from "components2"
/* ----------------------------------------------------------------------------
* Types
@ -111,6 +109,7 @@ document.documentElement.classList.add("js")
if (navigator.userAgent.match(/(iPad|iPhone|iPod)/g))
document.documentElement.classList.add("ios")
// add to config? default components to mount...?
const names: Component[] = [
"container", /* Container */
"header", /* Header */
@ -163,11 +162,14 @@ function repository() {
return of(x)
}
// TODO: do correct rounding, see GitHub
// TODO: do correct rounding, see GitHub - done
function format(value: number) {
return value > 999
? `${(value / 1000).toFixed(1)}k`
: `${(value)}`
if (value > 999) {
const digits = +((value - 950) % 1000 > 99)
return `${(++value / 1000).toFixed(digits)}k`
} else {
return value.toString()
}
}
// github repository...
@ -234,126 +236,63 @@ export function initialize(config: unknown) {
const viewport$ = watchViewport()
const screen$ = watchMedia("(min-width: 960px)")
const tablet$ = watchMedia("(min-width: 1220px)")
const key$ = watchKeyboard()
/* ----------------------------------------------------------------------- */
/* Create component map observable */
const components$ = watchComponentMap(names, { document$ })
const component = <T extends HTMLElement>(name: Component): Observable<T> => {
return components$
.pipe(
switchComponent<T>(name)
)
}
watchComponentMap(names, { document$ })
watchToggleMap(["drawer", "search"], { document$ })
/* Create header observable */
const header$ = component("header") // TODO:!
const header$ = useComponent("header")
.pipe(
switchMap(watchHeader)
switchMap(watchHeader) // TODO: should also be the mount...
)
/* Create header shadow toggle */
component("header")
const main$ = useComponent("main")
.pipe(
switchMap(el => main$
.pipe(
paintHeaderShadow(el) // technically, this could be done in paintMain
mountMain({ header$, viewport$ }),
)
)
)
.subscribe()
// DONE
const main$ = component("main")
.pipe(
switchMap(el => watchMain(el, { header$, viewport$ })),
shareReplay(1) // TODO: mount!?
)
// ---------------------------------------------------------------------------
/* ----------------------------------------------------------------------- */
const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")!
/* ----------------------------------------------------------------------- */
// build a single search observable???
const query$ = component<HTMLInputElement>("search-query")
.pipe(
switchMap(el => watchSearchQuery(el))
)
const sw = setupSearchWorker(config.worker.search, {
base: config.base,
query$
base: config.base
})
const result$ = sw.rx$ // move worker initialization into mountSearch ?
const search$ = useComponent("search")
.pipe(
filter(isSearchResultMessage),
pluck("data")
)
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
const searchActive$ = watchToggle(search)
.pipe(
delay(400)
)
query$
.pipe(
distinctUntilKeyChanged("focus"),
tap(query => {
if (query.focus)
setToggle(search, query.focus) // paintSearchQuery?
// console.log(query)
})
)
.subscribe()
// implement toggle function that returns the toggles as observable...
const reset$ = component("search-reset")
.pipe(
switchMap(watchSearchReset)
mountSearch(sw, { viewport$ }),
)
/* ----------------------------------------------------------------------- */
// DONE (partly)
const navigation$ = component("navigation")
const navigation$ = useComponent("navigation")
.pipe(
mountNavigation({ main$, viewport$, screen$ })
)
const toc$ = component("toc")
const toc$ = useComponent("toc")
.pipe(
mountTableOfContents({ header$, main$, viewport$, tablet$ })
)
// TODO: naming?
const resultComponent$ = component("search-result")
.pipe(
mountSearchResult({ viewport$, result$, query$: query$.pipe(
distinctUntilKeyChanged("value"),
) })
) // temporary fix
// mount hideable...
const tabs$ = component("tabs")
const tabs$ = useComponent("tabs")
.pipe(
mountTabs({ header$, viewport$, screen$ })
)
const hero$ = component("hero")
const hero$ = useComponent("hero")
.pipe(
mountHero({ header$, viewport$, screen$ })
)
// function watchKeyboard
const key$ = watchKeyboard()
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
const searchActive$ = useToggle("search").pipe(
switchMap(el => watchToggle(el)),
delay(400)
)
// shortcodes
key$
@ -420,16 +359,6 @@ export function initialize(config: unknown) {
// TODO: close search on hashchange
// anchor jump -> always close drawer + search
// focus search on reset, on toggle and on keypress if open
merge(searchActive$.pipe(filter(identity)), reset$)
.pipe(
switchMapTo(component<HTMLInputElement>("search-query")),
tap(el => el.focus()) // TODO: only if element isnt focused! setFocus? setToggle?
)
.subscribe()
// focusable -> setFocus(true, false)
/* ----------------------------------------------------------------------- */
/* Open details before printing */
@ -446,8 +375,14 @@ export function initialize(config: unknown) {
// Close drawer and search on hash change
hash$.subscribe(() => {
setToggle(drawer, false)
setToggle(search, false) // we probably need to delay the anchor jump for search
useToggle("drawer").subscribe(el => {
setToggle(el, false)
})
useToggle("search").subscribe(el => { // omit nested subscribes...
setToggle(el, false)
})
})
/* ----------------------------------------------------------------------- */
@ -547,16 +482,12 @@ export function initialize(config: unknown) {
// )
// .subscribe()
// // toiggle
// // toggle
/* ----------------------------------------------------------------------- */
const state = {
search: {
query$,
result$: resultComponent$,
reset$,
},
search$,
main$,
navigation$,
toc$,
@ -564,8 +495,8 @@ export function initialize(config: unknown) {
hero$
}
const { search: temp, ...rest } = state
merge(...values(rest), ...values(temp))
const { ...rest } = state
merge(...values(rest))
.subscribe() // potential memleak <-- use takeUntil
return {
@ -573,11 +504,3 @@ export function initialize(config: unknown) {
state
}
}
// function mountSearchQuery(
// ): OperatorFunction<HTMLInputElement, SearchQuery> {
// return pipe(
// switchMap(el => watchSearchQuery(el))
// )
// }

View File

@ -20,4 +20,41 @@
* IN THE SOFTWARE.
*/
export * from "./shadow"
import { Observable, of } from "rxjs"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Header
*/
export interface Header {
sticky: boolean /* Header stickyness */
height: number /* Header visible height */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch header
*
* The header is wrapped in an observable to pave the way for auto-hiding or
* other dynamic behaviors that may be implemented later on.
*
* @param el - Header element
*
* @return Header observable
*/
export function watchHeader(
el: HTMLElement
): Observable<Header> {
const styles = getComputedStyle(el)
const sticky = styles.position === "sticky"
return of({
sticky,
height: sticky ? el.offsetHeight : 0
})
}

View File

@ -20,41 +20,5 @@
* IN THE SOFTWARE.
*/
import { Observable, of } from "rxjs"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Header
*/
export interface Header {
sticky: boolean /* Header stickyness */
height: number /* Header visible height */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch header
*
* The header is wrapped in an observable to pave the way for auto-hiding or
* other dynamic behaviors that may be implemented later on.
*
* @param el - Header element
*
* @return Header observable
*/
export function watchHeader(
el: HTMLElement
): Observable<Header> {
const styles = getComputedStyle(el)
const sticky = styles.position === "sticky"
return of({
sticky,
height: sticky ? el.offsetHeight : 0
})
}
export * from "./_"
export * from "./shadow"

View File

@ -34,7 +34,7 @@ import {
import { resetHeaderShadow, setHeaderShadow } from "actions"
import { MainState } from "../../main"
import { Main } from "../../main"
/* ----------------------------------------------------------------------------
* Functions
@ -49,7 +49,7 @@ import { MainState } from "../../main"
*/
export function paintHeaderShadow(
el: HTMLElement
): MonoTypeOperatorFunction<MainState> {
): MonoTypeOperatorFunction<Main> {
return pipe(
distinctUntilKeyChanged("active"),

View File

@ -94,7 +94,7 @@ export function watchSearchQuery(
const value$ = fromEvent(el, "keyup")
.pipe(
map(() => transform(el.value)),
startWith(el.value),
startWith(transform(el.value)),
distinctUntilChanged()
)