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": "assets/javascripts/bundle.a08f7eca.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.525b1d98.min.js.map", "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": "assets/javascripts/worker/packer.c14659e8.min.js",
"assets/javascripts/worker/packer.js.map": "assets/javascripts/worker/packer.c14659e8.min.js.map", "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", "assets/javascripts/worker/search.js": "assets/javascripts/worker/search.0a5433f7.min.js",

View File

@ -190,7 +190,7 @@
{% endblock %} {% endblock %}
</div> </div>
{% block scripts %} {% 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"> <script id="__lang" type="application/json">
{%- set translations = {} -%} {%- set translations = {} -%}
{%- for key in [ {%- 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. * IN THE SOFTWARE.
*/ */
/* ---------------------------------------------------------------------------- export * from "./_"
* Functions export * from "./title"
* ------------------------------------------------------------------------- */
/**
* 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

@ -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. * IN THE SOFTWARE.
*/ */
import { OperatorFunction, pipe } from "rxjs" export * from "./_"
import { shareReplay, switchMap } from "rxjs/operators" export * from "./title"
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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,7 +52,7 @@ interface WatchOptions {
* *
* This function returns an observables that fetches a document if the provided * 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 * 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). * fragment identifier changes).
* *
* @param options - Options * @param options - Options

View File

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

View File

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

View File

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

View File

@ -22,3 +22,4 @@
export * from "./_" export * from "./_"
export * from "./shadow" 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 worker = new Worker(url)
const prefix = new URL(base, location.href) 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 tx$ = new Subject<SearchMessage>()
const rx$ = watchWorker(worker, { tx$ }) const rx$ = watchWorker(worker, { tx$ })
.pipe( .pipe(