Refactored project structure

This commit is contained in:
squidfunk 2021-11-14 16:09:09 +01:00
parent ad1b964aaa
commit 88ba609ee1
52 changed files with 721 additions and 459 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -34,7 +34,7 @@
{% endif %}
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.b5f74394.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.b29cf17d.min.css' | url }}">
{% if config.theme.palette %}
{% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.9204c3b2.min.css' | url }}">
@ -211,7 +211,7 @@
</script>
{% endblock %}
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.0d86bc28.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.4fa4ff07.min.js' | url }}"></script>
{% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script>
{% endfor %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,5 +16,5 @@
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ 'overrides/assets/javascripts/bundle.2a83b894.min.js' | url }}"></script>
<script src="{{ 'overrides/assets/javascripts/bundle.5ee92cf1.min.js' | url }}"></script>
{% endblock %}

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { getElementOrThrow, getLocation } from "~/browser"
import { getElement, getLocation } from "~/browser"
/* ----------------------------------------------------------------------------
* Types
@ -99,7 +99,7 @@ export interface Config {
/**
* Retrieve global configuration and make base URL absolute
*/
const script = getElementOrThrow("#__config")
const script = getElement("#__config")
const config: Config = JSON.parse(script.textContent!)
config.base = `${new URL(config.base, getLocation())}`

View File

@ -23,8 +23,7 @@
import {
ReplaySubject,
Subject,
fromEvent,
mapTo
fromEvent
} from "rxjs"
/* ----------------------------------------------------------------------------
@ -40,12 +39,9 @@ import {
* @returns Document subject
*/
export function watchDocument(): Subject<Document> {
const document$ = new ReplaySubject<Document>()
fromEvent(document, "DOMContentLoaded")
.pipe(
mapTo(document)
)
.subscribe(document$)
const document$ = new ReplaySubject<Document>(1)
fromEvent(document, "DOMContentLoaded", { once: true })
.subscribe(() => document$.next(document))
/* Return document */
return document$

View File

@ -24,72 +24,6 @@
* Functions
* ------------------------------------------------------------------------- */
/**
* Retrieve an element matching the query selector
*
* @template T - Element type
*
* @param selector - Query selector
* @param node - Node of reference
*
* @returns Element or nothing
*/
export function getElement<T extends keyof HTMLElementTagNameMap>(
selector: T, node?: ParentNode
): HTMLElementTagNameMap[T] | undefined
export function getElement<T extends HTMLElement>(
selector: string, node?: ParentNode
): T | undefined
export function getElement<T extends HTMLElement>(
selector: string, node: ParentNode = document
): T | undefined {
return node.querySelector<T>(selector) || undefined
}
/**
* Retrieve an element matching a query selector or throw a reference error
*
* @template T - Element type
*
* @param selector - Query selector
* @param node - Node of reference
*
* @returns Element
*/
export function getElementOrThrow<T extends keyof HTMLElementTagNameMap>(
selector: T, node?: ParentNode
): HTMLElementTagNameMap[T]
export function getElementOrThrow<T extends HTMLElement>(
selector: string, node?: ParentNode
): T
export function getElementOrThrow<T extends HTMLElement>(
selector: string, node: ParentNode = document
): T {
const el = getElement<T>(selector, node)
if (typeof el === "undefined")
throw new ReferenceError(
`Missing element: expected "${selector}" to be present`
)
/* Return element */
return el
}
/**
* Retrieve the currently active element
*
* @returns Element or nothing
*/
export function getActiveElement(): HTMLElement | undefined {
return document.activeElement instanceof HTMLElement
? document.activeElement
: undefined
}
/**
* Retrieve all elements matching the query selector
*
@ -114,16 +48,73 @@ export function getElements<T extends HTMLElement>(
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
}
/* ------------------------------------------------------------------------- */
/**
* Replace an element with the given list of nodes
* Retrieve an optional element matching the query selector
*
* @param el - Element
* @param nodes - Replacement nodes
* @template T - Element type
*
* @param selector - Query selector
* @param node - Node of reference
*
* @returns Element or nothing
*/
export function replaceElement(
el: HTMLElement, ...nodes: Node[]
): void {
el.replaceWith(...nodes)
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
}

View File

@ -25,3 +25,4 @@ export * from "./focus"
export * from "./offset"
export * from "./selection"
export * from "./size"
export * from "./visibility"

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 2016-2021 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
* ------------------------------------------------------------------------- */
/**
* 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 fromEvent(window, "resize")
.pipe(
map(() => getElementOffset(el)),
startWith(getElementOffset(el))
)
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2016-2021 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"
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(
map(() => getElementContentOffset(el)),
startWith(getElementContentOffset(el))
)
}

View File

@ -20,95 +20,5 @@
* IN THE SOFTWARE.
*/
import {
Observable,
distinctUntilChanged,
fromEvent,
map,
merge,
startWith
} from "rxjs"
import {
getElementContentSize,
getElementSize
} from "../size"
/* ----------------------------------------------------------------------------
* 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.scrollLeft,
y: el.scrollTop
}
}
/* ------------------------------------------------------------------------- */
/**
* Watch element offset
*
* @param el - Element
*
* @returns Element offset observable
*/
export function watchElementOffset(
el: HTMLElement
): Observable<ElementOffset> {
return merge(
fromEvent(el, "scroll"),
fromEvent(window, "resize")
)
.pipe(
map(() => getElementOffset(el)),
startWith(getElementOffset(el))
)
}
/**
* Watch element threshold
*
* This function returns an observable which emits whether the bottom scroll
* offset of an elements is within a certain threshold.
*
* @param el - Element
* @param threshold - Threshold
*
* @returns Element threshold observable
*/
export function watchElementThreshold(
el: HTMLElement, threshold = 16
): Observable<boolean> {
return watchElementOffset(el)
.pipe(
map(({ y }) => {
const visible = getElementSize(el)
const content = getElementContentSize(el)
return y >= (
content.height - visible.height - threshold
)
}),
distinctUntilChanged()
)
}
export * from "./_"
export * from "./content"

View File

@ -0,0 +1,138 @@
/*
* Copyright (c) 2016-2021 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,
of,
shareReplay,
startWith,
switchMap,
tap
} from "rxjs"
/* ----------------------------------------------------------------------------
* 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.
*
* @see https://bit.ly/3iIYfEm - Google Groups on performance
*/
const observer$ = defer(() => of(
new ResizeObserver(entries => {
for (const entry of entries)
entry$.next(entry)
})
))
.pipe(
switchMap(observer => NEVER.pipe(startWith(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))
)
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) 2016-2021 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
}
}

View File

@ -20,133 +20,5 @@
* IN THE SOFTWARE.
*/
import {
NEVER,
Observable,
Subject,
defer,
filter,
finalize,
map,
of,
shareReplay,
startWith,
switchMap,
tap
} from "rxjs"
/* ----------------------------------------------------------------------------
* 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.
*
* @see https://bit.ly/3iIYfEm - Google Groups on performance
*/
const observer$ = defer(() => of(
new ResizeObserver(entries => {
for (const entry of entries)
entry$.next(entry)
})
))
.pipe(
switchMap(resize => NEVER.pipe(startWith(resize))
.pipe(
finalize(() => resize.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
}
}
/**
* Retrieve element content size, i.e. including overflowing content
*
* @param el - Element
*
* @returns Element size
*/
export function getElementContentSize(el: HTMLElement): ElementSize {
return {
width: el.scrollWidth,
height: el.scrollHeight
}
}
/* ------------------------------------------------------------------------- */
/**
* 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))
)
}
export * from "./_"
export * from "./content"

View File

@ -0,0 +1,131 @@
/*
* Copyright (c) 2016-2021 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,
of,
shareReplay,
startWith,
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: 1
})
))
.pipe(
switchMap(observer => NEVER.pipe(startWith(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 threshold 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()
)
}

View File

@ -29,7 +29,7 @@ import {
startWith
} from "rxjs"
import { getElement } from "~/browser"
import { getOptionalElement } from "~/browser"
import { h } from "~/utilities"
/* ----------------------------------------------------------------------------
@ -86,7 +86,7 @@ export function watchLocationHash(): Observable<string> {
export function watchLocationTarget(): Observable<HTMLElement> {
return watchLocationHash()
.pipe(
map(id => getElement(`[id="${id}"]`)!),
map(id => getOptionalElement(`[id="${id}"]`)!),
filter(el => typeof el !== "undefined")
)
}

View File

@ -27,7 +27,7 @@ import {
startWith
} from "rxjs"
import { getElementOrThrow } from "../element"
import { getElement } from "../element"
/* ----------------------------------------------------------------------------
* Types
@ -48,8 +48,8 @@ export type Toggle =
* Toggle map
*/
const toggles: Record<Toggle, HTMLInputElement> = {
drawer: getElementOrThrow("[data-md-toggle=drawer]"),
search: getElementOrThrow("[data-md-toggle=search]")
drawer: getElement("[data-md-toggle=drawer]"),
search: getElement("[data-md-toggle=search]")
}
/* ----------------------------------------------------------------------------

View File

@ -30,6 +30,7 @@ import {
import { Header } from "~/components"
import { getElementOffset } from "../../element"
import {
ViewportOffset,
watchViewportOffset
@ -102,10 +103,7 @@ export function watchViewportAt(
/* Compute element offset */
const offset$ = combineLatest([size$, header$])
.pipe(
map((): ViewportOffset => ({
x: el.offsetLeft,
y: el.offsetTop
}))
map(() => getElementOffset(el))
)
/* Compute relative viewport, return hot observable */

View File

@ -54,8 +54,8 @@ export interface ViewportOffset {
*/
export function getViewportOffset(): ViewportOffset {
return {
x: Math.max(0, pageXOffset),
y: Math.max(0, pageYOffset)
x: Math.max(0, scrollX),
y: Math.max(0, scrollY)
}
}

View File

@ -37,7 +37,7 @@ import {
import { configuration, feature } from "./_"
import {
at,
getElement,
getOptionalElement,
requestJSON,
setToggle,
watchDocument,
@ -96,6 +96,7 @@ const keyboard$ = watchKeyboard()
const viewport$ = watchViewport()
const tablet$ = watchMedia("(min-width: 960px)")
const screen$ = watchMedia("(min-width: 1220px)")
const hover$ = watchMedia("(hover)")
const print$ = watchPrint()
/* Retrieve search index, if search is enabled */
@ -139,7 +140,7 @@ keyboard$
/* Go to previous page */
case "p":
case ",":
const prev = getElement("[href][rel=prev]")
const prev = getOptionalElement("[href][rel=prev]")
if (typeof prev !== "undefined")
prev.click()
break
@ -147,7 +148,7 @@ keyboard$
/* Go to next page */
case "n":
case ".":
const next = getElement("[href][rel=next]")
const next = getOptionalElement("[href][rel=next]")
if (typeof next !== "undefined")
next.click()
break
@ -197,7 +198,7 @@ const content$ = defer(() => merge(
/* Content */
...getComponentElements("content")
.map(el => mountContent(el, { target$, viewport$, print$ })),
.map(el => mountContent(el, { target$, viewport$, hover$, print$ })),
/* Search highlighting */
...getComponentElements("content")
@ -250,8 +251,9 @@ window.location$ = location$ /* Location subject */
window.target$ = target$ /* Location target observable */
window.keyboard$ = keyboard$ /* Keyboard observable */
window.viewport$ = viewport$ /* Viewport observable */
window.tablet$ = tablet$ /* Tablet observable */
window.screen$ = screen$ /* Screen observable */
window.print$ = print$ /* Print observable */
window.tablet$ = tablet$ /* Media tablet observable */
window.screen$ = screen$ /* Media screen observable */
window.hover$ = hover$ /* Media hover observable */
window.print$ = print$ /* Media print observable */
window.alert$ = alert$ /* Alert subject */
window.component$ = component$ /* Component observable */

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { getElementOrThrow, getElements } from "~/browser"
import { getElement, getElements } from "~/browser"
/* ----------------------------------------------------------------------------
* Types
@ -114,7 +114,7 @@ interface ComponentTypeMap {
export function getComponentElement<T extends ComponentType>(
type: T, node: ParentNode = document
): ComponentTypeMap[T] {
return getElementOrThrow(`[data-md-component=${type}]`, node)
return getElement(`[data-md-component=${type}]`, node)
}
/**

View File

@ -53,7 +53,8 @@ export type Content =
interface MountOptions {
target$: Observable<HTMLElement> /* Location target observable */
viewport$: Observable<Viewport> /* Viewport observable */
print$: Observable<boolean> /* Print observable */
hover$: Observable<boolean> /* Media hover observable */
print$: Observable<boolean> /* Media print observable */
}
/* ----------------------------------------------------------------------------
@ -72,13 +73,13 @@ interface MountOptions {
* @returns Content component observable
*/
export function mountContent(
el: HTMLElement, { target$, viewport$, print$ }: MountOptions
el: HTMLElement, { target$, viewport$, hover$, print$ }: MountOptions
): Observable<Component<Content>> {
return merge(
/* Code blocks */
...getElements("pre > code", el)
.map(child => mountCodeBlock(child, { viewport$, print$ })),
.map(child => mountCodeBlock(child, { viewport$, hover$, print$ })),
/* Data tables */
...getElements("table:not([class])", el)

View File

@ -41,14 +41,17 @@ import {
} from "rxjs"
import { feature } from "~/_"
import { resetFocusable, setFocusable } from "~/actions"
import {
resetFocusable,
setFocusable
} from "~/actions"
import {
Viewport,
getElement,
getElementContentSize,
getElementOrThrow,
getElementSize,
getElements,
getOptionalElement,
watchMedia
} from "~/browser"
import {
@ -174,7 +177,7 @@ export function watchCodeBlock(
container.insertAdjacentElement("afterend", list)
for (const annotation of annotations) {
const id = parseInt(annotation.getAttribute("data-index")!, 10)
const typeset = getElement(":scope .md-typeset", annotation)!
const typeset = getOptionalElement(":scope .md-typeset", annotation)!
items[id - 1].append(...Array.from(typeset.childNodes))
}
} else {
@ -182,7 +185,7 @@ export function watchCodeBlock(
for (const annotation of annotations) {
const id = parseInt(annotation.getAttribute("data-index")!, 10)
const nodes = items[id - 1].childNodes
getElementOrThrow(":scope .md-typeset", annotation)
getElement(":scope .md-typeset", annotation)
.append(...Array.from(nodes))
}
}
@ -239,7 +242,7 @@ export function mountCodeBlock(
take(1),
takeWhile(({ annotations }) => !!annotations?.length),
map(({ annotations }) => annotations!
.map(annotation => getElementOrThrow(".md-tooltip", annotation))
.map(annotation => getElement(".md-tooltip", annotation))
),
combineLatestWith(viewport$
.pipe(

View File

@ -54,7 +54,7 @@ export interface Details {
*/
interface WatchOptions {
target$: Observable<HTMLElement> /* Location target observable */
print$: Observable<boolean> /* Print observable */
print$: Observable<boolean> /* Media print observable */
}
/**
@ -62,7 +62,7 @@ interface WatchOptions {
*/
interface MountOptions {
target$: Observable<HTMLElement> /* Location target observable */
print$: Observable<boolean> /* Print observable */
print$: Observable<boolean> /* Media print observable */
}
/* ----------------------------------------------------------------------------

View File

@ -22,7 +22,6 @@
import { Observable, of } from "rxjs"
import { replaceElement } from "~/browser"
import { renderTable } from "~/templates"
import { h } from "~/utilities"
@ -63,8 +62,8 @@ const sentinel = h("table")
export function mountDataTable(
el: HTMLElement
): Observable<Component<DataTable>> {
replaceElement(el, sentinel)
replaceElement(sentinel, renderTable(el))
el.replaceWith(sentinel)
sentinel.replaceWith(renderTable(el))
/* Create and return component */
return of({ ref: el })

View File

@ -21,7 +21,6 @@
*/
import {
NEVER,
Observable,
Subject,
finalize,
@ -33,7 +32,7 @@ import {
} from "rxjs"
import {
getElementOrThrow,
getElement,
getElements
} from "~/browser"
@ -64,15 +63,12 @@ export interface ContentTabs {
export function watchContentTabs(
el: HTMLElement
): Observable<ContentTabs> {
if (!el.classList.contains("tabbed-alternate"))
return NEVER
else
return merge(...getElements(":scope > input", el)
.map(input => fromEvent(input, "change").pipe(mapTo(input.id)))
)
.pipe(
map(id => ({
active: getElementOrThrow<HTMLLabelElement>(`label[for=${id}]`)
active: getElement<HTMLLabelElement>(`label[for=${id}]`)
}))
)
}

View File

@ -40,7 +40,10 @@ import {
} from "rxjs"
import { feature } from "~/_"
import { resetHeaderState, setHeaderState } from "~/actions"
import {
resetHeaderState,
setHeaderState
} from "~/actions"
import {
Viewport,
watchElementSize,

View File

@ -38,8 +38,8 @@ import {
} from "~/actions"
import {
Viewport,
getElement,
getElementSize,
getOptionalElement,
watchViewportAt
} from "~/browser"
@ -131,7 +131,7 @@ export function mountHeaderTitle(
})
/* Obtain headline, if any */
const headline = getElement<HTMLHeadingElement>("article h1")
const headline = getOptionalElement<HTMLHeadingElement>("article h1")
if (typeof headline === "undefined")
return NEVER

View File

@ -29,7 +29,10 @@ import {
switchMap
} from "rxjs"
import { Viewport, watchElementSize } from "~/browser"
import {
Viewport,
watchElementSize
} from "~/browser"
import { Header } from "../header"

View File

@ -53,10 +53,19 @@ import {
getComponentElement,
getComponentElements
} from "../../_"
import { SearchQuery, mountSearchQuery } from "../query"
import {
SearchQuery,
mountSearchQuery
} from "../query"
import { mountSearchResult } from "../result"
import { SearchShare, mountSearchShare } from "../share"
import { SearchSuggest, mountSearchSuggest } from "../suggest"
import {
SearchShare,
mountSearchShare
} from "../share"
import {
SearchSuggest,
mountSearchSuggest
} from "../suggest"
/* ----------------------------------------------------------------------------
* Types

View File

@ -46,8 +46,8 @@ import {
setSearchResultMeta
} from "~/actions"
import {
getElementOrThrow,
watchElementThreshold
getElement,
watchElementBoundary
} from "~/browser"
import {
SearchResult,
@ -91,14 +91,14 @@ export function mountSearchResult(
el: HTMLElement, { rx$ }: SearchWorker, { query$ }: MountOptions
): Observable<Component<SearchResult>> {
const internal$ = new Subject<SearchResult>()
const boundary$ = watchElementThreshold(el.parentElement!)
const boundary$ = watchElementBoundary(el.parentElement!)
.pipe(
filter(Boolean)
)
/* Retrieve nested components */
const meta = getElementOrThrow(":scope > :first-child", el)
const list = getElementOrThrow(":scope > :last-child", el)
const meta = getElement(":scope > :first-child", el)
const list = getElement(":scope > :last-child", el)
/* Wait until search is ready */
const ready$ = rx$

View File

@ -98,9 +98,10 @@ interface MountOptions {
export function watchSidebar(
el: HTMLElement, { viewport$, main$ }: WatchOptions
): Observable<Sidebar> {
const parent = el.parentElement!
const adjust =
el.parentElement!.offsetTop -
el.parentElement!.parentElement!.offsetTop
parent.offsetTop -
parent.parentElement!.offsetTop
/* Compute the sidebar's available height and if it should be locked */
return combineLatest([main$, viewport$])

View File

@ -34,11 +34,17 @@ import {
tap
} from "rxjs"
import { setSourceFacts, setSourceState } from "~/actions"
import {
setSourceFacts,
setSourceState
} from "~/actions"
import { renderSourceFacts } from "~/templates"
import { Component } from "../../_"
import { SourceFacts, fetchSourceFacts } from "../facts"
import {
SourceFacts,
fetchSourceFacts
} from "../facts"
/* ----------------------------------------------------------------------------
* Types

View File

@ -34,7 +34,10 @@ import {
} from "rxjs"
import { feature } from "~/_"
import { resetTabsState, setTabsState } from "~/actions"
import {
resetTabsState,
setTabsState
} from "~/actions"
import {
Viewport,
watchElementSize,

View File

@ -48,9 +48,9 @@ import {
} from "~/actions"
import {
Viewport,
getElement,
getElements,
getLocation,
getOptionalElement,
watchElementSize
} from "~/browser"
@ -122,7 +122,7 @@ export function watchTableOfContents(
const anchors = getElements<HTMLAnchorElement>("[href^=\\#]", el)
for (const anchor of anchors) {
const id = decodeURIComponent(anchor.hash.substring(1))
const target = getElement(`[id="${id}"]`)
const target = getOptionalElement(`[id="${id}"]`)
if (typeof target !== "undefined")
table.set(anchor, target)
}

View File

@ -43,7 +43,10 @@ import {
setBackToTopState,
setFocusable
} from "~/actions"
import { Viewport, setElementFocus } from "~/browser"
import {
Viewport,
setElementFocus
} from "~/browser"
import { Component } from "../_"
import { Header } from "../header"

View File

@ -25,7 +25,7 @@ import { Observable, Subject } from "rxjs"
import { translation } from "~/_"
import {
getElementOrThrow,
getElement,
getElements
} from "~/browser"
@ -85,7 +85,7 @@ export function setupClipboardJS(
new ClipboardJS("[data-clipboard-target], [data-clipboard-text]", {
text: el => (
el.getAttribute("data-clipboard-text")! ||
extract(getElementOrThrow(
extract(getElement(
el.getAttribute("data-clipboard-target")!
))
)

View File

@ -47,9 +47,8 @@ import { configuration, feature } from "~/_"
import {
Viewport,
ViewportOffset,
getElement,
getElements,
replaceElement,
getOptionalElement,
request,
requestXML,
setLocation,
@ -166,7 +165,7 @@ export function setupInstantLoading(
}
/* Hack: ensure absolute favicon link to omit 404s when switching */
const favicon = getElement<HTMLLinkElement>("link[rel=icon]")
const favicon = getOptionalElement<HTMLLinkElement>("link[rel=icon]")
if (typeof favicon !== "undefined")
favicon.href = favicon.href
@ -286,13 +285,13 @@ export function setupInstantLoading(
? ["[data-md-component=tabs]"]
: []
]) {
const source = getElement(selector)
const target = getElement(selector, replacement)
const source = getOptionalElement(selector)
const target = getOptionalElement(selector, replacement)
if (
typeof source !== "undefined" &&
typeof target !== "undefined"
) {
replaceElement(source, target)
source.replaceWith(target)
}
}
})
@ -308,7 +307,7 @@ export function setupInstantLoading(
if (el.src) {
for (const name of el.getAttributeNames())
script.setAttribute(name, el.getAttribute(name)!)
replaceElement(el, script)
el.replaceWith(script)
/* Complete when script is loaded */
return new Observable(observer => {
@ -318,7 +317,7 @@ export function setupInstantLoading(
/* Complete immediately */
} else {
script.textContent = el.textContent
replaceElement(el, script)
el.replaceWith(script)
return EMPTY
}
})

View File

@ -24,7 +24,7 @@ import { combineLatest, map } from "rxjs"
import { configuration } from "~/_"
import {
getElementOrThrow,
getElement,
requestJSON
} from "~/browser"
import { getComponentElements } from "~/components"
@ -60,7 +60,7 @@ export function setupVersionSelector(): void {
/* Render version selector and warning */
combineLatest([versions$, current$])
.subscribe(([versions, current]) => {
const topic = getElementOrThrow(".md-header__topic")
const topic = getElement(".md-header__topic")
topic.appendChild(renderVersionSelector(versions, current))
/* Check if version state was already determined */

View File

@ -43,7 +43,7 @@ import { getElements } from "~/browser"
*/
interface PatchOptions {
document$: Observable<Document> /* Document observable */
tablet$: Observable<boolean> /* Tablet breakpoint observable */
tablet$: Observable<boolean> /* Media tablet observable */
}
/* ----------------------------------------------------------------------------

View File

@ -32,8 +32,14 @@ import {
withLatestFrom
} from "rxjs"
import { resetScrollLock, setScrollLock } from "~/actions"
import { Viewport, watchToggle } from "~/browser"
import {
resetScrollLock,
setScrollLock
} from "~/actions"
import {
Viewport,
watchToggle
} from "~/browser"
/* ----------------------------------------------------------------------------
* Helper types
@ -44,7 +50,7 @@ import { Viewport, watchToggle } from "~/browser"
*/
interface PatchOptions {
viewport$: Observable<Viewport> /* Viewport observable */
tablet$: Observable<boolean> /* Tablet breakpoint observable */
tablet$: Observable<boolean> /* Media tablet observable */
}
/* ----------------------------------------------------------------------------

View File

@ -27,7 +27,7 @@ import { h } from "~/utilities"
* ------------------------------------------------------------------------- */
/**
* Render a 'copy-to-clipboard' button
* Render a a code annotation
*
* @param id - Unique identifier
* @param content - Annotation content

View File

@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { getElementOrThrow, getElements } from "~/browser"
import { getElement, getElements } from "~/browser"
/* ----------------------------------------------------------------------------
* Types
@ -84,7 +84,7 @@ interface ComponentTypeMap {
export function getComponentElement<T extends ComponentType>(
type: T, node: ParentNode = document
): ComponentTypeMap[T] {
return getElementOrThrow(`[data-mdx-component=${type}]`, node)
return getElement(`[data-mdx-component=${type}]`, node)
}
/**

View File

@ -47,8 +47,8 @@ import {
setSearchResultMeta
} from "~/actions"
import {
getElementOrThrow,
watchElementThreshold
getElement,
watchElementBoundary
} from "~/browser"
import { Icon, renderIconSearchResult } from "_/templates"
@ -147,13 +147,13 @@ export function mountIconSearchResult(
el: HTMLElement, { index$, query$ }: MountOptions
): Observable<Component<IconSearchResult, HTMLElement>> {
const internal$ = new Subject<IconSearchResult>()
const boundary$ = watchElementThreshold(el)
const boundary$ = watchElementBoundary(el)
.pipe(
filter(Boolean)
)
/* Update search result metadata */
const meta = getElementOrThrow(":scope > :first-child", el)
const meta = getElement(":scope > :first-child", el)
internal$
.pipe(
observeOn(animationFrameScheduler),
@ -167,7 +167,7 @@ export function mountIconSearchResult(
})
/* Update icon search result list */
const list = getElementOrThrow(":scope > :last-child", el)
const list = getElement(":scope > :last-child", el)
internal$
.pipe(
observeOn(animationFrameScheduler),

View File

@ -22,7 +22,7 @@
import { Observable, map } from "rxjs"
import { getElementOrThrow, requestJSON } from "~/browser"
import { getElement, requestJSON } from "~/browser"
import { renderPrivateSponsor, renderPublicSponsor } from "_/templates"
@ -121,7 +121,7 @@ export function mountSponsorship(
el.removeAttribute("hidden")
/* Render public sponsors with avatar and links */
const list = getElementOrThrow(":scope > :first-child", el)
const list = getElement(":scope > :first-child", el)
for (const sponsor of sponsorship.sponsors)
if (sponsor.type === "public")
list.appendChild(renderPublicSponsor(sponsor.user))

View File

@ -182,6 +182,7 @@ export function transformScript(
map: Buffer.from(data, "base64")
})
}),
catchError(() => NEVER),
switchMap(({ js, map }) => {
const file = digest(options.to, js)
return concat(

View File

@ -106,9 +106,10 @@ declare global {
var target$: Observable<HTMLElement> /* Location target observable */
var keyboard$: Observable<Keyboard> /* Keyboard observable */
var viewport$: Observable<Viewport> /* Viewport obsevable */
var tablet$: Observable<boolean> /* Tablet breakpoint observable */
var screen$: Observable<boolean> /* Screen breakpoint observable */
var print$: Observable<boolean> /* Print observable */
var tablet$: Observable<boolean> /* Media tablet observable */
var screen$: Observable<boolean> /* Media screen observable */
var hover$: Observable<boolean> /* Media hover observable */
var print$: Observable<boolean> /* Media print observable */
var alert$: Subject<string> /* Alert subject */
var component$: Observable<Component>/* Component observable */
}