Refactored location observables

This commit is contained in:
squidfunk 2020-02-19 11:07:34 +01:00
parent 74b02ad382
commit ae05805124
16 changed files with 170 additions and 85 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.0e463231.min.js", "assets/javascripts/bundle.js": "assets/javascripts/bundle.4120b2e2.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.0e463231.min.js.map", "assets/javascripts/bundle.js.map": "assets/javascripts/bundle.4120b2e2.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.0e463231.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/bundle.4120b2e2.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

@ -148,10 +148,10 @@ export function useComponent<T extends HTMLElement>(
): Observable<T> { ): Observable<T> {
return components$ return components$
.pipe( .pipe(
switchMap(components => { switchMap(components => (
return typeof components[name] !== "undefined" typeof components[name] !== "undefined"
? of(components[name] as T) ? of(components[name] as T)
: NEVER : NEVER
}) ))
) )
} }

View File

@ -74,7 +74,7 @@ export function mountSearch(
switchMap(() => { switchMap(() => {
/* Mount search reset */ /* Mount search reset */
const reset$ = useComponent<HTMLInputElement>("search-reset") const reset$ = useComponent("search-reset")
.pipe( .pipe(
mountSearchReset() mountSearchReset()
) )

View File

@ -0,0 +1,56 @@
/*
* 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 { Subject, fromEvent } from "rxjs"
import { map } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Retrieve location
*
* @return Location
*/
export function getLocation(): string {
return location.href
}
/* ------------------------------------------------------------------------- */
/**
* Watch location
*
* @return Location subject
*/
export function watchLocation(): Subject<string> {
const location$ = new Subject<string>()
fromEvent<PopStateEvent>(window, "popstate")
.pipe(
map(getLocation)
)
.subscribe(location$)
/* Return subject */
return location$
}

View File

@ -0,0 +1,69 @@
/*
* 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, fromEvent } from "rxjs"
import { filter, map, share } from "rxjs/operators"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Retrieve location hash
*
* @return Location hash
*/
export function getLocationHash(): string {
return location.hash
}
/**
* Set location hash
*
* This will force a reset of the location hash, inducing an anchor jump if
* the hash matches the `id` of an element. It is implemented outside of the
* whole RxJS architecture using `setTimeout` to keep it plain and simple.
*
* @param value - Location hash
*/
export function setLocationHash(value: string): void {
location.hash = ""
setTimeout(() => {
location.hash = value
}, 1)
}
/* ------------------------------------------------------------------------- */
/**
* Watch location hash
*
* @return Location hash observable
*/
export function watchLocationHash(): Observable<string> {
return fromEvent<HashChangeEvent>(window, "hashchange")
.pipe(
map(getLocationHash),
filter(hash => hash.length > 0),
share()
)
}

View File

@ -20,40 +20,5 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, Subject, fromEvent } from "rxjs" export * from "./_"
import { filter, map, share } from "rxjs/operators" export * from "./hash"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch location
*
* @return Location subject
*/
export function watchLocation(): Subject<string> {
const location$ = new Subject<string>()
fromEvent<PopStateEvent>(window, "popstate")
.pipe(
map(() => location.href)
)
.subscribe(location$)
/* Return subject */
return location$
}
/**
* Watch location hash
*
* @return Location hash observable
*/
export function watchLocationHash(): Observable<string> {
return fromEvent<HashChangeEvent>(window, "hashchange")
.pipe(
map(() => location.hash),
filter(hash => hash.length > 0),
share()
)
}

View File

@ -52,7 +52,7 @@ export function getViewportOffset(): ViewportOffset {
} }
/** /**
* Retrieve viewport offset * Set viewport offset
* *
* @param offset - Viewport offset * @param offset - Viewport offset
*/ */

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
import { Observable, Subject, fromEvent, fromEventPattern } from "rxjs" import { Observable, Subject, fromEventPattern } from "rxjs"
import { import {
pluck, pluck,
share, share,

View File

@ -26,8 +26,7 @@ import {
distinctUntilChanged, distinctUntilChanged,
map, map,
shareReplay, shareReplay,
startWith, startWith
tap
} from "rxjs/operators" } from "rxjs/operators"
import { watchElementFocus } from "../../agent" import { watchElementFocus } from "../../agent"
@ -92,8 +91,9 @@ function defaultTransform(value: string): string {
* @return Search query observable * @return Search query observable
*/ */
export function watchSearchQuery( export function watchSearchQuery(
el: HTMLInputElement, { transform = defaultTransform }: WatchOptions = {} el: HTMLInputElement, { transform }: WatchOptions = {}
): Observable<SearchQuery> { ): Observable<SearchQuery> {
const fn = transform || defaultTransform
/* Intercept keyboard events */ /* Intercept keyboard events */
const value$ = merge( const value$ = merge(
@ -101,8 +101,8 @@ export function watchSearchQuery(
fromEvent(el, "focus").pipe(delay(1)) fromEvent(el, "focus").pipe(delay(1))
) )
.pipe( .pipe(
map(() => transform(el.value)), map(() => fn(el.value)),
startWith(transform(el.value)), startWith(fn(el.value)),
distinctUntilChanged() distinctUntilChanged()
) )

View File

@ -121,11 +121,11 @@ export function useToggle(
): Observable<HTMLInputElement> { ): Observable<HTMLInputElement> {
return toggles$ return toggles$
.pipe( .pipe(
switchMap(toggles => { switchMap(toggles => (
return typeof toggles[name] !== "undefined" typeof toggles[name] !== "undefined"
? of(toggles[name]!) ? of(toggles[name]!)
: NEVER : NEVER
}) ))
) )
} }

View File

@ -21,24 +21,22 @@
*/ */
import { identity } from "ramda" import { identity } from "ramda"
import { NEVER, Observable, fromEvent, merge, of } from "rxjs"
import { import {
Observable, catchError,
animationFrameScheduler,
fromEvent,
merge,
of
} from "rxjs"
import {
filter, filter,
map, map,
mapTo,
observeOn,
switchMap, switchMap,
switchMapTo, switchMapTo,
tap
} from "rxjs/operators" } from "rxjs/operators"
import { getElement, getElements, watchMedia } from "observables" import {
getElementOrThrow,
getElements,
getLocationHash,
setLocationHash,
watchMedia
} from "observables"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper types * Helper types
@ -86,27 +84,20 @@ export function patchDetails(
}) })
/* Open parent details before anchor jump */ /* Open parent details before anchor jump */
merge(hash$, of(location.hash)) merge(hash$, of(getLocationHash()))
.pipe( .pipe(
filter(hash => !!hash.length), filter(hash => !!hash.length),
switchMap(hash => of(getElement<HTMLElement>(hash) || document.body) switchMap(hash => of(getElementOrThrow<HTMLElement>(hash))
.pipe( .pipe(
map(el => el.closest("details")!), map(el => el.closest("details")!),
filter(el => el && !el.open), filter(el => el && !el.open),
map(el => [el, hash] as const)
/* Open details and temporarily reset anchor */
tap(el => {
el.setAttribute("open", "")
location.hash = ""
}),
/* Defer anchor jump to next animation frame */
observeOn(animationFrameScheduler),
mapTo(hash)
) )
) ),
catchError(() => NEVER)
) )
.subscribe(hash => { .subscribe(([el, hash]) => {
location.hash = hash el.setAttribute("open", "")
setLocationHash(hash)
}) })
} }

View File

@ -25,7 +25,11 @@ import { ajax } from "rxjs/ajax"
import { map, pluck } from "rxjs/operators" import { map, pluck } from "rxjs/operators"
import { SearchIndexOptions } from "integrations/search" import { SearchIndexOptions } from "integrations/search"
import { WorkerHandler, watchWorker } from "observables" import {
WorkerHandler,
getLocation,
watchWorker
} from "observables"
import { import {
SearchMessage, SearchMessage,
@ -83,7 +87,7 @@ export function setupSearchWorker(
url: string, { base, index }: SetupOptions url: string, { base, index }: SetupOptions
): WorkerHandler<SearchMessage> { ): WorkerHandler<SearchMessage> {
const worker = new Worker(url) const worker = new Worker(url)
const prefix = new URL(base, location.href) const prefix = new URL(base, getLocation())
/* Create communication channels and resolve relative links */ /* Create communication channels and resolve relative links */
const tx$ = new Subject<SearchMessage>() const tx$ = new Subject<SearchMessage>()