Fixed observable completion semantics

This commit is contained in:
squidfunk
2021-11-28 14:54:14 +01:00
parent abe475e151
commit c30c3d196e
55 changed files with 493 additions and 1347 deletions

View File

@@ -1,78 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set focusable property
*
* @param el - Element
* @param value - Tabindex value
*/
export function setFocusable(
el: HTMLElement, value = 0
): void {
el.setAttribute("tabindex", value.toString())
}
/**
* Reset focusable property
*
* @param el - Element
*/
export function resetFocusable(
el: HTMLElement
): void {
el.removeAttribute("tabindex")
}
/* ------------------------------------------------------------------------- */
/**
* Set scroll lock
*
* @param el - Scrollable element
* @param value - Vertical offset
*/
export function setScrollLock(
el: HTMLElement, value: number
): void {
el.setAttribute("data-md-state", "lock")
el.style.top = `-${value}px`
}
/**
* Reset scroll lock
*
* @param el - Scrollable element
*/
export function resetScrollLock(
el: HTMLElement
): void {
const value = -1 * parseInt(el.style.top, 10)
el.removeAttribute("data-md-state")
el.style.top = ""
if (value)
window.scrollTo(0, value)
}

View File

@@ -1,73 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set anchor state
*
* @param el - Anchor element
* @param state - Anchor state
*/
export function setAnchorState(
el: HTMLElement, state: "blur"
): void {
el.setAttribute("data-md-state", state)
}
/**
* Reset anchor state
*
* @param el - Anchor element
*/
export function resetAnchorState(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}
/* ------------------------------------------------------------------------- */
/**
* Set anchor active
*
* @param el - Anchor element
* @param value - Whether the anchor is active
*/
export function setAnchorActive(
el: HTMLElement, value: boolean
): void {
el.classList.toggle("md-nav__link--active", value)
}
/**
* Reset anchor active
*
* @param el - Anchor element
*/
export function resetAnchorActive(
el: HTMLElement
): void {
el.classList.remove("md-nav__link--active")
}

View File

@@ -1,62 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set dialog message
*
* @param el - Dialog element
* @param value - Dialog message
*/
export function setDialogMessage(
el: HTMLElement, value: string
): void {
el.firstElementChild!.innerHTML = value
}
/* ------------------------------------------------------------------------- */
/**
* Set dialog state
*
* @param el - Dialog element
* @param state - Dialog state
*/
export function setDialogState(
el: HTMLElement, state: "open"
): void {
el.setAttribute("data-md-state", state)
}
/**
* Reset dialog state
*
* @param el - Dialog element
*/
export function resetDialogState(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@@ -1,48 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set header state
*
* @param el - Header element
* @param state - Header state
*/
export function setHeaderState(
el: HTMLElement, state: "shadow" | "hidden"
): void {
el.setAttribute("data-md-state", state)
}
/**
* Reset header state
*
* @param el - Header element
*/
export function resetHeaderState(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@@ -1,24 +0,0 @@
/*
* 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.
*/
export * from "./_"
export * from "./title"

View File

@@ -1,48 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set header title state
*
* @param el - Header title element
* @param state - Header title state
*/
export function setHeaderTitleState(
el: HTMLElement, state: "active"
): void {
el.setAttribute("data-md-state", state)
}
/**
* Reset header title state
*
* @param el - Header title element
*/
export function resetHeaderTitleState(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@@ -1,31 +0,0 @@
/*
* 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.
*/
export * from "./_"
export * from "./anchor"
export * from "./dialog"
export * from "./header"
export * from "./search"
export * from "./sidebar"
export * from "./source"
export * from "./tabs"
export * from "./top"

View File

@@ -1,24 +0,0 @@
/*
* 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.
*/
export * from "./query"
export * from "./result"

View File

@@ -1,50 +0,0 @@
/*
* 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 { translation } from "~/_"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set search query placeholder
*
* @param el - Search query element
* @param value - Placeholder
*/
export function setSearchQueryPlaceholder(
el: HTMLInputElement, value: string
): void {
el.placeholder = value
}
/**
* Reset search query placeholder
*
* @param el - Search query element
*/
export function resetSearchQueryPlaceholder(
el: HTMLInputElement
): void {
el.placeholder = translation("search.placeholder")
}

View File

@@ -1,91 +0,0 @@
/*
* 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 { translation } from "~/_"
import { round } from "~/utilities"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set number of search results
*
* @param el - Search result metadata element
* @param value - Number of results
*/
export function setSearchResultMeta(
el: HTMLElement, value: number
): void {
switch (value) {
/* No results */
case 0:
el.textContent = translation("search.result.none")
break
/* One result */
case 1:
el.textContent = translation("search.result.one")
break
/* Multiple result */
default:
el.textContent = translation("search.result.other", round(value))
}
}
/**
* Reset number of search results
*
* @param el - Search result metadata element
*/
export function resetSearchResultMeta(
el: HTMLElement
): void {
el.textContent = translation("search.result.placeholder")
}
/* ------------------------------------------------------------------------- */
/**
* Add an element to the search result list
*
* @param el - Search result list element
* @param child - Search result element
*/
export function addToSearchResultList(
el: HTMLElement, child: Element
): void {
el.appendChild(child)
}
/**
* Reset search result list
*
* @param el - Search result list element
*/
export function resetSearchResultList(
el: HTMLElement
): void {
el.innerHTML = ""
}

View File

@@ -1,88 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set sidebar offset
*
* @param el - Sidebar element
* @param value - Sidebar offset
*/
export function setSidebarOffset(
el: HTMLElement, value: number
): void {
el.style.top = `${value}px`
}
/**
* Reset sidebar offset
*
* @param el - Sidebar element
*/
export function resetSidebarOffset(
el: HTMLElement
): void {
el.style.top = ""
}
/* ------------------------------------------------------------------------- */
/**
* Set sidebar height
*
* This function doesn't set the height of the actual sidebar, but of its first
* child the `.md-sidebar__scrollwrap` element in order to mitigiate jittery
* sidebars when the footer is scrolled into view. At some point we switched
* from `absolute` / `fixed` positioning to `sticky` positioning, significantly
* reducing jitter in some browsers (respectively Firefox and Safari) when
* scrolling from the top. However, top-aligned sticky positioning means that
* the sidebar snaps to the bottom when the end of the container is reached.
* This is what leads to the mentioned jitter, as the sidebar's height may be
* updated too slowly.
*
* This behaviour can be mitigiated by setting the height of the sidebar to `0`
* while preserving the padding, and the height on its first element.
*
* @param el - Sidebar element
* @param value - Sidebar height
*/
export function setSidebarHeight(
el: HTMLElement, value: number
): void {
const scrollwrap = el.firstElementChild as HTMLElement
scrollwrap.style.height = `${value - 2 * scrollwrap.offsetTop}px`
}
/**
* Reset sidebar height
*
* @param el - Sidebar element
*/
export function resetSidebarHeight(
el: HTMLElement
): void {
const scrollwrap = el.firstElementChild as HTMLElement
scrollwrap.style.height = ""
}

View File

@@ -1,49 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set repository facts
*
* @param el - Repository element
* @param child - Repository facts element
*/
export function setSourceFacts(
el: HTMLElement, child: Element
): void {
el.lastElementChild!.appendChild(child)
}
/**
* Set repository state
*
* @param el - Repository element
* @param state - Repository state
*/
export function setSourceState(
el: HTMLElement, state: "done"
): void {
el.lastElementChild!.setAttribute("data-md-state", state)
}

View File

@@ -1,48 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set tabs state
*
* @param el - Tabs element
* @param state - Tabs state
*/
export function setTabsState(
el: HTMLElement, state: "hidden"
): void {
el.setAttribute("data-md-state", state)
}
/**
* Reset tabs state
*
* @param el - Tabs element
*/
export function resetTabsState(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}

View File

@@ -1,73 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set back-to-top state
*
* @param el - Back-to-top element
* @param state - Back-to-top state
*/
export function setBackToTopState(
el: HTMLElement, state: "hidden"
): void {
el.setAttribute("data-md-state", state)
}
/**
* Reset back-to-top state
*
* @param el - Back-to-top element
*/
export function resetBackToTopState(
el: HTMLElement
): void {
el.removeAttribute("data-md-state")
}
/* ------------------------------------------------------------------------- */
/**
* Set back-to-top offset
*
* @param el - Back-to-top element
* @param value - Back-to-top offset
*/
export function setBackToTopOffset(
el: HTMLElement, value: number
): void {
el.style.top = `${value}px`
}
/**
* Reset back-to-top offset
*
* @param el - Back-to-top element
*/
export function resetBackToTopOffset(
el: HTMLElement
): void {
el.style.top = ""
}

View File

@@ -34,23 +34,6 @@ import { getActiveElement } from "../_"
* Functions
* ------------------------------------------------------------------------- */
/**
* Set element focus
*
* @param el - Element
* @param value - Whether the element should be focused
*/
export function setElementFocus(
el: HTMLElement, value = true
): void {
if (value)
el.focus()
else
el.blur()
}
/* ------------------------------------------------------------------------- */
/**
* Watch element focus
*

View File

@@ -23,6 +23,5 @@
export * from "./_"
export * from "./focus"
export * from "./offset"
export * from "./selection"
export * from "./size"
export * from "./visibility"

View File

@@ -1,39 +0,0 @@
/*
* 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.
*/
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Set element text selection
*
* @param el - Element
*/
export function setElementSelection(
el: HTMLElement
): void {
if (el instanceof HTMLInputElement)
el.select()
else
throw new Error("Not implemented")
}

View File

@@ -32,7 +32,6 @@ import {
merge,
of,
shareReplay,
startWith,
switchMap,
tap
} from "rxjs"
@@ -113,7 +112,7 @@ export function watchElementVisibility(
* @param el - Element
* @param threshold - Threshold
*
* @returns Element threshold observable
* @returns Element boundary observable
*/
export function watchElementBoundary(
el: HTMLElement, threshold = 16

View File

@@ -21,7 +21,7 @@
*/
import {
NEVER,
EMPTY,
Observable,
fromEvent,
fromEventPattern,
@@ -86,6 +86,6 @@ export function at<T>(
): Observable<T> {
return query$
.pipe(
switchMap(active => active ? factory() : NEVER)
switchMap(active => active ? factory() : EMPTY)
)
}

View File

@@ -59,17 +59,6 @@ export function getViewportOffset(): ViewportOffset {
}
}
/**
* Set viewport offset
*
* @param offset - Viewport offset
*/
export function setViewportOffset(
{ x, y }: Partial<ViewportOffset>
): void {
window.scrollTo(x || 0, y || 0)
}
/* ------------------------------------------------------------------------- */
/**

View File

@@ -22,6 +22,7 @@
import "focus-visible"
import {
EMPTY,
NEVER,
Subject,
defer,
@@ -198,13 +199,13 @@ const content$ = defer(() => merge(
/* Content */
...getComponentElements("content")
.map(el => mountContent(el, { target$, viewport$, hover$, print$ })),
.map(el => mountContent(el, { target$, hover$, print$ })),
/* Search highlighting */
...getComponentElements("content")
.map(el => feature("search.highlight")
? mountSearchHiglight(el, { index$, location$ })
: NEVER
: EMPTY
),
/* Header title */

View File

@@ -36,6 +36,7 @@ import {
import {
ElementOffset,
getElement,
watchElementContentOffset,
watchElementFocus,
watchElementOffset
@@ -122,7 +123,7 @@ export function mountAnnotation(
})
/* Blur open annotation on click (= close) */
const index = el.lastElementChild!
const index = getElement(":scope > :last-child")
const blur$ = fromEvent(index, "mousedown", { once: true })
push$
.pipe(

View File

@@ -120,36 +120,36 @@ export function mountAnnotationList(
/* Create and return component */
return defer(() => {
const push$ = new Subject<Annotation>()
const done$ = new Subject<void>()
/* Handle print mode - see https://bit.ly/3rgPdpt */
print$
.pipe(
startWith(false),
takeUntil(push$.pipe(takeLast(1)))
takeUntil(done$.pipe(takeLast(1)))
)
.subscribe(active => {
el.hidden = !active
/* Move annotation contents back into list */
for (const [id, annotation] of annotations) {
const tooltip = getElement(".md-typeset", annotation)
const inner = getElement(".md-typeset", annotation)
const child = getElement(`li:nth-child(${id})`, el)
if (!active)
swap(child, tooltip)
swap(child, inner)
else
swap(tooltip, child)
swap(inner, child)
}
})
/* Create and return component */
return merge(
...[...annotations].map(([, annotation]) => (
return merge(...[...annotations]
.map(([, annotation]) => (
mountAnnotation(annotation, container)
))
)
.pipe(
finalize(() => push$.complete()),
finalize(() => done$.complete()),
share()
)
})

View File

@@ -23,6 +23,7 @@
import {
Observable,
Subject,
defer,
filter,
finalize,
map,
@@ -41,7 +42,7 @@ import { Component } from "../../_"
* Details
*/
export interface Details {
action: "open" | "close" /* Action */
action: "open" | "close" /* Details state */
scroll?: boolean /* Scroll into view */
}
@@ -117,21 +118,23 @@ export function watchDetails(
export function mountDetails(
el: HTMLDetailsElement, options: MountOptions
): Observable<Component<Details>> {
const internal$ = new Subject<Details>()
internal$.subscribe(({ action, scroll }) => {
if (action === "open")
el.setAttribute("open", "")
else
el.removeAttribute("open")
if (scroll)
el.scrollIntoView()
})
return defer(() => {
const push$ = new Subject<Details>()
push$.subscribe(({ action, scroll }) => {
if (action === "open")
el.setAttribute("open", "")
else
el.removeAttribute("open")
if (scroll)
el.scrollIntoView()
})
/* Create and return component */
return watchDetails(el, options)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
/* Create and return component */
return watchDetails(el, options)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -23,6 +23,7 @@
import {
Observable,
Subject,
defer,
finalize,
fromEvent,
map,
@@ -33,6 +34,7 @@ import {
import {
getElement,
getElementOffset,
getElements
} from "~/browser"
@@ -64,7 +66,11 @@ export function watchContentTabs(
el: HTMLElement
): Observable<ContentTabs> {
return merge(...getElements(":scope > input", el)
.map(input => fromEvent(input, "change").pipe(mapTo(input.id)))
.map(input => fromEvent(input, "change")
.pipe(
mapTo(input.id)
)
)
)
.pipe(
map(id => ({
@@ -76,6 +82,11 @@ export function watchContentTabs(
/**
* Mount content tabs
*
* This function scrolls the active tab into view. While this functionality is
* provided by browsers as part of `scrollInfoView`, browsers will always also
* scroll the vertical axis, which we do not want. Thus, we decided to provide
* this functionality ourselves.
*
* @param el - Content tabs element
*
* @returns Content tabs component observable
@@ -83,25 +94,23 @@ export function watchContentTabs(
export function mountContentTabs(
el: HTMLElement
): Observable<Component<ContentTabs>> {
const internal$ = new Subject<ContentTabs>()
internal$.subscribe(({ active }) => {
// TODO: Hack, scrollIntoView is too buggy
const container = active.parentElement!
if (
active.offsetLeft + active.offsetWidth > container.scrollLeft + container.offsetWidth ||
active.offsetLeft < container.scrollLeft
)
const container = getElement(".tabbed-labels", el)
return defer(() => {
const push$ = new Subject<ContentTabs>()
push$.subscribe(({ active }) => {
const { x } = getElementOffset(active)
container.scrollTo({
behavior: "smooth",
left: active.offsetLeft
left: x
})
})
})
/* Create and return component */
return watchContentTabs(el)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
/* Create and return component */
return watchContentTabs(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -23,22 +23,17 @@
import {
Observable,
Subject,
animationFrameScheduler,
defer,
delay,
finalize,
map,
merge,
observeOn,
of,
switchMap,
tap
} from "rxjs"
import {
resetDialogState,
setDialogMessage,
setDialogState
} from "~/actions"
import { getElement } from "~/browser"
import { Component } from "../_"
@@ -103,7 +98,7 @@ export function watchDialog(
/**
* Mount dialog
*
* This function reveals the dialog in the right cornerwhen a new alert is
* This function reveals the dialog in the right corner when a new alert is
* emitted through the subject that is passed as part of the options.
*
* @param el - Dialog element
@@ -114,24 +109,23 @@ export function watchDialog(
export function mountDialog(
el: HTMLElement, options: MountOptions
): Observable<Component<Dialog>> {
const internal$ = new Subject<Dialog>()
internal$
.pipe(
observeOn(animationFrameScheduler)
)
.subscribe(({ message, open }) => {
setDialogMessage(el, message)
if (open)
setDialogState(el, "open")
else
resetDialogState(el)
})
const inner = getElement(".md-typeset", el)
return defer(() => {
const push$ = new Subject<Dialog>()
push$.subscribe(({ message, open }) => {
inner.textContent = message
if (open)
el.setAttribute("data-md-state", "open")
else
el.removeAttribute("data-md-state")
})
/* Create and return component */
return watchDialog(el, options)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
/* Create and return component */
return watchDialog(el, options)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -23,7 +23,6 @@
import {
Observable,
Subject,
animationFrameScheduler,
bufferCount,
combineLatest,
combineLatestWith,
@@ -32,18 +31,15 @@ import {
distinctUntilKeyChanged,
filter,
map,
observeOn,
of,
shareReplay,
startWith,
switchMap
switchMap,
takeLast,
takeUntil
} from "rxjs"
import { feature } from "~/_"
import {
resetHeaderState,
setHeaderState
} from "~/actions"
import {
Viewport,
watchElementSize,
@@ -184,24 +180,28 @@ export function watchHeader(
export function mountHeader(
el: HTMLElement, { header$, main$ }: MountOptions
): Observable<Component<Header>> {
const internal$ = new Subject<Main>()
internal$
.pipe(
distinctUntilKeyChanged("active"),
combineLatestWith(header$),
observeOn(animationFrameScheduler)
)
.subscribe(([{ active }, { hidden }]) => {
if (active)
setHeaderState(el, hidden ? "hidden" : "shadow")
else
resetHeaderState(el)
})
return defer(() => {
const push$ = new Subject<Main>()
push$
.pipe(
distinctUntilKeyChanged("active"),
combineLatestWith(header$)
)
.subscribe(([{ active }, { hidden }]) => {
if (active)
el.setAttribute("data-md-state", hidden ? "hidden" : "shadow")
else
el.removeAttribute("data-md-state")
})
/* Connect to long-living subject and return component */
main$.subscribe(main => internal$.next(main))
return header$
.pipe(
map(state => ({ ref: el, ...state }))
)
/* Link to main area */
main$.subscribe(push$)
/* Create and return component */
return header$
.pipe(
takeUntil(push$.pipe(takeLast(1))),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -21,21 +21,16 @@
*/
import {
NEVER,
EMPTY,
Observable,
Subject,
animationFrameScheduler,
defer,
distinctUntilKeyChanged,
finalize,
map,
observeOn,
tap
} from "rxjs"
import {
resetHeaderTitleState,
setHeaderTitleState
} from "~/actions"
import {
Viewport,
getElementSize,
@@ -118,28 +113,26 @@ export function watchHeaderTitle(
export function mountHeaderTitle(
el: HTMLElement, options: MountOptions
): Observable<Component<HeaderTitle>> {
const internal$ = new Subject<HeaderTitle>()
internal$
.pipe(
observeOn(animationFrameScheduler)
)
.subscribe(({ active }) => {
if (active)
setHeaderTitleState(el, "active")
else
resetHeaderTitleState(el)
})
return defer(() => {
const push$ = new Subject<HeaderTitle>()
push$.subscribe(({ active }) => {
if (active)
el.setAttribute("data-md-state", "active")
else
el.removeAttribute("data-md-state")
})
/* Obtain headline, if any */
const headline = getOptionalElement<HTMLHeadingElement>("article h1")
if (typeof headline === "undefined")
return NEVER
/* Obtain headline, if any */
const heading = getOptionalElement<HTMLHeadingElement>("article h1")
if (typeof heading === "undefined")
return EMPTY
/* Create and return component */
return watchHeaderTitle(headline, options)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
/* Create and return component */
return watchHeaderTitle(heading, options)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -23,6 +23,7 @@
import {
Observable,
Subject,
defer,
finalize,
fromEvent,
map,
@@ -74,9 +75,9 @@ export function watchPalette(
inputs: HTMLInputElement[]
): Observable<Palette> {
const current = __md_get<Palette>("__palette") || {
index: inputs.findIndex(input => (
matchMedia(input.getAttribute("data-md-color-media")!).matches
))
index: inputs.findIndex(input => matchMedia(
input.getAttribute("data-md-color-media")!
).matches)
}
/* Emit changes in color palette */
@@ -99,11 +100,6 @@ export function watchPalette(
shareReplay(1)
)
/* Persist preference in local storage */
palette$.subscribe(palette => {
__md_set("__palette", palette)
})
/* Return palette */
return palette$
}
@@ -118,28 +114,33 @@ export function watchPalette(
export function mountPalette(
el: HTMLElement
): Observable<Component<Palette>> {
const internal$ = new Subject<Palette>()
return defer(() => {
const push$ = new Subject<Palette>()
push$.subscribe(palette => {
/* Set color palette */
internal$.subscribe(palette => {
for (const [key, value] of Object.entries(palette.color))
if (typeof value === "string")
document.body.setAttribute(`data-md-color-${key}`, value)
/* Set color palette */
for (const [key, value] of Object.entries(palette.color))
if (typeof value === "string")
document.body.setAttribute(`data-md-color-${key}`, value)
/* Toggle visibility */
for (let index = 0; index < inputs.length; index++) {
const label = inputs[index].nextElementSibling
if (label instanceof HTMLElement)
label.hidden = palette.index !== index
}
/* Toggle visibility */
for (let index = 0; index < inputs.length; index++) {
const label = inputs[index].nextElementSibling
if (label instanceof HTMLElement)
label.hidden = palette.index !== index
}
/* Persist preference in local storage */
__md_set("__palette", palette)
})
/* Create and return component */
const inputs = getElements<HTMLInputElement>("input", el)
return watchPalette(inputs)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
/* Create and return component */
const inputs = getElements<HTMLInputElement>("input", el)
return watchPalette(inputs)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -36,8 +36,6 @@ import {
Keyboard,
getActiveElement,
getElements,
setElementFocus,
setElementSelection,
setToggle
} from "~/browser"
import {
@@ -166,14 +164,14 @@ export function mountSearch(
case "Escape":
case "Tab":
setToggle("search", false)
setElementFocus(query, false)
query.blur()
break
/* Vertical arrows: select previous or next search result */
case "ArrowUp":
case "ArrowDown":
if (typeof active === "undefined") {
setElementFocus(query)
query.focus()
} else {
const els = [query, ...getElements(
":not(details) > [href], summary, details[open] [href]",
@@ -184,7 +182,7 @@ export function mountSearch(
key.type === "ArrowUp" ? -1 : +1
)
) % els.length)
setElementFocus(els[i])
els[i].focus()
}
/* Prevent scrolling of page */
@@ -194,7 +192,7 @@ export function mountSearch(
/* All other keys: hand to search query */
default:
if (query !== getActiveElement())
setElementFocus(query)
query.focus()
}
})
@@ -210,8 +208,10 @@ export function mountSearch(
case "f":
case "s":
case "/":
setElementFocus(query)
setElementSelection(query)
query.focus()
query.select()
/* Prevent scrolling of page */
key.claim()
break
}

View File

@@ -40,13 +40,9 @@ import {
tap
} from "rxjs"
import {
resetSearchQueryPlaceholder,
setSearchQueryPlaceholder
} from "~/actions"
import { translation } from "~/_"
import {
getLocation,
setElementFocus,
setToggle,
watchElementFocus
} from "~/browser"
@@ -143,10 +139,10 @@ export function watchSearchQuery(
export function mountSearchQuery(
el: HTMLInputElement, { tx$, rx$ }: SearchWorker
): Observable<Component<SearchQuery, HTMLInputElement>> {
const internal$ = new Subject<SearchQuery>()
const push$ = new Subject<SearchQuery>()
/* Handle value changes */
internal$
push$
.pipe(
distinctUntilKeyChanged("value"),
map(({ value }): SearchQueryMessage => ({
@@ -157,31 +153,31 @@ export function mountSearchQuery(
.subscribe(tx$.next.bind(tx$))
/* Handle focus changes */
internal$
push$
.pipe(
distinctUntilKeyChanged("focus")
)
.subscribe(({ focus }) => {
if (focus) {
setToggle("search", focus)
setSearchQueryPlaceholder(el, "")
el.placeholder = ""
} else {
resetSearchQueryPlaceholder(el)
el.placeholder = translation("search.placeholder")
}
})
/* Handle reset */
fromEvent(el.form!, "reset")
.pipe(
takeUntil(internal$.pipe(takeLast(1)))
takeUntil(push$.pipe(takeLast(1)))
)
.subscribe(() => setElementFocus(el))
.subscribe(() => el.focus())
/* Create and return component */
return watchSearchQuery(el, { tx$, rx$ })
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -39,12 +39,7 @@ import {
zipWith
} from "rxjs"
import {
addToSearchResultList,
resetSearchResultList,
resetSearchResultMeta,
setSearchResultMeta
} from "~/actions"
import { translation } from "~/_"
import {
getElement,
watchElementBoundary
@@ -56,6 +51,7 @@ import {
isSearchResultMessage
} from "~/integrations"
import { renderSearchResultItem } from "~/templates"
import { round } from "~/utilities"
import { Component } from "../../_"
import { SearchQuery } from "../query"
@@ -90,7 +86,7 @@ interface MountOptions {
export function mountSearchResult(
el: HTMLElement, { rx$ }: SearchWorker, { query$ }: MountOptions
): Observable<Component<SearchResult>> {
const internal$ = new Subject<SearchResult>()
const push$ = new Subject<SearchResult>()
const boundary$ = watchElementBoundary(el.parentElement!)
.pipe(
filter(Boolean)
@@ -108,24 +104,43 @@ export function mountSearchResult(
)
/* Update search result metadata */
internal$
push$
.pipe(
observeOn(animationFrameScheduler),
withLatestFrom(query$),
skipUntil(ready$)
)
.subscribe(([{ items }, { value }]) => {
if (value)
setSearchResultMeta(meta, items.length)
else
resetSearchResultMeta(meta)
if (value) {
switch (items.length) {
/* No results */
case 0:
meta.textContent = translation("search.result.none")
break
/* One result */
case 1:
meta.textContent = translation("search.result.one")
break
/* Multiple result */
default:
meta.textContent = translation(
"search.result.other",
round(items.length)
)
}
} else {
meta.textContent = translation("search.result.placeholder")
}
})
/* Update search result list */
internal$
push$
.pipe(
observeOn(animationFrameScheduler),
tap(() => resetSearchResultList(list)),
tap(() => list.innerHTML = ""),
switchMap(({ items }) => merge(
of(...items.slice(0, 10)),
of(...items.slice(10))
@@ -136,9 +151,9 @@ export function mountSearchResult(
)
))
)
.subscribe(result => {
addToSearchResultList(list, renderSearchResultItem(result))
})
.subscribe(result => list.appendChild(
renderSearchResultItem(result)
))
/* Filter search result message */
const result$ = rx$
@@ -150,8 +165,8 @@ export function mountSearchResult(
/* Create and return component */
return result$
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -101,8 +101,8 @@ export function watchSearchShare(
export function mountSearchShare(
el: HTMLAnchorElement, options: MountOptions
): Observable<Component<SearchShare>> {
const internal$ = new Subject<SearchShare>()
internal$.subscribe(({ url }) => {
const push$ = new Subject<SearchShare>()
push$.subscribe(({ url }) => {
el.setAttribute("data-clipboard-text", el.href)
el.href = `${url}`
})
@@ -114,8 +114,8 @@ export function mountSearchShare(
/* Create and return component */
return watchSearchShare(el, options)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -83,7 +83,7 @@ interface MountOptions {
export function mountSearchSuggest(
el: HTMLElement, { rx$ }: SearchWorker, { keyboard$ }: MountOptions
): Observable<Component<SearchSuggest>> {
const internal$ = new Subject<SearchResult>()
const push$ = new Subject<SearchResult>()
/* Retrieve query component and track all changes */
const query = getComponentElement("search-query")
@@ -98,7 +98,7 @@ export function mountSearchSuggest(
)
/* Update search suggestions */
internal$
push$
.pipe(
combineLatestWith(query$),
map(([{ suggestions }, value]) => {
@@ -147,8 +147,8 @@ export function mountSearchSuggest(
/* Create and return component */
return result$
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(() => ({ ref: el }))
)
}

View File

@@ -24,22 +24,21 @@ import {
Observable,
Subject,
animationFrameScheduler,
auditTime,
combineLatest,
defer,
distinctUntilChanged,
finalize,
map,
observeOn,
tap,
withLatestFrom
} from "rxjs"
import {
resetSidebarHeight,
resetSidebarOffset,
setSidebarHeight,
setSidebarOffset
} from "~/actions"
import { Viewport } from "~/browser"
Viewport,
getElement,
getElementOffset
} from "~/browser"
import { Component } from "../_"
import { Header } from "../header"
@@ -125,6 +124,19 @@ export function watchSidebar(
/**
* Mount sidebar
*
* This function doesn't set the height of the actual sidebar, but of its first
* child the `.md-sidebar__scrollwrap` element in order to mitigiate jittery
* sidebars when the footer is scrolled into view. At some point we switched
* from `absolute` / `fixed` positioning to `sticky` positioning, significantly
* reducing jitter in some browsers (respectively Firefox and Safari) when
* scrolling from the top. However, top-aligned sticky positioning means that
* the sidebar snaps to the bottom when the end of the container is reached.
* This is what leads to the mentioned jitter, as the sidebar's height may be
* updated too slowly.
*
* This behaviour can be mitigiated by setting the height of the sidebar to `0`
* while preserving the padding, and the height on its first element.
*
* @param el - Sidebar element
* @param options - Options
*
@@ -133,32 +145,36 @@ export function watchSidebar(
export function mountSidebar(
el: HTMLElement, { header$, ...options }: MountOptions
): Observable<Component<Sidebar>> {
const internal$ = new Subject<Sidebar>()
internal$
.pipe(
observeOn(animationFrameScheduler),
withLatestFrom(header$)
)
.subscribe({
const inner = getElement(".md-sidebar__scrollwrap", el)
const { y } = getElementOffset(inner)
return defer(() => {
const push$ = new Subject<Sidebar>()
push$
.pipe(
auditTime(0, animationFrameScheduler),
withLatestFrom(header$)
)
.subscribe({
/* Update height and offset */
next([{ height }, { height: offset }]) {
setSidebarHeight(el, height)
setSidebarOffset(el, offset)
},
/* Handle emission */
next([{ height }, { height: offset }]) {
inner.style.height = `${height - 2 * y}px`
el.style.top = `${offset}px`
},
/* Reset on complete */
complete() {
resetSidebarOffset(el)
resetSidebarHeight(el)
}
})
/* Handle complete */
complete() {
inner.style.height = ""
el.style.top = ""
}
})
/* Create and return component */
return watchSidebar(el, options)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
/* Create and return component */
return watchSidebar(el, options)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -21,7 +21,7 @@
*/
import {
NEVER,
EMPTY,
Observable,
Subject,
catchError,
@@ -34,10 +34,7 @@ import {
tap
} from "rxjs"
import {
setSourceFacts,
setSourceState
} from "~/actions"
import { getElement } from "~/browser"
import { renderSourceFacts } from "~/templates"
import { Component } from "../../_"
@@ -94,7 +91,7 @@ export function watchSource(
)
})
.pipe(
catchError(() => NEVER),
catchError(() => EMPTY),
filter(facts => Object.keys(facts).length > 0),
map(facts => ({ facts })),
shareReplay(1)
@@ -111,17 +108,20 @@ export function watchSource(
export function mountSource(
el: HTMLAnchorElement
): Observable<Component<Source>> {
const internal$ = new Subject<Source>()
internal$.subscribe(({ facts }) => {
setSourceFacts(el, renderSourceFacts(facts))
setSourceState(el, "done")
})
const inner = getElement(":scope > :last-child", el)
return defer(() => {
const push$ = new Subject<Source>()
push$.subscribe(({ facts }) => {
inner.appendChild(renderSourceFacts(facts))
inner.setAttribute("data-md-state", "done")
})
/* Create and return component */
return watchSource(el)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
/* Create and return component */
return watchSource(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -20,7 +20,7 @@
* IN THE SOFTWARE.
*/
import { NEVER, Observable } from "rxjs"
import { EMPTY, Observable } from "rxjs"
import { fetchSourceFactsFromGitHub } from "../github"
import { fetchSourceFactsFromGitLab } from "../gitlab"
@@ -83,6 +83,6 @@ export function fetchSourceFacts(
/* Everything else */
default:
return NEVER
return EMPTY
}
}

View File

@@ -50,7 +50,7 @@ interface Release {
/**
* Fetch GitHub repository facts
*
* @param user - GitHub user
* @param user - GitHub user or organization
* @param repo - GitHub repository
*
* @returns Repository facts observable

View File

@@ -23,21 +23,16 @@
import {
Observable,
Subject,
animationFrameScheduler,
defer,
distinctUntilKeyChanged,
finalize,
map,
observeOn,
of,
switchMap,
tap
} from "rxjs"
import { feature } from "~/_"
import {
resetTabsState,
setTabsState
} from "~/actions"
import {
Viewport,
watchElementSize,
@@ -119,36 +114,34 @@ export function watchTabs(
export function mountTabs(
el: HTMLElement, options: MountOptions
): Observable<Component<Tabs>> {
const internal$ = new Subject<Tabs>()
internal$
.pipe(
observeOn(animationFrameScheduler)
)
.subscribe({
/* Update state */
next({ hidden }) {
if (hidden)
setTabsState(el, "hidden")
else
resetTabsState(el)
},
/* Reset on complete */
complete() {
resetTabsState(el)
}
})
/* Create and return component */
return (
feature("navigation.tabs.sticky")
? of({ hidden: false })
: watchTabs(el, options)
)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
return defer(() => {
const push$ = new Subject<Tabs>()
push$.subscribe({
/* Handle emission */
next({ hidden }) {
if (hidden)
el.setAttribute("data-md-state", "hidden")
else
el.removeAttribute("data-md-state")
},
/* Handle complete */
complete() {
el.removeAttribute("data-md-state")
}
})
/* Create and return component */
return (
feature("navigation.tabs.sticky")
? of({ hidden: false })
: watchTabs(el, options)
)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -23,7 +23,6 @@
import {
Observable,
Subject,
animationFrameScheduler,
bufferCount,
combineLatest,
defer,
@@ -31,7 +30,6 @@ import {
distinctUntilKeyChanged,
finalize,
map,
observeOn,
of,
scan,
startWith,
@@ -40,12 +38,6 @@ import {
} from "rxjs"
import { feature } from "~/_"
import {
resetAnchorActive,
resetAnchorState,
setAnchorActive,
setAnchorState
} from "~/actions"
import {
Viewport,
getElements,
@@ -253,52 +245,55 @@ export function watchTableOfContents(
export function mountTableOfContents(
el: HTMLElement, options: MountOptions
): Observable<Component<TableOfContents>> {
const internal$ = new Subject<TableOfContents>()
internal$
.pipe(
observeOn(animationFrameScheduler),
)
.subscribe(({ prev, next }) => {
return defer(() => {
const push$ = new Subject<TableOfContents>()
push$.subscribe(({ prev, next }) => {
/* Look forward */
for (const [anchor] of next) {
resetAnchorActive(anchor)
resetAnchorState(anchor)
}
/* Look forward */
for (const [anchor] of next) {
anchor.removeAttribute("data-md-state")
anchor.classList.remove(
"md-nav__link--active"
)
}
/* Look backward */
for (const [index, [anchor]] of prev.entries()) {
setAnchorActive(anchor, index === prev.length - 1)
setAnchorState(anchor, "blur")
}
/* Look backward */
for (const [index, [anchor]] of prev.entries()) {
anchor.setAttribute("data-md-state", "blur")
anchor.classList.toggle(
"md-nav__link--active",
index === prev.length - 1
)
}
/* Set up anchor tracking, if enabled */
if (feature("navigation.tracking")) {
const url = getLocation()
/* Set up anchor tracking, if enabled */
if (feature("navigation.tracking")) {
const url = getLocation()
/* Set hash fragment to active anchor */
const anchor = prev[prev.length - 1]
if (anchor && anchor.length) {
const [active] = anchor
const { hash } = new URL(active.href)
if (url.hash !== hash) {
url.hash = hash
history.replaceState({}, "", `${url}`)
}
/* Reset anchor when at the top */
} else {
url.hash = ""
/* Set hash fragment to active anchor */
const anchor = prev[prev.length - 1]
if (anchor && anchor.length) {
const [active] = anchor
const { hash } = new URL(active.href)
if (url.hash !== hash) {
url.hash = hash
history.replaceState({}, "", `${url}`)
}
}
})
/* Create and return component */
return watchTableOfContents(el, options)
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
map(state => ({ ref: el, ...state }))
)
/* Reset anchor when at the top */
} else {
url.hash = ""
history.replaceState({}, "", `${url}`)
}
}
})
/* Create and return component */
return watchTableOfContents(el, options)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@@ -23,30 +23,17 @@
import {
Observable,
Subject,
animationFrameScheduler,
bufferCount,
combineLatest,
distinctUntilChanged,
distinctUntilKeyChanged,
finalize,
map,
observeOn,
tap,
withLatestFrom
} from "rxjs"
import {
resetBackToTopOffset,
resetBackToTopState,
resetFocusable,
setBackToTopOffset,
setBackToTopState,
setFocusable
} from "~/actions"
import {
Viewport,
setElementFocus
} from "~/browser"
import { Viewport } from "~/browser"
import { Component } from "../_"
import { Header } from "../header"
@@ -141,10 +128,9 @@ export function watchBackToTop(
export function mountBackToTop(
el: HTMLElement, { viewport$, header$, main$ }: MountOptions
): Observable<Component<BackToTop>> {
const internal$ = new Subject<BackToTop>()
internal$
const push$ = new Subject<BackToTop>()
push$
.pipe(
observeOn(animationFrameScheduler),
withLatestFrom(header$
.pipe(
distinctUntilKeyChanged("height")
@@ -153,32 +139,33 @@ export function mountBackToTop(
)
.subscribe({
/* Update state */
/* Handle emission */
next([{ hidden }, { height }]) {
setBackToTopOffset(el, height + 16)
el.style.top = `${height + 16}px`
if (hidden) {
setBackToTopState(el, "hidden")
setElementFocus(el, false)
setFocusable(el, -1)
el.setAttribute("data-md-state", "hidden")
el.setAttribute("tabindex", "-1")
el.blur()
} else {
resetBackToTopState(el)
resetFocusable(el)
el.style.top = ""
el.removeAttribute("data-md-state")
el.removeAttribute("tabindex")
}
},
/* Reset on complete */
/* Handle complete */
complete() {
resetBackToTopOffset(el)
resetBackToTopState(el)
resetFocusable(el)
el.style.top = ""
el.removeAttribute("data-md-state")
el.removeAttribute("tabindex")
}
})
/* Create and return component */
return watchBackToTop(el, { viewport$, header$, main$ })
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@@ -52,8 +52,7 @@ import {
request,
requestXML,
setLocation,
setLocationHash,
setViewportOffset
setLocationHash
} from "~/browser"
import { getComponentElement } from "~/components"
import { h } from "~/utilities"
@@ -329,11 +328,11 @@ export function setupInstantLoading(
.pipe(
sample(document$),
)
.subscribe(({ url, offset }) => {
.subscribe(({ url, offset = { y: 0 } }) => {
if (url.hash && !offset) {
setLocationHash(url.hash)
} else {
setViewportOffset(offset || { y: 0 })
window.scrollTo(0, offset.y)
}
})
@@ -355,7 +354,7 @@ export function setupInstantLoading(
filter(([a, b]) => a.url.pathname === b.url.pathname),
map(([, state]) => state)
)
.subscribe(({ offset }) => {
setViewportOffset(offset || { y: 0 })
.subscribe(({ offset = { y: 0 } }) => {
window.scrollTo(0, offset.y)
})
}

View File

@@ -32,10 +32,6 @@ import {
withLatestFrom
} from "rxjs"
import {
resetScrollLock,
setScrollLock
} from "~/actions"
import {
Viewport,
watchToggle
@@ -82,9 +78,15 @@ export function patchScrolllock(
withLatestFrom(viewport$)
)
.subscribe(([active, { offset: { y }}]) => {
if (active)
setScrollLock(document.body, y)
else
resetScrollLock(document.body)
if (active) {
document.body.setAttribute("data-md-state", "lock")
document.body.style.top = `-${y}px`
} else {
const value = -1 * parseInt(document.body.style.top, 10)
document.body.removeAttribute("data-md-state")
document.body.style.top = ""
if (value)
window.scrollTo(0, value)
}
})
}

View File

@@ -70,21 +70,3 @@ export function round(value: number): string {
return value.toString()
}
}
/**
* Simple hash function
*
* @see https://bit.ly/2wsVjJ4 - Original source
*
* @param value - Value to be hashed
*
* @returns Hash as 32bit integer
*/
export function hash(value: string): number {
let h = 0
for (let i = 0, len = value.length; i < len; i++) {
h = ((h << 5) - h) + value.charCodeAt(i)
h |= 0 // Convert to 32bit integer
}
return h
}