diff --git a/src/assets/javascripts/components/_/index.ts b/src/assets/javascripts/components/_/index.ts deleted file mode 100644 index 7b0a1a0ed..000000000 --- a/src/assets/javascripts/components/_/index.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2016-2020 Martin Donath - * - * 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 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 { - const components$ = document$ - .pipe( - - /* Build component map */ - map(document => names.reduce((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( - name: Component -): OperatorFunction { - return pipe( - switchMap(components => { - return typeof components[name] !== "undefined" - ? of(components[name] as T) - : NEVER - }) - ) -} diff --git a/src/assets/javascripts/components2/index.ts b/src/assets/javascripts/components2/index.ts index a1b1b6576..941987f48 100644 --- a/src/assets/javascripts/components2/index.ts +++ b/src/assets/javascripts/components2/index.ts @@ -21,4 +21,5 @@ */ export * from "./_" +export * from "./main" export * from "./search" diff --git a/src/assets/javascripts/components2/main/index.ts b/src/assets/javascripts/components2/main/index.ts new file mode 100644 index 000000000..3e2d773f0 --- /dev/null +++ b/src/assets/javascripts/components2/main/index.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016-2020 Martin Donath + * + * 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 observable */ + viewport$: Observable /* Viewport observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Mount main area from source observable + * + * @param options - Options + * + * @return Main area observable + */ +export function mountMain( + { header$, viewport$ }: MountOptions +): OperatorFunction { + return pipe( + switchMap(el => useComponent("header") + .pipe( + switchMap(header => watchMain(el, { header$, viewport$ }) + .pipe( + paintHeaderShadow(header) + ) + ) + ) + ), + shareReplay(1) + ) +} diff --git a/src/assets/javascripts/components2/toc/index.ts b/src/assets/javascripts/components2/toc/index.ts deleted file mode 100644 index 9aba86980..000000000 --- a/src/assets/javascripts/components2/toc/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -// /* -// * Copyright (c) 2016-2020 Martin Donath -// * -// * 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 observable */ -// main$: Observable
/* 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 { - -// /* Watch and paint sidebar */ -// const sidebar$ = watchSidebar(el, agent, { main$ }) -// .pipe( -// paintSidebar(el) -// ) - -// /* Watch and paint anchor list (scroll spy) */ -// const els = getElements(".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 { -// 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) -// ) -// } diff --git a/src/assets/javascripts/index.ts b/src/assets/javascripts/index.ts index 0099f2837..c032b0d08 100644 --- a/src/assets/javascripts/index.ts +++ b/src/assets/javascripts/index.ts @@ -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 = (name: Component): Observable => { - return components$ - .pipe( - switchComponent(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("[data-md-toggle=drawer]")! - - /* ----------------------------------------------------------------------- */ - - // build a single search observable??? - - const query$ = component("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("[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("[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("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 { -// return pipe( -// switchMap(el => watchSearchQuery(el)) -// ) -// } diff --git a/src/assets/javascripts/components/header/index.ts b/src/assets/javascripts/observables/header/_/index.ts similarity index 53% rename from src/assets/javascripts/components/header/index.ts rename to src/assets/javascripts/observables/header/_/index.ts index f9b51db6a..1ff275bce 100644 --- a/src/assets/javascripts/components/header/index.ts +++ b/src/assets/javascripts/observables/header/_/index.ts @@ -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
{ + const styles = getComputedStyle(el) + const sticky = styles.position === "sticky" + return of({ + sticky, + height: sticky ? el.offsetHeight : 0 + }) +} diff --git a/src/assets/javascripts/observables/header/index.ts b/src/assets/javascripts/observables/header/index.ts index 1ff275bce..16cde2762 100644 --- a/src/assets/javascripts/observables/header/index.ts +++ b/src/assets/javascripts/observables/header/index.ts @@ -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
{ - const styles = getComputedStyle(el) - const sticky = styles.position === "sticky" - return of({ - sticky, - height: sticky ? el.offsetHeight : 0 - }) -} +export * from "./_" +export * from "./shadow" diff --git a/src/assets/javascripts/components/header/shadow/index.ts b/src/assets/javascripts/observables/header/shadow/index.ts similarity index 96% rename from src/assets/javascripts/components/header/shadow/index.ts rename to src/assets/javascripts/observables/header/shadow/index.ts index 0034e7485..63f47432c 100644 --- a/src/assets/javascripts/components/header/shadow/index.ts +++ b/src/assets/javascripts/observables/header/shadow/index.ts @@ -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 { +): MonoTypeOperatorFunction
{ return pipe( distinctUntilKeyChanged("active"), diff --git a/src/assets/javascripts/observables/search/query/index.ts b/src/assets/javascripts/observables/search/query/index.ts index 53ba973b0..6432194fc 100644 --- a/src/assets/javascripts/observables/search/query/index.ts +++ b/src/assets/javascripts/observables/search/query/index.ts @@ -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() )