Refactored header title component

This commit is contained in:
squidfunk 2020-02-17 16:25:49 +01:00
parent 7876148fbd
commit dd40bc2fcf
23 changed files with 308 additions and 126 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"assets/javascripts/bundle.js": "assets/javascripts/bundle.525b1d98.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.525b1d98.min.js.map",
"assets/javascripts/bundle.js": "assets/javascripts/bundle.a08f7eca.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.a08f7eca.min.js.map",
"assets/javascripts/worker/packer.js": "assets/javascripts/worker/packer.c14659e8.min.js",
"assets/javascripts/worker/packer.js.map": "assets/javascripts/worker/packer.c14659e8.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.0a5433f7.min.js",

View File

@ -190,7 +190,7 @@
{% endblock %}
</div>
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.525b1d98.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.a08f7eca.min.js' | url }}"></script>
<script id="__lang" type="application/json">
{%- set translations = {} -%}
{%- for key in [

View File

@ -0,0 +1,48 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set header shadow
*
* @param el - Header element
* @param value - Whether the shadow is shown
*/
export function setHeaderShadow(
el: HTMLElement, value: boolean
): void {
el.setAttribute("data-md-state", value ? "shadow" : "")
}
/**
* Reset header shadow
*
* @param el - Header element
*/
export function resetHeaderShadow(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@ -20,29 +20,5 @@
* IN THE SOFTWARE.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set header shadow
*
* @param el - Header element
* @param value - Whether the shadow is shown
*/
export function setHeaderShadow(
el: HTMLElement, value: boolean
): void {
el.setAttribute("data-md-state", value ? "shadow" : "")
}
/**
* Reset header shadow
*
* @param el - Header element
*/
export function resetHeaderShadow(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}
export * from "./_"
export * from "./title"

View File

@ -0,0 +1,48 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set header title
*
* @param el - Header title element
* @param value - Whether the title is shown
*/
export function setHeaderTitleActive(
el: HTMLElement, value: boolean
): void {
el.setAttribute("data-md-state", value ? "active" : "")
}
/**
* Reset header title
*
* @param el - Header element
*/
export function resetHeaderTitleActive(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@ -0,0 +1,42 @@
/*
* 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 { OperatorFunction, pipe } from "rxjs"
import { shareReplay, switchMap } from "rxjs/operators"
import { Header, watchHeader } from "observables"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Mount header from source observable
*
* @return Header observable
*/
export function mountHeader(): OperatorFunction<HTMLElement, Header> {
return pipe(
switchMap(watchHeader),
shareReplay(1)
)
}

View File

@ -20,23 +20,5 @@
* IN THE SOFTWARE.
*/
import { OperatorFunction, pipe } from "rxjs"
import { shareReplay, switchMap } from "rxjs/operators"
import { Header, watchHeader } from "observables"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Mount header from source observable
*
* @return Header observable
*/
export function mountHeader(): OperatorFunction<HTMLElement, Header> {
return pipe(
switchMap(watchHeader),
shareReplay(1)
)
}
export * from "./_"
export * from "./title"

View File

@ -0,0 +1,75 @@
/*
* 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 { map, switchMap } from "rxjs/operators"
import {
Header,
Viewport,
getElementOrThrow,
paintHeaderTitle,
watchViewportAt
} from "observables"
import { useComponent } from "../../_"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
header$: Observable<Header> /* Header observable */
viewport$: Observable<Viewport> /* Viewport observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Mount header title from source observable
*
* @param options - Options
*
* @return Header title observable
*/
export function mountHeaderTitle(
{ header$, viewport$ }: MountOptions
): OperatorFunction<HTMLElement, any> {
return pipe(
switchMap(el => useComponent("main")
.pipe(
map(main => getElementOrThrow("h1, h2, h3, h4, h5, h6", main)),
switchMap(headline => watchViewportAt(headline, { header$, viewport$ })
.pipe(
map(({ offset: { y } }) => y >= headline.offsetHeight),
paintHeaderTitle(el)
)
)
)
)
)
}

View File

@ -27,7 +27,7 @@ import {
Header,
Viewport,
paintHideable,
watchViewportFrom
watchViewportAt
} from "observables"
/* ----------------------------------------------------------------------------
@ -68,7 +68,7 @@ export function mountHero(
{ header$, viewport$ }: MountOptions
): OperatorFunction<HTMLElement, Hero> {
return pipe(
switchMap(el => watchViewportFrom(el, { header$, viewport$ })
switchMap(el => watchViewportAt(el, { header$, viewport$ })
.pipe(
paintHideable(el, 20),
map(hidden => ({ hidden }))

View File

@ -24,12 +24,7 @@ import { Observable, OperatorFunction, combineLatest, pipe } from "rxjs"
import { map, shareReplay, switchMap } from "rxjs/operators"
import { SearchResult } from "integrations/search"
import {
Key,
SearchQuery,
Viewport,
WorkerHandler
} from "observables"
import { Key, SearchQuery, WorkerHandler } from "observables"
import { SearchMessage } from "workers"
import { useComponent } from "../_"
@ -57,7 +52,6 @@ export interface Search {
* Mount options
*/
interface MountOptions {
viewport$: Observable<Viewport> /* Viewport observable */
keyboard$: Observable<Key> /* Keyboard observable */
}
@ -74,7 +68,7 @@ interface MountOptions {
* @return Search observable
*/
export function mountSearch(
handler: WorkerHandler<SearchMessage>, { viewport$, keyboard$ }: MountOptions
handler: WorkerHandler<SearchMessage>, { keyboard$ }: MountOptions
): OperatorFunction<HTMLElement, Search> {
return pipe(
switchMap(() => {
@ -88,14 +82,14 @@ export function mountSearch(
/* Mount search query */
const query$ = useComponent<HTMLInputElement>("search-query")
.pipe(
mountSearchQuery(handler, { reset$ }),
mountSearchQuery(handler),
shareReplay(1)
)
/* Mount search result */
const result$ = useComponent("search-result")
.pipe(
mountSearchResult(handler, { query$, viewport$, keyboard$ })
mountSearchResult(handler, { query$, keyboard$ })
)
/* Combine into a single hot observable */

View File

@ -37,7 +37,6 @@ import { SearchResult } from "integrations/search"
import {
Key,
SearchQuery,
Viewport,
WorkerHandler,
getActiveElement,
getElements,
@ -64,7 +63,6 @@ import { useComponent } from "../../_"
*/
interface MountOptions {
query$: Observable<SearchQuery> /* Search query observable */
viewport$: Observable<Viewport> /* Viewport observable */
keyboard$: Observable<Key> /* Keyboard observable */
}
@ -81,8 +79,7 @@ interface MountOptions {
* @return Operator function
*/
export function mountSearchResult(
{ rx$ }: WorkerHandler<SearchMessage>,
{ query$, viewport$, keyboard$ }: MountOptions
{ rx$ }: WorkerHandler<SearchMessage>, { query$, keyboard$ }: MountOptions
): OperatorFunction<HTMLElement, SearchResult[]> {
const toggle$ = useToggle("search")
return pipe(
@ -90,7 +87,7 @@ export function mountSearchResult(
const container = el.parentElement!
/* Compute whether there are more search results to fetch */
const fetch$ = watchElementOffset(container, { viewport$ })
const fetch$ = watchElementOffset(container)
.pipe(
map(({ y }) => {
return y >= container.scrollHeight - container.offsetHeight - 16

View File

@ -27,7 +27,7 @@ import {
Header,
Viewport,
paintHideable,
watchViewportFrom
watchViewportAt
} from "observables"
/* ----------------------------------------------------------------------------
@ -75,7 +75,7 @@ export function mountTabs(
/* Mount tabs above screen breakpoint */
if (screen) {
return watchViewportFrom(el, { header$, viewport$ })
return watchViewportAt(el, { header$, viewport$ })
.pipe(
paintHideable(el, 10),
map(hidden => ({ hidden }))

View File

@ -62,8 +62,7 @@ import {
watchKeyboard,
watchToggleMap,
useToggle,
setViewportOffset,
watchViewportFrom,
watchViewportAt,
getElementOrThrow
} from "./observables"
import { setupSearchWorker } from "./workers"
@ -80,7 +79,8 @@ import {
mountTableOfContents,
mountTabs,
useComponent,
watchComponentMap
watchComponentMap,
mountHeaderTitle
} from "components"
import { mountClipboard } from "./integrations/clipboard"
import { patchTables, patchDetails } from "patches"
@ -219,7 +219,7 @@ export function initialize(config: unknown) {
const search$ = useComponent("search")
.pipe(
mountSearch(worker, { viewport$, keyboard$ }),
mountSearch(worker, { keyboard$ }),
)
const navigation$ = useComponent("navigation")
@ -242,24 +242,11 @@ export function initialize(config: unknown) {
mountHero({ header$, viewport$ })
)
/* Create header title toggle */
useComponent("main")
// TODO: make this part of mountHeader!?
const title$ = useComponent("header-title")
.pipe(
map(el => getElementOrThrow("h1", el)), // catch error? just ignore?
switchMap(el => {
return watchViewportFrom(el, { header$, viewport$ })
.pipe(
map(({ offset: { y } }) => y >= el.offsetHeight),
withLatestFrom(useComponent("header-title")),
tap(([active, title]) => {
title.dataset.mdState = active ? "active" : ""
})
mountHeaderTitle({ header$, viewport$ })
)
})
)
.subscribe()
// paintHeaderTitle <- same with shadow...
/* ----------------------------------------------------------------------- */
@ -385,7 +372,8 @@ export function initialize(config: unknown) {
navigation$,
toc$,
tabs$,
hero$
hero$,
title$
}
const { ...rest } = state

View File

@ -52,7 +52,7 @@ interface WatchOptions {
*
* This function returns an observables that fetches a document if the provided
* location observable emits a new value (i.e. URL). If the emitted URL points
* to the same page, the request is effectively ignored (e.g. when only the
* to the same page, the request is effectively ignored (i.e. when only the
* fragment identifier changes).
*
* @param options - Options

View File

@ -21,14 +21,7 @@
*/
import { Observable, fromEvent, merge } from "rxjs"
import {
distinctUntilKeyChanged,
map,
shareReplay,
startWith
} from "rxjs/operators"
import { Viewport } from "../../viewport"
import { map, shareReplay, startWith } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Types
@ -42,17 +35,6 @@ export interface ElementOffset {
y: number /* Vertical offset */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Watch options
*/
interface WatchOptions {
viewport$: Observable<Viewport> /* Viewport observable */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
@ -77,21 +59,16 @@ export function getElementOffset(el: HTMLElement): ElementOffset {
* Watch element offset
*
* @param el - Element
* @param options - Options
*
* @return Element offset observable
*/
export function watchElementOffset(
el: HTMLElement, { viewport$ }: WatchOptions
el: HTMLElement
): Observable<ElementOffset> {
const scroll$ = fromEvent(el, "scroll")
const size$ = viewport$
.pipe(
distinctUntilKeyChanged("size")
return merge(
fromEvent<UIEvent>(el, "scroll"),
fromEvent<UIEvent>(window, "resize")
)
/* Merge into a single hot observable */
return merge(scroll$, size$)
.pipe(
map(() => getElementOffset(el)),
startWith(getElementOffset(el)),

View File

@ -36,8 +36,7 @@ export function watchLocation(): Subject<string> {
const location$ = new Subject<string>()
fromEvent<PopStateEvent>(window, "popstate")
.pipe(
map(() => location.href),
share()
map(() => location.href)
)
.subscribe(location$)

View File

@ -85,7 +85,7 @@ export function watchViewport(): Observable<Viewport> {
*
* @return Viewport observable
*/
export function watchViewportFrom(
export function watchViewportAt(
el: HTMLElement, { header$, viewport$ }: WatchRelativeOptions
): Observable<Viewport> {
return combineLatest([viewport$, header$])

View File

@ -22,3 +22,4 @@
export * from "./_"
export * from "./shadow"
export * from "./title"

View File

@ -0,0 +1,55 @@
/*
* 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 { animationFrameScheduler, pipe, MonoTypeOperatorFunction } from "rxjs"
import { finalize, observeOn, tap } from "rxjs/operators"
import { resetHeaderTitleActive, setHeaderTitleActive } from "actions"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Paint header title from source observable
*
* @param el - Header element
*
* @return Operator function
*/
export function paintHeaderTitle(
el: HTMLElement
): MonoTypeOperatorFunction<boolean> {
return pipe(
/* Defer repaint to next animation frame */
observeOn(animationFrameScheduler),
tap(active => {
setHeaderTitleActive(el, active)
}),
/* Reset on complete or error */
finalize(() => {
resetHeaderTitleActive(el)
})
)
}

View File

@ -85,7 +85,7 @@ export function setupSearchWorker(
const worker = new Worker(url)
const prefix = new URL(base, location.href)
/* Create communication channels and correct relative links */
/* Create communication channels and resolve relative links */
const tx$ = new Subject<SearchMessage>()
const rx$ = watchWorker(worker, { tx$ })
.pipe(