mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-24 23:04:32 -04:00
Fixed observable completion semantics
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user