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> </script>
{% endblock %} {% endblock %}
{% block scripts %} {% 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"] %} {% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script> <script src="{{ path | url }}"></script>
{% endfor %} {% 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 %} {% endblock %}
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{ 'overrides/assets/javascripts/bundle.afb943e6.min.js' | url }}"></script> <script src="{{ 'overrides/assets/javascripts/bundle.a08d04cf.min.js' | url }}"></script>
{% endblock %} {% 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 * 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 * Watch element focus
* *

View File

@ -23,6 +23,5 @@
export * from "./_" export * from "./_"
export * from "./focus" export * from "./focus"
export * from "./offset" export * from "./offset"
export * from "./selection"
export * from "./size" export * from "./size"
export * from "./visibility" 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, merge,
of, of,
shareReplay, shareReplay,
startWith,
switchMap, switchMap,
tap tap
} from "rxjs" } from "rxjs"
@ -113,7 +112,7 @@ export function watchElementVisibility(
* @param el - Element * @param el - Element
* @param threshold - Threshold * @param threshold - Threshold
* *
* @returns Element threshold observable * @returns Element boundary observable
*/ */
export function watchElementBoundary( export function watchElementBoundary(
el: HTMLElement, threshold = 16 el: HTMLElement, threshold = 16

View File

@ -21,7 +21,7 @@
*/ */
import { import {
NEVER, EMPTY,
Observable, Observable,
fromEvent, fromEvent,
fromEventPattern, fromEventPattern,
@ -86,6 +86,6 @@ export function at<T>(
): Observable<T> { ): Observable<T> {
return query$ return query$
.pipe( .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 "focus-visible"
import { import {
EMPTY,
NEVER, NEVER,
Subject, Subject,
defer, defer,
@ -198,13 +199,13 @@ const content$ = defer(() => merge(
/* Content */ /* Content */
...getComponentElements("content") ...getComponentElements("content")
.map(el => mountContent(el, { target$, viewport$, hover$, print$ })), .map(el => mountContent(el, { target$, hover$, print$ })),
/* Search highlighting */ /* Search highlighting */
...getComponentElements("content") ...getComponentElements("content")
.map(el => feature("search.highlight") .map(el => feature("search.highlight")
? mountSearchHiglight(el, { index$, location$ }) ? mountSearchHiglight(el, { index$, location$ })
: NEVER : EMPTY
), ),
/* Header title */ /* Header title */

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@
import { import {
Observable, Observable,
Subject, Subject,
defer,
finalize, finalize,
fromEvent, fromEvent,
map, map,
@ -33,6 +34,7 @@ import {
import { import {
getElement, getElement,
getElementOffset,
getElements getElements
} from "~/browser" } from "~/browser"
@ -64,7 +66,11 @@ export function watchContentTabs(
el: HTMLElement el: HTMLElement
): Observable<ContentTabs> { ): Observable<ContentTabs> {
return merge(...getElements(":scope > input", el) 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( .pipe(
map(id => ({ map(id => ({
@ -76,6 +82,11 @@ export function watchContentTabs(
/** /**
* Mount content tabs * 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 * @param el - Content tabs element
* *
* @returns Content tabs component observable * @returns Content tabs component observable
@ -83,25 +94,23 @@ export function watchContentTabs(
export function mountContentTabs( export function mountContentTabs(
el: HTMLElement el: HTMLElement
): Observable<Component<ContentTabs>> { ): Observable<Component<ContentTabs>> {
const internal$ = new Subject<ContentTabs>() const container = getElement(".tabbed-labels", el)
internal$.subscribe(({ active }) => { return defer(() => {
// TODO: Hack, scrollIntoView is too buggy const push$ = new Subject<ContentTabs>()
const container = active.parentElement! push$.subscribe(({ active }) => {
if ( const { x } = getElementOffset(active)
active.offsetLeft + active.offsetWidth > container.scrollLeft + container.offsetWidth ||
active.offsetLeft < container.scrollLeft
)
container.scrollTo({ container.scrollTo({
behavior: "smooth", behavior: "smooth",
left: active.offsetLeft left: x
}) })
}) })
/* Create and return component */ /* Create and return component */
return watchContentTabs(el) return watchContentTabs(el)
.pipe( .pipe(
tap(state => internal$.next(state)), tap(state => push$.next(state)),
finalize(() => internal$.complete()), finalize(() => push$.complete()),
map(state => ({ ref: el, ...state })) map(state => ({ ref: el, ...state }))
) )
})
} }

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@
import { import {
Observable, Observable,
Subject, Subject,
defer,
finalize, finalize,
fromEvent, fromEvent,
map, map,
@ -74,9 +75,9 @@ export function watchPalette(
inputs: HTMLInputElement[] inputs: HTMLInputElement[]
): Observable<Palette> { ): Observable<Palette> {
const current = __md_get<Palette>("__palette") || { const current = __md_get<Palette>("__palette") || {
index: inputs.findIndex(input => ( index: inputs.findIndex(input => matchMedia(
matchMedia(input.getAttribute("data-md-color-media")!).matches input.getAttribute("data-md-color-media")!
)) ).matches)
} }
/* Emit changes in color palette */ /* Emit changes in color palette */
@ -99,11 +100,6 @@ export function watchPalette(
shareReplay(1) shareReplay(1)
) )
/* Persist preference in local storage */
palette$.subscribe(palette => {
__md_set("__palette", palette)
})
/* Return palette */ /* Return palette */
return palette$ return palette$
} }
@ -118,28 +114,33 @@ export function watchPalette(
export function mountPalette( export function mountPalette(
el: HTMLElement el: HTMLElement
): Observable<Component<Palette>> { ): Observable<Component<Palette>> {
const internal$ = new Subject<Palette>() return defer(() => {
const push$ = new Subject<Palette>()
push$.subscribe(palette => {
/* Set color palette */ /* Set color palette */
internal$.subscribe(palette => { for (const [key, value] of Object.entries(palette.color))
for (const [key, value] of Object.entries(palette.color)) if (typeof value === "string")
if (typeof value === "string") document.body.setAttribute(`data-md-color-${key}`, value)
document.body.setAttribute(`data-md-color-${key}`, value)
/* Toggle visibility */ /* Toggle visibility */
for (let index = 0; index < inputs.length; index++) { for (let index = 0; index < inputs.length; index++) {
const label = inputs[index].nextElementSibling const label = inputs[index].nextElementSibling
if (label instanceof HTMLElement) if (label instanceof HTMLElement)
label.hidden = palette.index !== index 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, Keyboard,
getActiveElement, getActiveElement,
getElements, getElements,
setElementFocus,
setElementSelection,
setToggle setToggle
} from "~/browser" } from "~/browser"
import { import {
@ -166,14 +164,14 @@ export function mountSearch(
case "Escape": case "Escape":
case "Tab": case "Tab":
setToggle("search", false) setToggle("search", false)
setElementFocus(query, false) query.blur()
break break
/* Vertical arrows: select previous or next search result */ /* Vertical arrows: select previous or next search result */
case "ArrowUp": case "ArrowUp":
case "ArrowDown": case "ArrowDown":
if (typeof active === "undefined") { if (typeof active === "undefined") {
setElementFocus(query) query.focus()
} else { } else {
const els = [query, ...getElements( const els = [query, ...getElements(
":not(details) > [href], summary, details[open] [href]", ":not(details) > [href], summary, details[open] [href]",
@ -184,7 +182,7 @@ export function mountSearch(
key.type === "ArrowUp" ? -1 : +1 key.type === "ArrowUp" ? -1 : +1
) )
) % els.length) ) % els.length)
setElementFocus(els[i]) els[i].focus()
} }
/* Prevent scrolling of page */ /* Prevent scrolling of page */
@ -194,7 +192,7 @@ export function mountSearch(
/* All other keys: hand to search query */ /* All other keys: hand to search query */
default: default:
if (query !== getActiveElement()) if (query !== getActiveElement())
setElementFocus(query) query.focus()
} }
}) })
@ -210,8 +208,10 @@ export function mountSearch(
case "f": case "f":
case "s": case "s":
case "/": case "/":
setElementFocus(query) query.focus()
setElementSelection(query) query.select()
/* Prevent scrolling of page */
key.claim() key.claim()
break break
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -24,22 +24,21 @@ import {
Observable, Observable,
Subject, Subject,
animationFrameScheduler, animationFrameScheduler,
auditTime,
combineLatest, combineLatest,
defer,
distinctUntilChanged, distinctUntilChanged,
finalize, finalize,
map, map,
observeOn,
tap, tap,
withLatestFrom withLatestFrom
} from "rxjs" } from "rxjs"
import { import {
resetSidebarHeight, Viewport,
resetSidebarOffset, getElement,
setSidebarHeight, getElementOffset
setSidebarOffset } from "~/browser"
} from "~/actions"
import { Viewport } from "~/browser"
import { Component } from "../_" import { Component } from "../_"
import { Header } from "../header" import { Header } from "../header"
@ -125,6 +124,19 @@ export function watchSidebar(
/** /**
* Mount sidebar * 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 el - Sidebar element
* @param options - Options * @param options - Options
* *
@ -133,32 +145,36 @@ export function watchSidebar(
export function mountSidebar( export function mountSidebar(
el: HTMLElement, { header$, ...options }: MountOptions el: HTMLElement, { header$, ...options }: MountOptions
): Observable<Component<Sidebar>> { ): Observable<Component<Sidebar>> {
const internal$ = new Subject<Sidebar>() const inner = getElement(".md-sidebar__scrollwrap", el)
internal$ const { y } = getElementOffset(inner)
.pipe( return defer(() => {
observeOn(animationFrameScheduler), const push$ = new Subject<Sidebar>()
withLatestFrom(header$) push$
) .pipe(
.subscribe({ auditTime(0, animationFrameScheduler),
withLatestFrom(header$)
)
.subscribe({
/* Update height and offset */ /* Handle emission */
next([{ height }, { height: offset }]) { next([{ height }, { height: offset }]) {
setSidebarHeight(el, height) inner.style.height = `${height - 2 * y}px`
setSidebarOffset(el, offset) el.style.top = `${offset}px`
}, },
/* Reset on complete */ /* Handle complete */
complete() { complete() {
resetSidebarOffset(el) inner.style.height = ""
resetSidebarHeight(el) el.style.top = ""
} }
}) })
/* Create and return component */ /* Create and return component */
return watchSidebar(el, options) return watchSidebar(el, options)
.pipe( .pipe(
tap(state => internal$.next(state)), tap(state => push$.next(state)),
finalize(() => internal$.complete()), finalize(() => push$.complete()),
map(state => ({ ref: el, ...state })) map(state => ({ ref: el, ...state }))
) )
})
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,6 @@
import { import {
Observable, Observable,
Subject, Subject,
animationFrameScheduler,
bufferCount, bufferCount,
combineLatest, combineLatest,
defer, defer,
@ -31,7 +30,6 @@ import {
distinctUntilKeyChanged, distinctUntilKeyChanged,
finalize, finalize,
map, map,
observeOn,
of, of,
scan, scan,
startWith, startWith,
@ -40,12 +38,6 @@ import {
} from "rxjs" } from "rxjs"
import { feature } from "~/_" import { feature } from "~/_"
import {
resetAnchorActive,
resetAnchorState,
setAnchorActive,
setAnchorState
} from "~/actions"
import { import {
Viewport, Viewport,
getElements, getElements,
@ -253,52 +245,55 @@ export function watchTableOfContents(
export function mountTableOfContents( export function mountTableOfContents(
el: HTMLElement, options: MountOptions el: HTMLElement, options: MountOptions
): Observable<Component<TableOfContents>> { ): Observable<Component<TableOfContents>> {
const internal$ = new Subject<TableOfContents>() return defer(() => {
internal$ const push$ = new Subject<TableOfContents>()
.pipe( push$.subscribe(({ prev, next }) => {
observeOn(animationFrameScheduler),
)
.subscribe(({ prev, next }) => {
/* Look forward */ /* Look forward */
for (const [anchor] of next) { for (const [anchor] of next) {
resetAnchorActive(anchor) anchor.removeAttribute("data-md-state")
resetAnchorState(anchor) anchor.classList.remove(
} "md-nav__link--active"
)
}
/* Look backward */ /* Look backward */
for (const [index, [anchor]] of prev.entries()) { for (const [index, [anchor]] of prev.entries()) {
setAnchorActive(anchor, index === prev.length - 1) anchor.setAttribute("data-md-state", "blur")
setAnchorState(anchor, "blur") anchor.classList.toggle(
} "md-nav__link--active",
index === prev.length - 1
)
}
/* Set up anchor tracking, if enabled */ /* Set up anchor tracking, if enabled */
if (feature("navigation.tracking")) { if (feature("navigation.tracking")) {
const url = getLocation() const url = getLocation()
/* Set hash fragment to active anchor */ /* Set hash fragment to active anchor */
const anchor = prev[prev.length - 1] const anchor = prev[prev.length - 1]
if (anchor && anchor.length) { if (anchor && anchor.length) {
const [active] = anchor const [active] = anchor
const { hash } = new URL(active.href) const { hash } = new URL(active.href)
if (url.hash !== hash) { if (url.hash !== hash) {
url.hash = hash url.hash = hash
history.replaceState({}, "", `${url}`)
}
/* Reset anchor when at the top */
} else {
url.hash = ""
history.replaceState({}, "", `${url}`) history.replaceState({}, "", `${url}`)
} }
}
})
/* Create and return component */ /* Reset anchor when at the top */
return watchTableOfContents(el, options) } else {
.pipe( url.hash = ""
tap(state => internal$.next(state)), history.replaceState({}, "", `${url}`)
finalize(() => internal$.complete()), }
map(state => ({ ref: el, ...state })) }
) })
/* 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 { import {
Observable, Observable,
Subject, Subject,
animationFrameScheduler,
bufferCount, bufferCount,
combineLatest, combineLatest,
distinctUntilChanged, distinctUntilChanged,
distinctUntilKeyChanged, distinctUntilKeyChanged,
finalize, finalize,
map, map,
observeOn,
tap, tap,
withLatestFrom withLatestFrom
} from "rxjs" } from "rxjs"
import { import { Viewport } from "~/browser"
resetBackToTopOffset,
resetBackToTopState,
resetFocusable,
setBackToTopOffset,
setBackToTopState,
setFocusable
} from "~/actions"
import {
Viewport,
setElementFocus
} from "~/browser"
import { Component } from "../_" import { Component } from "../_"
import { Header } from "../header" import { Header } from "../header"
@ -141,10 +128,9 @@ export function watchBackToTop(
export function mountBackToTop( export function mountBackToTop(
el: HTMLElement, { viewport$, header$, main$ }: MountOptions el: HTMLElement, { viewport$, header$, main$ }: MountOptions
): Observable<Component<BackToTop>> { ): Observable<Component<BackToTop>> {
const internal$ = new Subject<BackToTop>() const push$ = new Subject<BackToTop>()
internal$ push$
.pipe( .pipe(
observeOn(animationFrameScheduler),
withLatestFrom(header$ withLatestFrom(header$
.pipe( .pipe(
distinctUntilKeyChanged("height") distinctUntilKeyChanged("height")
@ -153,32 +139,33 @@ export function mountBackToTop(
) )
.subscribe({ .subscribe({
/* Update state */ /* Handle emission */
next([{ hidden }, { height }]) { next([{ hidden }, { height }]) {
setBackToTopOffset(el, height + 16) el.style.top = `${height + 16}px`
if (hidden) { if (hidden) {
setBackToTopState(el, "hidden") el.setAttribute("data-md-state", "hidden")
setElementFocus(el, false) el.setAttribute("tabindex", "-1")
setFocusable(el, -1) el.blur()
} else { } else {
resetBackToTopState(el) el.style.top = ""
resetFocusable(el) el.removeAttribute("data-md-state")
el.removeAttribute("tabindex")
} }
}, },
/* Reset on complete */ /* Handle complete */
complete() { complete() {
resetBackToTopOffset(el) el.style.top = ""
resetBackToTopState(el) el.removeAttribute("data-md-state")
resetFocusable(el) el.removeAttribute("tabindex")
} }
}) })
/* Create and return component */ /* Create and return component */
return watchBackToTop(el, { viewport$, header$, main$ }) return watchBackToTop(el, { viewport$, header$, main$ })
.pipe( .pipe(
tap(state => internal$.next(state)), tap(state => push$.next(state)),
finalize(() => internal$.complete()), finalize(() => push$.complete()),
map(state => ({ ref: el, ...state })) map(state => ({ ref: el, ...state }))
) )
} }

View File

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

View File

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

View File

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