mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-24 06:43:50 -04:00
Refactored project structure
This commit is contained in:
@@ -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$
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -25,3 +25,4 @@ export * from "./focus"
|
||||
export * from "./offset"
|
||||
export * from "./selection"
|
||||
export * from "./size"
|
||||
export * from "./visibility"
|
||||
|
||||
77
src/assets/javascripts/browser/element/offset/_/index.ts
Normal file
77
src/assets/javascripts/browser/element/offset/_/index.ts
Normal 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))
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
138
src/assets/javascripts/browser/element/size/_/index.ts
Normal file
138
src/assets/javascripts/browser/element/size/_/index.ts
Normal 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))
|
||||
)
|
||||
}
|
||||
41
src/assets/javascripts/browser/element/size/content/index.ts
Normal file
41
src/assets/javascripts/browser/element/size/content/index.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
131
src/assets/javascripts/browser/element/visibility/index.ts
Normal file
131
src/assets/javascripts/browser/element/visibility/index.ts
Normal 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()
|
||||
)
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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]")
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user