diff options
| author | 2023-10-07 06:48:07 +0800 | |
|---|---|---|
| committer | 2023-10-07 06:48:07 +0800 | |
| commit | 991fd7a6d67ee017c57beaaa21fc31c4bee7944d (patch) | |
| tree | e895202203fcaa50b0052f60ef6fc7d6d2928cf9 /src/templates/assets/javascripts/browser | |
| parent | d62900046bb6f754a8e6e7e670a66a90134055d9 (diff) | |
| download | infini-991fd7a6d67ee017c57beaaa21fc31c4bee7944d.tar.gz infini-991fd7a6d67ee017c57beaaa21fc31c4bee7944d.zip | |
feat(version): versions
Diffstat (limited to 'src/templates/assets/javascripts/browser')
27 files changed, 2082 insertions, 0 deletions
diff --git a/src/templates/assets/javascripts/browser/document/index.ts b/src/templates/assets/javascripts/browser/document/index.ts new file mode 100644 index 00000000..354c9b5c --- /dev/null +++ b/src/templates/assets/javascripts/browser/document/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-2023 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 { + ReplaySubject, + Subject, + fromEvent +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch document + * + * Documents are implemented as subjects, so all downstream observables are + * automatically updated when a new document is emitted. + * + * @returns Document subject + */ +export function watchDocument(): Subject<Document> { + const document$ = new ReplaySubject<Document>(1) + fromEvent(document, "DOMContentLoaded", { once: true }) + .subscribe(() => document$.next(document)) + + /* Return document */ + return document$ +} diff --git a/src/templates/assets/javascripts/browser/element/_/.eslintrc b/src/templates/assets/javascripts/browser/element/_/.eslintrc new file mode 100644 index 00000000..16973760 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/_/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "jsdoc/require-jsdoc": "off", + "jsdoc/require-returns-check": "off" + } +} diff --git a/src/templates/assets/javascripts/browser/element/_/index.ts b/src/templates/assets/javascripts/browser/element/_/index.ts new file mode 100644 index 00000000..b7beb462 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/_/index.ts @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2016-2023 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 + * ------------------------------------------------------------------------- */ + +/** + * Retrieve all elements matching the query selector + * + * @template T - Element type + * + * @param selector - Query selector + * @param node - Node of reference + * + * @returns Elements + */ +export function getElements<T extends keyof HTMLElementTagNameMap>( + selector: T, node?: ParentNode +): HTMLElementTagNameMap[T][] + +export function getElements<T extends HTMLElement>( + selector: string, node?: ParentNode +): T[] + +export function getElements<T extends HTMLElement>( + selector: string, node: ParentNode = document +): T[] { + return Array.from(node.querySelectorAll<T>(selector)) +} + +/** + * Retrieve an element matching a query selector or throw a reference error + * + * Note that this function assumes that the element is present. If unsure if an + * element is existent, use the `getOptionalElement` function instead. + * + * @template T - Element type + * + * @param selector - Query selector + * @param node - Node of reference + * + * @returns Element + */ +export function getElement<T extends keyof HTMLElementTagNameMap>( + selector: T, node?: ParentNode +): HTMLElementTagNameMap[T] + +export function getElement<T extends HTMLElement>( + selector: string, node?: ParentNode +): T + +export function getElement<T extends HTMLElement>( + selector: string, node: ParentNode = document +): T { + const el = getOptionalElement<T>(selector, node) + if (typeof el === "undefined") + throw new ReferenceError( + `Missing element: expected "${selector}" to be present` + ) + + /* Return element */ + return el +} + +/* ------------------------------------------------------------------------- */ + +/** + * Retrieve an optional element matching the query selector + * + * @template T - Element type + * + * @param selector - Query selector + * @param node - Node of reference + * + * @returns Element or nothing + */ +export function getOptionalElement<T extends keyof HTMLElementTagNameMap>( + selector: T, node?: ParentNode +): HTMLElementTagNameMap[T] | undefined + +export function getOptionalElement<T extends HTMLElement>( + selector: string, node?: ParentNode +): T | undefined + +export function getOptionalElement<T extends HTMLElement>( + selector: string, node: ParentNode = document +): T | undefined { + return node.querySelector<T>(selector) || undefined +} + +/** + * Retrieve the currently active element + * + * @returns Element or nothing + */ +export function getActiveElement(): HTMLElement | undefined { + return document.activeElement instanceof HTMLElement + ? document.activeElement || undefined + : undefined +} diff --git a/src/templates/assets/javascripts/browser/element/focus/index.ts b/src/templates/assets/javascripts/browser/element/focus/index.ts new file mode 100644 index 00000000..f31fe276 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/focus/index.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-2023 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, + debounceTime, + distinctUntilChanged, + fromEvent, + map, + merge, + shareReplay, + startWith +} from "rxjs" + +import { getActiveElement } from "../_" + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Focus observable + * + * Previously, this observer used `focus` and `blur` events to determine whether + * an element is focused, but this doesn't work if there are focusable elements + * within the elements itself. A better solutions are `focusin` and `focusout` + * events, which bubble up the tree and allow for more fine-grained control. + * + * `debounceTime` is necessary, because when a focus change happens inside an + * element, the observable would first emit `false` and then `true` again. + */ +const observer$ = merge( + fromEvent(document.body, "focusin"), + fromEvent(document.body, "focusout") +) + .pipe( + debounceTime(1), + startWith(undefined), + map(() => getActiveElement() || document.body), + shareReplay(1) + ) + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch element focus + * + * @param el - Element + * + * @returns Element focus observable + */ +export function watchElementFocus( + el: HTMLElement +): Observable<boolean> { + return observer$ + .pipe( + map(active => el.contains(active)), + distinctUntilChanged() + ) +} diff --git a/src/templates/assets/javascripts/browser/element/index.ts b/src/templates/assets/javascripts/browser/element/index.ts new file mode 100644 index 00000000..50ce84b2 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-2023 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. + */ + +export * from "./_" +export * from "./focus" +export * from "./offset" +export * from "./size" +export * from "./visibility" diff --git a/src/templates/assets/javascripts/browser/element/offset/_/index.ts b/src/templates/assets/javascripts/browser/element/offset/_/index.ts new file mode 100644 index 00000000..6dd229d5 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/offset/_/index.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-2023 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, + animationFrameScheduler, + auditTime, + fromEvent, + map, + merge, + startWith +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Element offset + */ +export interface ElementOffset { + x: number /* Horizontal offset */ + y: number /* Vertical offset */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve element offset + * + * @param el - Element + * + * @returns Element offset + */ +export function getElementOffset( + el: HTMLElement +): ElementOffset { + return { + x: el.offsetLeft, + y: el.offsetTop + } +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch element offset + * + * @param el - Element + * + * @returns Element offset observable + */ +export function watchElementOffset( + el: HTMLElement +): Observable<ElementOffset> { + return merge( + fromEvent(window, "load"), + fromEvent(window, "resize") + ) + .pipe( + auditTime(0, animationFrameScheduler), + map(() => getElementOffset(el)), + startWith(getElementOffset(el)) + ) +} diff --git a/src/templates/assets/javascripts/browser/element/offset/content/index.ts b/src/templates/assets/javascripts/browser/element/offset/content/index.ts new file mode 100644 index 00000000..557301a6 --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/offset/content/index.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-2023 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, + animationFrameScheduler, + auditTime, + fromEvent, + map, + merge, + startWith +} from "rxjs" + +import { ElementOffset } from "../_" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve element content offset (= scroll offset) + * + * @param el - Element + * + * @returns Element content offset + */ +export function getElementContentOffset( + el: HTMLElement +): ElementOffset { + return { + x: el.scrollLeft, + y: el.scrollTop + } +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch element content offset + * + * @param el - Element + * + * @returns Element content offset observable + */ +export function watchElementContentOffset( + el: HTMLElement +): Observable<ElementOffset> { + return merge( + fromEvent(el, "scroll"), + fromEvent(window, "resize") + ) + .pipe( + auditTime(0, animationFrameScheduler), + map(() => getElementContentOffset(el)), + startWith(getElementContentOffset(el)) + ) +} diff --git a/src/templates/assets/javascripts/browser/element/offset/index.ts b/src/templates/assets/javascripts/browser/element/offset/index.ts new file mode 100644 index 00000000..602ff2cf --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/offset/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 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. + */ + +export * from "./_" +export * from "./content" diff --git a/src/templates/assets/javascripts/browser/element/size/_/index.ts b/src/templates/assets/javascripts/browser/element/size/_/index.ts new file mode 100644 index 00000000..35a5e68b --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/size/_/index.ts @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2016-2023 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 { + NEVER, + Observable, + Subject, + defer, + filter, + finalize, + map, + merge, + of, + shareReplay, + startWith, + switchMap, + tap +} from "rxjs" + +import { watchScript } from "../../../script" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Element offset + */ +export interface ElementSize { + width: number /* Element width */ + height: number /* Element height */ +} + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Resize observer entry subject + */ +const entry$ = new Subject<ResizeObserverEntry>() + +/** + * Resize observer observable + * + * This observable will create a `ResizeObserver` on the first subscription + * and will automatically terminate it when there are no more subscribers. + * It's quite important to centralize observation in a single `ResizeObserver`, + * as the performance difference can be quite dramatic, as the link shows. + * + * If the browser doesn't have a `ResizeObserver` implementation available, a + * polyfill is automatically downloaded from unpkg.com. This is also compatible + * with the built-in privacy plugin, which will download the polyfill and put + * it alongside the built site for self-hosting. + * + * @see https://bit.ly/3iIYfEm - Google Groups on performance + */ +const observer$ = defer(() => ( + typeof ResizeObserver === "undefined" + ? watchScript("https://unpkg.com/resize-observer-polyfill") + : of(undefined) +)) + .pipe( + map(() => new ResizeObserver(entries => { + for (const entry of entries) + entry$.next(entry) + })), + switchMap(observer => merge(NEVER, of(observer)) + .pipe( + finalize(() => observer.disconnect()) + ) + ), + shareReplay(1) + ) + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve element size + * + * @param el - Element + * + * @returns Element size + */ +export function getElementSize( + el: HTMLElement +): ElementSize { + return { + width: el.offsetWidth, + height: el.offsetHeight + } +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch element size + * + * This function returns an observable that subscribes to a single internal + * instance of `ResizeObserver` upon subscription, and emit resize events until + * termination. Note that this function should not be called with the same + * element twice, as the first unsubscription will terminate observation. + * + * Sadly, we can't use the `DOMRect` objects returned by the observer, because + * we need the emitted values to be consistent with `getElementSize`, which will + * return the used values (rounded) and not actual values (unrounded). Thus, we + * use the `offset*` properties. See the linked GitHub issue. + * + * @see https://bit.ly/3m0k3he - GitHub issue + * + * @param el - Element + * + * @returns Element size observable + */ +export function watchElementSize( + el: HTMLElement +): Observable<ElementSize> { + return observer$ + .pipe( + tap(observer => observer.observe(el)), + switchMap(observer => entry$ + .pipe( + filter(({ target }) => target === el), + finalize(() => observer.unobserve(el)), + map(() => getElementSize(el)) + ) + ), + startWith(getElementSize(el)) + ) +} diff --git a/src/templates/assets/javascripts/browser/element/size/content/index.ts b/src/templates/assets/javascripts/browser/element/size/content/index.ts new file mode 100644 index 00000000..5ed388cf --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/size/content/index.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-2023 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 { ElementSize } from "../_" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve element content size (= scroll width and height) + * + * @param el - Element + * + * @returns Element content size + */ +export function getElementContentSize( + el: HTMLElement +): ElementSize { + return { + width: el.scrollWidth, + height: el.scrollHeight + } +} + +/** + * Retrieve the overflowing container of an element, if any + * + * @param el - Element + * + * @returns Overflowing container or nothing + */ +export function getElementContainer( + el: HTMLElement +): HTMLElement | undefined { + let parent = el.parentElement + while (parent) + if ( + el.scrollWidth <= parent.scrollWidth && + el.scrollHeight <= parent.scrollHeight + ) + parent = (el = parent).parentElement + else + break + + /* Return overflowing container */ + return parent ? el : undefined +} diff --git a/src/templates/assets/javascripts/browser/element/size/index.ts b/src/templates/assets/javascripts/browser/element/size/index.ts new file mode 100644 index 00000000..602ff2cf --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/size/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 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. + */ + +export * from "./_" +export * from "./content" diff --git a/src/templates/assets/javascripts/browser/element/visibility/index.ts b/src/templates/assets/javascripts/browser/element/visibility/index.ts new file mode 100644 index 00000000..1ffe0b8d --- /dev/null +++ b/src/templates/assets/javascripts/browser/element/visibility/index.ts @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-2023 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 { + NEVER, + Observable, + Subject, + defer, + distinctUntilChanged, + filter, + finalize, + map, + merge, + of, + shareReplay, + switchMap, + tap +} from "rxjs" + +import { + getElementContentSize, + getElementSize, + watchElementContentOffset +} from "~/browser" + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Intersection observer entry subject + */ +const entry$ = new Subject<IntersectionObserverEntry>() + +/** + * Intersection observer observable + * + * This observable will create an `IntersectionObserver` on first subscription + * and will automatically terminate it when there are no more subscribers. + * + * @see https://bit.ly/3iIYfEm - Google Groups on performance + */ +const observer$ = defer(() => of( + new IntersectionObserver(entries => { + for (const entry of entries) + entry$.next(entry) + }, { + threshold: 0 + }) +)) + .pipe( + switchMap(observer => merge(NEVER, of(observer)) + .pipe( + finalize(() => observer.disconnect()) + ) + ), + shareReplay(1) + ) + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch element visibility + * + * @param el - Element + * + * @returns Element visibility observable + */ +export function watchElementVisibility( + el: HTMLElement +): Observable<boolean> { + return observer$ + .pipe( + tap(observer => observer.observe(el)), + switchMap(observer => entry$ + .pipe( + filter(({ target }) => target === el), + finalize(() => observer.unobserve(el)), + map(({ isIntersecting }) => isIntersecting) + ) + ) + ) +} + +/** + * Watch element boundary + * + * This function returns an observable which emits whether the bottom content + * boundary (= scroll offset) of an element is within a certain threshold. + * + * @param el - Element + * @param threshold - Threshold + * + * @returns Element boundary observable + */ +export function watchElementBoundary( + el: HTMLElement, threshold = 16 +): Observable<boolean> { + return watchElementContentOffset(el) + .pipe( + map(({ y }) => { + const visible = getElementSize(el) + const content = getElementContentSize(el) + return y >= ( + content.height - visible.height - threshold + ) + }), + distinctUntilChanged() + ) +} diff --git a/src/templates/assets/javascripts/browser/index.ts b/src/templates/assets/javascripts/browser/index.ts new file mode 100644 index 00000000..f1ee2bae --- /dev/null +++ b/src/templates/assets/javascripts/browser/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-2023 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. + */ + +export * from "./document" +export * from "./element" +export * from "./keyboard" +export * from "./location" +export * from "./media" +export * from "./request" +export * from "./script" +export * from "./toggle" +export * from "./viewport" +export * from "./worker" diff --git a/src/templates/assets/javascripts/browser/keyboard/index.ts b/src/templates/assets/javascripts/browser/keyboard/index.ts new file mode 100644 index 00000000..783f2cda --- /dev/null +++ b/src/templates/assets/javascripts/browser/keyboard/index.ts @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2016-2023 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 { + EMPTY, + Observable, + filter, + fromEvent, + map, + merge, + share, + startWith, + switchMap +} from "rxjs" + +import { getActiveElement } from "../element" +import { getToggle } from "../toggle" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Keyboard mode + */ +export type KeyboardMode = + | "global" /* Global */ + | "search" /* Search is open */ + +/* ------------------------------------------------------------------------- */ + +/** + * Keyboard + */ +export interface Keyboard { + mode: KeyboardMode /* Keyboard mode */ + type: string /* Key type */ + claim(): void /* Key claim */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Check whether an element may receive keyboard input + * + * @param el - Element + * @param type - Key type + * + * @returns Test result + */ +function isSusceptibleToKeyboard( + el: HTMLElement, type: string +): boolean { + switch (el.constructor) { + + /* Input elements */ + case HTMLInputElement: + /* @ts-expect-error - omit unnecessary type cast */ + if (el.type === "radio") + return /^Arrow/.test(type) + else + return true + + /* Select element and textarea */ + case HTMLSelectElement: + case HTMLTextAreaElement: + return true + + /* Everything else */ + default: + return el.isContentEditable + } +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch composition events + * + * @returns Composition observable + */ +export function watchComposition(): Observable<boolean> { + return merge( + fromEvent(window, "compositionstart").pipe(map(() => true)), + fromEvent(window, "compositionend").pipe(map(() => false)) + ) + .pipe( + startWith(false) + ) +} + +/** + * Watch keyboard + * + * @returns Keyboard observable + */ +export function watchKeyboard(): Observable<Keyboard> { + const keyboard$ = fromEvent<KeyboardEvent>(window, "keydown") + .pipe( + filter(ev => !(ev.metaKey || ev.ctrlKey)), + map(ev => ({ + mode: getToggle("search") ? "search" : "global", + type: ev.key, + claim() { + ev.preventDefault() + ev.stopPropagation() + } + } as Keyboard)), + filter(({ mode, type }) => { + if (mode === "global") { + const active = getActiveElement() + if (typeof active !== "undefined") + return !isSusceptibleToKeyboard(active, type) + } + return true + }), + share() + ) + + /* Don't emit during composition events - see https://bit.ly/3te3Wl8 */ + return watchComposition() + .pipe( + switchMap(active => !active ? keyboard$ : EMPTY) + ) +} diff --git a/src/templates/assets/javascripts/browser/location/_/index.ts b/src/templates/assets/javascripts/browser/location/_/index.ts new file mode 100644 index 00000000..2672fa74 --- /dev/null +++ b/src/templates/assets/javascripts/browser/location/_/index.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-2023 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 } from "rxjs" + +import { feature } from "~/_" +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve location + * + * This function returns a `URL` object (and not `Location`) to normalize the + * typings across the application. Furthermore, locations need to be tracked + * without setting them and `Location` is a singleton which represents the + * current location. + * + * @returns URL + */ +export function getLocation(): URL { + return new URL(location.href) +} + +/** + * Set location + * + * If instant navigation is enabled, this function creates a temporary anchor + * element, sets the `href` attribute, appends it to the body, clicks it, and + * then removes it again. The event will bubble up the DOM and trigger be + * intercepted by the instant loading business logic. + * + * Note that we must append and remove the anchor element, or the event will + * not bubble up the DOM, making it impossible to intercept it. + * + * @param url - URL to navigate to + * @param navigate - Force navigation + */ +export function setLocation( + url: URL | HTMLLinkElement, navigate = false +): void { + if (feature("navigation.instant") && !navigate) { + const el = h("a", { href: url.href }) + document.body.appendChild(el) + el.click() + el.remove() + + // If we're not using instant navigation, and the page should not be reloaded + // just instruct the browser to navigate to the given URL + } else { + location.href = url.href + } +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch location + * + * @returns Location subject + */ +export function watchLocation(): Subject<URL> { + return new Subject<URL>() +} diff --git a/src/templates/assets/javascripts/browser/location/hash/index.ts b/src/templates/assets/javascripts/browser/location/hash/index.ts new file mode 100644 index 00000000..5d3a134a --- /dev/null +++ b/src/templates/assets/javascripts/browser/location/hash/index.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-2023 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, + filter, + fromEvent, + map, + merge, + shareReplay, + startWith +} from "rxjs" + +import { getOptionalElement } from "~/browser" +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve location hash + * + * @returns Location hash + */ +export function getLocationHash(): string { + return location.hash.slice(1) +} + +/** + * Set location hash + * + * Setting a new fragment identifier via `location.hash` will have no effect + * if the value doesn't change. When a new fragment identifier is set, we want + * the browser to target the respective element at all times, which is why we + * use this dirty little trick. + * + * @param hash - Location hash + */ +export function setLocationHash(hash: string): void { + const el = h("a", { href: hash }) + el.addEventListener("click", ev => ev.stopPropagation()) + el.click() +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch location hash + * + * @param location$ - Location observable + * + * @returns Location hash observable + */ +export function watchLocationHash( + location$: Observable<URL> +): Observable<string> { + return merge( + fromEvent<HashChangeEvent>(window, "hashchange"), + location$ + ) + .pipe( + map(getLocationHash), + startWith(getLocationHash()), + filter(hash => hash.length > 0), + shareReplay(1) + ) +} + +/** + * Watch location target + * + * @param location$ - Location observable + * + * @returns Location target observable + */ +export function watchLocationTarget( + location$: Observable<URL> +): Observable<HTMLElement> { + return watchLocationHash(location$) + .pipe( + map(id => getOptionalElement(`[id="${id}"]`)!), + filter(el => typeof el !== "undefined") + ) +} diff --git a/src/templates/assets/javascripts/browser/location/index.ts b/src/templates/assets/javascripts/browser/location/index.ts new file mode 100644 index 00000000..d77a5444 --- /dev/null +++ b/src/templates/assets/javascripts/browser/location/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2016-2023 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. + */ + +export * from "./_" +export * from "./hash" diff --git a/src/templates/assets/javascripts/browser/media/index.ts b/src/templates/assets/javascripts/browser/media/index.ts new file mode 100644 index 00000000..dd7400d4 --- /dev/null +++ b/src/templates/assets/javascripts/browser/media/index.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-2023 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 { + EMPTY, + Observable, + fromEvent, + fromEventPattern, + map, + merge, + startWith, + switchMap +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch media query + * + * Note that although `MediaQueryList.addListener` is deprecated we have to + * use it, because it's the only way to ensure proper downward compatibility. + * + * @see https://bit.ly/3dUBH2m - GitHub issue + * + * @param query - Media query + * + * @returns Media observable + */ +export function watchMedia(query: string): Observable<boolean> { + const media = matchMedia(query) + return fromEventPattern<boolean>(next => ( + media.addListener(() => next(media.matches)) + )) + .pipe( + startWith(media.matches) + ) +} + +/** + * Watch print mode + * + * @returns Print observable + */ +export function watchPrint(): Observable<boolean> { + const media = matchMedia("print") + return merge( + fromEvent(window, "beforeprint").pipe(map(() => true)), + fromEvent(window, "afterprint").pipe(map(() => false)) + ) + .pipe( + startWith(media.matches) + ) +} + +/* ------------------------------------------------------------------------- */ + +/** + * Toggle an observable with a media observable + * + * @template T - Data type + * + * @param query$ - Media observable + * @param factory - Observable factory + * + * @returns Toggled observable + */ +export function at<T>( + query$: Observable<boolean>, factory: () => Observable<T> +): Observable<T> { + return query$ + .pipe( + switchMap(active => active ? factory() : EMPTY) + ) +} diff --git a/src/templates/assets/javascripts/browser/request/index.ts b/src/templates/assets/javascripts/browser/request/index.ts new file mode 100644 index 00000000..74a56a64 --- /dev/null +++ b/src/templates/assets/javascripts/browser/request/index.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-2023 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, + Subject, + map, + shareReplay, + switchMap +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Options + */ +interface Options { + progress$?: Subject<number> // Progress subject +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Fetch the given URL + * + * If the request fails (e.g. when dispatched from `file://` locations), the + * observable will complete without emitting a value. + * + * @param url - Request URL + * @param options - Options + * + * @returns Response observable + */ +export function request( + url: URL | string, options?: Options +): Observable<Blob> { + return new Observable<Blob>(observer => { + const req = new XMLHttpRequest() + req.open("GET", `${url}`) + req.responseType = "blob" + + // Handle response + req.addEventListener("load", () => { + if (req.status >= 200 && req.status < 300) { + observer.next(req.response) + observer.complete() + } else { + observer.error(new Error(req.statusText)) + } + }) + + // Handle network errors + req.addEventListener("error", () => { + observer.error(new Error("Network Error")) + }) + + // Handle aborted requests + req.addEventListener("abort", () => { + observer.error(new Error("Request aborted")) + }) + + // Handle download progress + if (typeof options?.progress$ !== "undefined") { + req.addEventListener("progress", event => { + options.progress$!.next((event.loaded / event.total) * 100) + }) + + // Immediately set progress to 5% to indicate that we're loading + options.progress$.next(5) + } + + // Send request + req.send() + }) +} + +/* ------------------------------------------------------------------------- */ + +/** + * Fetch JSON from the given URL + * + * @template T - Data type + * + * @param url - Request URL + * @param options - Options + * + * @returns Data observable + */ +export function requestJSON<T>( + url: URL | string, options?: Options +): Observable<T> { + return request(url, options) + .pipe( + switchMap(res => res.text()), + map(body => JSON.parse(body) as T), + shareReplay(1) + ) +} + +/** + * Fetch XML from the given URL + * + * @param url - Request URL + * @param options - Options + * + * @returns Data observable + */ +export function requestXML( + url: URL | string, options?: Options +): Observable<Document> { + const dom = new DOMParser() + return request(url, options) + .pipe( + switchMap(res => res.text()), + map(res => dom.parseFromString(res, "text/xml")), + shareReplay(1) + ) +} diff --git a/src/templates/assets/javascripts/browser/script/index.ts b/src/templates/assets/javascripts/browser/script/index.ts new file mode 100644 index 00000000..ef5c89e6 --- /dev/null +++ b/src/templates/assets/javascripts/browser/script/index.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-2023 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, + defer, + finalize, + fromEvent, + map, + merge, + switchMap, + take, + throwError +} from "rxjs" + +import { h } from "~/utilities" + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create and load a `script` element + * + * This function returns an observable that will emit when the script was + * successfully loaded, or throw an error if it wasn't. + * + * @param src - Script URL + * + * @returns Script observable + */ +export function watchScript(src: string): Observable<void> { + const script = h("script", { src }) + return defer(() => { + document.head.appendChild(script) + return merge( + fromEvent(script, "load"), + fromEvent(script, "error") + .pipe( + switchMap(() => ( + throwError(() => new ReferenceError(`Invalid script: ${src}`)) + )) + ) + ) + .pipe( + map(() => undefined), + finalize(() => document.head.removeChild(script)), + take(1) + ) + }) +} diff --git a/src/templates/assets/javascripts/browser/toggle/index.ts b/src/templates/assets/javascripts/browser/toggle/index.ts new file mode 100644 index 00000000..0be4b29d --- /dev/null +++ b/src/templates/assets/javascripts/browser/toggle/index.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-2023 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, + map, + startWith +} from "rxjs" + +import { getElement } from "../element" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Toggle + */ +export type Toggle = + | "drawer" /* Toggle for drawer */ + | "search" /* Toggle for search */ + +/* ---------------------------------------------------------------------------- + * Data + * ------------------------------------------------------------------------- */ + +/** + * Toggle map + */ +const toggles: Record<Toggle, HTMLInputElement> = { + drawer: getElement("[data-md-toggle=drawer]"), + search: getElement("[data-md-toggle=search]") +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve the value of a toggle + * + * @param name - Toggle + * + * @returns Toggle value + */ +export function getToggle(name: Toggle): boolean { + return toggles[name].checked +} + +/** + * Set toggle + * + * Simulating a click event seems to be the most cross-browser compatible way + * of changing the value while also emitting a `change` event. Before, Material + * used `CustomEvent` to programmatically change the value of a toggle, but this + * is a much simpler and cleaner solution which doesn't require a polyfill. + * + * @param name - Toggle + * @param value - Toggle value + */ +export function setToggle(name: Toggle, value: boolean): void { + if (toggles[name].checked !== value) + toggles[name].click() +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch toggle + * + * @param name - Toggle + * + * @returns Toggle value observable + */ +export function watchToggle(name: Toggle): Observable<boolean> { + const el = toggles[name] + return fromEvent(el, "change") + .pipe( + map(() => el.checked), + startWith(el.checked) + ) +} diff --git a/src/templates/assets/javascripts/browser/viewport/_/index.ts b/src/templates/assets/javascripts/browser/viewport/_/index.ts new file mode 100644 index 00000000..09c45f32 --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/_/index.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-2023 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, + combineLatest, + map, + shareReplay +} from "rxjs" + +import { + ViewportOffset, + watchViewportOffset +} from "../offset" +import { + ViewportSize, + watchViewportSize +} from "../size" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Viewport + */ +export interface Viewport { + offset: ViewportOffset /* Viewport offset */ + size: ViewportSize /* Viewport size */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch viewport + * + * @returns Viewport observable + */ +export function watchViewport(): Observable<Viewport> { + return combineLatest([ + watchViewportOffset(), + watchViewportSize() + ]) + .pipe( + map(([offset, size]) => ({ offset, size })), + shareReplay(1) + ) +} diff --git a/src/templates/assets/javascripts/browser/viewport/at/index.ts b/src/templates/assets/javascripts/browser/viewport/at/index.ts new file mode 100644 index 00000000..8769cf3b --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/at/index.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-2023 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, + combineLatest, + distinctUntilKeyChanged, + map +} from "rxjs" + +import { Header } from "~/components" + +import { getElementOffset } from "../../element" +import { Viewport } from "../_" + +/* ---------------------------------------------------------------------------- + * Helper types + * ------------------------------------------------------------------------- */ + +/** + * Watch options + */ +interface WatchOptions { + viewport$: Observable<Viewport> /* Viewport observable */ + header$: Observable<Header> /* Header observable */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Watch viewport relative to element + * + * @param el - Element + * @param options - Options + * + * @returns Viewport observable + */ +export function watchViewportAt( + el: HTMLElement, { viewport$, header$ }: WatchOptions +): Observable<Viewport> { + const size$ = viewport$ + .pipe( + distinctUntilKeyChanged("size") + ) + + /* Compute element offset */ + const offset$ = combineLatest([size$, header$]) + .pipe( + map(() => getElementOffset(el)) + ) + + /* Compute relative viewport, return hot observable */ + return combineLatest([header$, viewport$, offset$]) + .pipe( + map(([{ height }, { offset, size }, { x, y }]) => ({ + offset: { + x: offset.x - x, + y: offset.y - y + height + }, + size + })) + ) +} diff --git a/src/templates/assets/javascripts/browser/viewport/index.ts b/src/templates/assets/javascripts/browser/viewport/index.ts new file mode 100644 index 00000000..b3d135e9 --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-2023 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. + */ + +export * from "./_" +export * from "./at" +export * from "./offset" +export * from "./size" diff --git a/src/templates/assets/javascripts/browser/viewport/offset/index.ts b/src/templates/assets/javascripts/browser/viewport/offset/index.ts new file mode 100644 index 00000000..63d37dd2 --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/offset/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-2023 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, + map, + merge, + startWith +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Viewport offset + */ +export interface ViewportOffset { + x: number /* Horizontal offset */ + y: number /* Vertical offset */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve viewport offset + * + * On iOS Safari, viewport offset can be negative due to overflow scrolling. + * As this may induce strange behaviors downstream, we'll just limit it to 0. + * + * @returns Viewport offset + */ +export function getViewportOffset(): ViewportOffset { + return { + x: Math.max(0, scrollX), + y: Math.max(0, scrollY) + } +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch viewport offset + * + * @returns Viewport offset observable + */ +export function watchViewportOffset(): Observable<ViewportOffset> { + return merge( + fromEvent(window, "scroll", { passive: true }), + fromEvent(window, "resize", { passive: true }) + ) + .pipe( + map(getViewportOffset), + startWith(getViewportOffset()) + ) +} diff --git a/src/templates/assets/javascripts/browser/viewport/size/index.ts b/src/templates/assets/javascripts/browser/viewport/size/index.ts new file mode 100644 index 00000000..06694888 --- /dev/null +++ b/src/templates/assets/javascripts/browser/viewport/size/index.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-2023 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, + map, + startWith +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Viewport size + */ +export interface ViewportSize { + width: number /* Viewport width */ + height: number /* Viewport height */ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Retrieve viewport size + * + * @returns Viewport size + */ +export function getViewportSize(): ViewportSize { + return { + width: innerWidth, + height: innerHeight + } +} + +/* ------------------------------------------------------------------------- */ + +/** + * Watch viewport size + * + * @returns Viewport size observable + */ +export function watchViewportSize(): Observable<ViewportSize> { + return fromEvent(window, "resize", { passive: true }) + .pipe( + map(getViewportSize), + startWith(getViewportSize()) + ) +} diff --git a/src/templates/assets/javascripts/browser/worker/index.ts b/src/templates/assets/javascripts/browser/worker/index.ts new file mode 100644 index 00000000..12e4e63b --- /dev/null +++ b/src/templates/assets/javascripts/browser/worker/index.ts @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-2023 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, + Subject, + endWith, + fromEvent, + ignoreElements, + mergeWith, + share, + takeUntil +} from "rxjs" + +/* ---------------------------------------------------------------------------- + * Types + * ------------------------------------------------------------------------- */ + +/** + * Worker message + */ +export interface WorkerMessage { + type: unknown /* Message type */ + data?: unknown /* Message data */ +} + +/* ---------------------------------------------------------------------------- + * Helper functions + * ------------------------------------------------------------------------- */ + +/** + * Create an observable for receiving from a web worker + * + * @template T - Data type + * + * @param worker - Web worker + * + * @returns Message observable + */ +function recv<T>(worker: Worker): Observable<T> { + return fromEvent<MessageEvent<T>, T>(worker, "message", ev => ev.data) +} + +/** + * Create a subject for sending to a web worker + * + * @template T - Data type + * + * @param worker - Web worker + * + * @returns Message subject + */ +function send<T>(worker: Worker): Subject<T> { + const send$ = new Subject<T>() + send$.subscribe(data => worker.postMessage(data)) + + /* Return message subject */ + return send$ +} + +/* ---------------------------------------------------------------------------- + * Functions + * ------------------------------------------------------------------------- */ + +/** + * Create a bidirectional communication channel to a web worker + * + * @template T - Data type + * + * @param url - Worker URL + * @param worker - Worker + * + * @returns Worker subject + */ +export function watchWorker<T extends WorkerMessage>( + url: string, worker = new Worker(url) +): Subject<T> { + const recv$ = recv<T>(worker) + const send$ = send<T>(worker) + + /* Create worker subject and forward messages */ + const worker$ = new Subject<T>() + worker$.subscribe(send$) + + /* Return worker subject */ + const done$ = send$.pipe(ignoreElements(), endWith(true)) + return worker$ + .pipe( + ignoreElements(), + mergeWith(recv$.pipe(takeUntil(done$))), + share() + ) as Subject<T> +} |
