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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -213,7 +213,7 @@
</script>
{% endblock %}
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.7c769d4b.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.054bf2ee.min.js' | url }}"></script>
{% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script>
{% endfor %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

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
}

View File

@ -40,16 +40,12 @@ import {
zipWith
} from "rxjs"
import {
addToSearchResultList,
resetSearchResultList,
resetSearchResultMeta,
setSearchResultMeta
} from "~/actions"
import { translation } from "~/_"
import {
getElement,
watchElementBoundary
} from "~/browser"
import { round } from "~/utilities"
import { Icon, renderIconSearchResult } from "_/templates"
@ -146,7 +142,7 @@ export function watchIconSearchResult(
export function mountIconSearchResult(
el: HTMLElement, { index$, query$ }: MountOptions
): Observable<Component<IconSearchResult, HTMLElement>> {
const internal$ = new Subject<IconSearchResult>()
const push$ = new Subject<IconSearchResult>()
const boundary$ = watchElementBoundary(el)
.pipe(
filter(Boolean)
@ -154,24 +150,43 @@ export function mountIconSearchResult(
/* Update search result metadata */
const meta = getElement(":scope > :first-child", el)
internal$
push$
.pipe(
observeOn(animationFrameScheduler),
withLatestFrom(query$)
)
.subscribe(([{ data }, { value }]) => {
if (value)
setSearchResultMeta(meta, data.length)
else
resetSearchResultMeta(meta)
if (value) {
switch (data.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(data.length)
)
}
} else {
meta.textContent = translation("search.result.placeholder")
}
})
/* Update icon search result list */
const list = getElement(":scope > :last-child", el)
internal$
push$
.pipe(
observeOn(animationFrameScheduler),
tap(() => resetSearchResultList(list)),
tap(() => list.innerHTML = ""),
switchMap(({ data }) => merge(
of(...data.slice(0, 10)),
of(...data.slice(10))
@ -183,15 +198,15 @@ export function mountIconSearchResult(
)),
withLatestFrom(query$)
)
.subscribe(([result, { value }]) => {
addToSearchResultList(list, renderIconSearchResult(result, value))
})
.subscribe(([result, { value }]) => list.appendChild(
renderIconSearchResult(result, value)
))
/* Create and return component */
return watchIconSearchResult(el, { query$, index$ })
.pipe(
tap(state => internal$.next(state)),
finalize(() => internal$.complete()),
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
}

View File

@ -25,7 +25,7 @@ import { build as esbuild } from "esbuild"
import * as path from "path"
import postcss from "postcss"
import {
NEVER,
EMPTY,
Observable,
catchError,
concat,
@ -132,7 +132,7 @@ export function transformStyle(
),
catchError(err => {
console.log(err.formatted || err.message)
return NEVER
return EMPTY
}),
switchMap(({ css, map }) => {
const file = digest(options.to, css)
@ -182,7 +182,7 @@ export function transformScript(
map: Buffer.from(data, "base64")
})
}),
catchError(() => NEVER),
catchError(() => EMPTY),
switchMap(({ js, map }) => {
const file = digest(options.to, js)
return concat(