mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-24 06:43:50 -04:00
Restructured project
This commit is contained in:
@@ -27,9 +27,9 @@ import { Observable, defer, of } from "rxjs"
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Header
|
||||
* Header state
|
||||
*/
|
||||
export interface Header {
|
||||
export interface HeaderState {
|
||||
sticky: boolean /* Header stickyness */
|
||||
height: number /* Header visible height */
|
||||
}
|
||||
@@ -46,11 +46,11 @@ export interface Header {
|
||||
*
|
||||
* @param el - Header element
|
||||
*
|
||||
* @return Header observable
|
||||
* @return Header state observable
|
||||
*/
|
||||
export function watchHeader(
|
||||
el: HTMLElement
|
||||
): Observable<Header> {
|
||||
): Observable<HeaderState> {
|
||||
return defer(() => {
|
||||
const sticky = getComputedStyle(el)
|
||||
.getPropertyValue("position") === "fixed"
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
|
||||
import { Agent, ViewportOffset } from "utilities"
|
||||
|
||||
import { Header } from "../_"
|
||||
import { HeaderState } from "../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
@@ -40,7 +40,7 @@ import { Header } from "../_"
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
header$: Observable<Header> /* Header observable */
|
||||
header$: Observable<HeaderState> /* Header state observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -59,7 +59,7 @@ interface Options {
|
||||
*
|
||||
* @return Viewport offset observable
|
||||
*/
|
||||
export function watchHeaderOffsetToTopOf(
|
||||
export function watchViewportOffsetFromTopOf(
|
||||
el: HTMLElement, { viewport }: Agent, { header$ }: Options
|
||||
): Observable<ViewportOffset> {
|
||||
|
||||
@@ -91,7 +91,7 @@ export function watchHeaderOffsetToTopOf(
|
||||
*
|
||||
* @return Viewport offset observable
|
||||
*/
|
||||
export function watchHeaderOffsetToBottomOf(
|
||||
export function watchViewportOffsetFromBottomOf(
|
||||
el: HTMLElement, { viewport }: Agent, { header$ }: Options
|
||||
): Observable<ViewportOffset> {
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
|
||||
import { resetHeaderShadow, setHeaderShadow } from "actions"
|
||||
|
||||
import { Main } from "../../main"
|
||||
import { MainState } from "../../main"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@@ -49,7 +49,7 @@ import { Main } from "../../main"
|
||||
*/
|
||||
export function paintHeaderShadow(
|
||||
el: HTMLElement
|
||||
): MonoTypeOperatorFunction<Main> {
|
||||
): MonoTypeOperatorFunction<MainState> {
|
||||
return pipe(
|
||||
distinctUntilKeyChanged("active"),
|
||||
|
||||
|
||||
@@ -26,16 +26,16 @@ import { map, shareReplay } from "rxjs/operators"
|
||||
import { switchMapIf } from "extensions"
|
||||
import { Agent, paintHidden } from "utilities"
|
||||
|
||||
import { Header, watchHeaderOffsetToTopOf } from "../header"
|
||||
import { HeaderState, watchViewportOffsetFromTopOf } from "../header"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Hero
|
||||
* Hero state
|
||||
*/
|
||||
export interface Hero {
|
||||
export interface HeroState {
|
||||
hidden: boolean /* Whether the hero is hidden */
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface Hero {
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
header$: Observable<Header> /* Header observable */
|
||||
header$: Observable<HeaderState> /* Header state observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -55,32 +55,47 @@ interface Options {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup hero from source observable
|
||||
* Watch hero
|
||||
*
|
||||
* @param el - Hero element
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Hero state
|
||||
*/
|
||||
export function watchHero(
|
||||
el: HTMLElement, agent: Agent, { header$ }: Options
|
||||
): Observable<HeroState> {
|
||||
|
||||
/* Watch and paint visibility */
|
||||
const hidden$ = watchViewportOffsetFromTopOf(el, agent, { header$ })
|
||||
.pipe(
|
||||
paintHidden(el, 20)
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return hidden$
|
||||
.pipe(
|
||||
map(hidden => ({ hidden }))
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount hero from source observable
|
||||
*
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function setupHero(
|
||||
agent: Agent, { header$ }: Options
|
||||
): OperatorFunction<HTMLElement, Hero> {
|
||||
export function mountHero(
|
||||
agent: Agent, options: Options
|
||||
): OperatorFunction<HTMLElement, HeroState> {
|
||||
const { media } = agent
|
||||
return pipe(
|
||||
switchMapIf(media.screen$, el => {
|
||||
|
||||
/* Watch and paint visibility */
|
||||
const hidden$ = watchHeaderOffsetToTopOf(el, agent, { header$ })
|
||||
.pipe(
|
||||
paintHidden(el, 20)
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return hidden$
|
||||
.pipe(
|
||||
map(hidden => ({ hidden }))
|
||||
)
|
||||
}),
|
||||
switchMapIf(media.screen$, el => watchHero(el, agent, options)),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,16 +31,16 @@ import {
|
||||
|
||||
import { Agent } from "utilities"
|
||||
|
||||
import { Header } from "../../header"
|
||||
import { HeaderState } from "../../header"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Main area
|
||||
* Main area state
|
||||
*/
|
||||
export interface Main {
|
||||
export interface MainState {
|
||||
offset: number /* Main area top offset */
|
||||
height: number /* Main area visible height */
|
||||
active: boolean /* Scrolled past top offset */
|
||||
@@ -54,7 +54,7 @@ export interface Main {
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
header$: Observable<Header> /* Header observable */
|
||||
header$: Observable<HeaderState> /* Header state observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -62,64 +62,78 @@ interface Options {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup main area from source observable
|
||||
* Watch main area
|
||||
*
|
||||
* This function returns an observable that computes the visual parameters of
|
||||
* the main area from the viewport height and vertical offset, as well as the
|
||||
* height of the header element. The height of the main area is corrected by
|
||||
* the height of the header (if fixed) and footer element.
|
||||
* the main area which depends on the viewport height and vertical offset, as
|
||||
* well as the height of the header element, if the header is fixed.
|
||||
*
|
||||
* @param el - Main area element
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Main area state observable
|
||||
*/
|
||||
export function watchMain(
|
||||
el: HTMLElement, { viewport }: Agent, { header$ }: Options
|
||||
): Observable<MainState> {
|
||||
|
||||
/* Compute necessary adjustment for header */
|
||||
const adjust$ = header$
|
||||
.pipe(
|
||||
pluck("height")
|
||||
)
|
||||
|
||||
/* Compute the main area's visible height */
|
||||
const height$ = combineLatest([
|
||||
viewport.offset$,
|
||||
viewport.size$,
|
||||
adjust$
|
||||
])
|
||||
.pipe(
|
||||
map(([{ y }, { height }, adjust]) => {
|
||||
const top = el.offsetTop
|
||||
const bottom = el.offsetHeight + top
|
||||
return height
|
||||
- Math.max(0, top - y, adjust)
|
||||
- Math.max(0, height + y - bottom)
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Compute whether the viewport offset is past the main area's top */
|
||||
const active$ = combineLatest([viewport.offset$, adjust$])
|
||||
.pipe(
|
||||
map(([{ y }, adjust]) => y >= el.offsetTop - adjust),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return combineLatest([height$, adjust$, active$])
|
||||
.pipe(
|
||||
map(([height, adjust, active]) => ({
|
||||
offset: el.offsetTop - adjust,
|
||||
height,
|
||||
active
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount main area from source observable
|
||||
*
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function setupMain(
|
||||
{ viewport }: Agent, { header$ }: Options
|
||||
): OperatorFunction<HTMLElement, Main> {
|
||||
export function mountMain(
|
||||
agent: Agent, options: Options
|
||||
): OperatorFunction<HTMLElement, MainState> {
|
||||
return pipe(
|
||||
switchMap(el => {
|
||||
|
||||
/* Compute necessary adjustment for header */
|
||||
const adjust$ = header$
|
||||
.pipe(
|
||||
pluck("height")
|
||||
)
|
||||
|
||||
/* Compute the main area's visible height */
|
||||
const height$ = combineLatest([
|
||||
viewport.offset$,
|
||||
viewport.size$,
|
||||
adjust$
|
||||
])
|
||||
.pipe(
|
||||
map(([{ y }, { height }, adjust]) => {
|
||||
const top = el.offsetTop
|
||||
const bottom = el.offsetHeight + top
|
||||
return height
|
||||
- Math.max(0, top - y, adjust)
|
||||
- Math.max(0, height + y - bottom)
|
||||
}),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Compute whether the viewport offset is past the main area's top */
|
||||
const active$ = combineLatest([viewport.offset$, adjust$])
|
||||
.pipe(
|
||||
map(([{ y }, adjust]) => y >= el.offsetTop - adjust),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return combineLatest([height$, adjust$, active$])
|
||||
.pipe(
|
||||
map(([height, adjust, active]) => ({
|
||||
offset: el.offsetTop - adjust,
|
||||
height,
|
||||
active
|
||||
}))
|
||||
)
|
||||
}),
|
||||
switchMap(el => watchMain(el, agent, options)),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,16 +45,16 @@ import {
|
||||
} from "actions"
|
||||
import { Agent } from "utilities"
|
||||
|
||||
import { Main } from "../main"
|
||||
import { MainState } from "../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Sidebar
|
||||
* Sidebar state
|
||||
*/
|
||||
export interface Sidebar {
|
||||
export interface SidebarState {
|
||||
height: number /* Sidebar height */
|
||||
lock: boolean /* Sidebar lock */
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export interface Sidebar {
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
main$: Observable<Main> /* Main area observable */
|
||||
main$: Observable<MainState> /* Main area state observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -78,19 +78,19 @@ interface Options {
|
||||
* Watch sidebar
|
||||
*
|
||||
* This function returns an observable that computes the visual parameters of
|
||||
* the given element (a sidebar) from the vertical viewport offset, as well as
|
||||
* the height of the main area. When the page is scrolled beyond the header,
|
||||
* the sidebar is locked and fills the remaining space.
|
||||
* the sidebar which depends on the vertical viewport offset, as well as the
|
||||
* height of the main area. When the page is scrolled beyond the header, the
|
||||
* sidebar is locked and fills the remaining space.
|
||||
*
|
||||
* @param el - Sidebar element
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Sidebar observable
|
||||
* @return Sidebar state observable
|
||||
*/
|
||||
export function watchSidebar(
|
||||
el: HTMLElement, { viewport }: Agent, { main$ }: Options
|
||||
): Observable<Sidebar> {
|
||||
): Observable<SidebarState> {
|
||||
|
||||
/* Adjust for internal main area offset */
|
||||
const adjust = parseFloat(
|
||||
@@ -116,7 +116,7 @@ export function watchSidebar(
|
||||
return combineLatest([height$, lock$])
|
||||
.pipe(
|
||||
map(([height, lock]) => ({ height, lock })),
|
||||
distinctUntilChanged<Sidebar>(equals),
|
||||
distinctUntilChanged<SidebarState>(equals),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
@@ -132,7 +132,7 @@ export function watchSidebar(
|
||||
*/
|
||||
export function paintSidebar(
|
||||
el: HTMLElement
|
||||
): MonoTypeOperatorFunction<Sidebar> {
|
||||
): MonoTypeOperatorFunction<SidebarState> {
|
||||
return pipe(
|
||||
|
||||
/* Defer repaint to next animation frame */
|
||||
|
||||
@@ -27,8 +27,8 @@ import { switchMapIf } from "extensions"
|
||||
import { Agent } from "utilities"
|
||||
|
||||
import {
|
||||
Main,
|
||||
Sidebar,
|
||||
MainState,
|
||||
SidebarState,
|
||||
paintSidebar,
|
||||
watchSidebar
|
||||
} from "../../main"
|
||||
@@ -38,10 +38,10 @@ import {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Navigation
|
||||
* Navigation state
|
||||
*/
|
||||
export interface Navigation {
|
||||
sidebar: Sidebar /* Sidebar */
|
||||
export interface NavigationState {
|
||||
sidebar: SidebarState /* Sidebar state */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -52,7 +52,7 @@ export interface Navigation {
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
main$: Observable<Main> /* Main observable */
|
||||
main$: Observable<MainState> /* Main area state observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -60,32 +60,47 @@ interface Options {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup navigation from source observable
|
||||
* Watch navigation
|
||||
*
|
||||
* @param el - Navigation element
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Navigation state observable
|
||||
*/
|
||||
export function watchNavigation(
|
||||
el: HTMLElement, agent: Agent, { main$ }: Options
|
||||
): Observable<NavigationState> {
|
||||
|
||||
/* Watch and paint sidebar */
|
||||
const sidebar$ = watchSidebar(el, agent, { main$ })
|
||||
.pipe(
|
||||
paintSidebar(el)
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return sidebar$
|
||||
.pipe(
|
||||
map(sidebar => ({ sidebar }))
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount navigation from source observable
|
||||
*
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function setupNavigation(
|
||||
agent: Agent, { main$ }: Options
|
||||
): OperatorFunction<HTMLElement, Navigation> {
|
||||
export function mountNavigation(
|
||||
agent: Agent, options: Options
|
||||
): OperatorFunction<HTMLElement, NavigationState> {
|
||||
const { media } = agent
|
||||
return pipe(
|
||||
switchMapIf(media.screen$, el => {
|
||||
|
||||
/* Watch and paint sidebar */
|
||||
const sidebar$ = watchSidebar(el, agent, { main$ })
|
||||
.pipe(
|
||||
paintSidebar(el)
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return sidebar$
|
||||
.pipe(
|
||||
map(sidebar => ({ sidebar }))
|
||||
)
|
||||
}),
|
||||
switchMapIf(media.screen$, el => watchNavigation(el, agent, options)),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
// tslint:disable-next-line
|
||||
// export * from "./query"
|
||||
export * from "./query"
|
||||
export * from "./reset"
|
||||
export * from "./result"
|
||||
|
||||
@@ -19,3 +19,71 @@
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable, combineLatest, fromEvent } from "rxjs"
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
shareReplay,
|
||||
startWith
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { watchElementFocus } from "utilities"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search query state
|
||||
*/
|
||||
export interface SearchQueryState {
|
||||
value: string /* Query value */
|
||||
focus: boolean /* Query focus state */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
prepare(value: string): string /* Preparation function */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch search query
|
||||
*
|
||||
* @param el - Search query element
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Search query state observable
|
||||
*/
|
||||
export function watchSearchQuery(
|
||||
el: HTMLInputElement, { prepare }: Options
|
||||
): Observable<SearchQueryState> {
|
||||
|
||||
/* Intercept keyboard events */
|
||||
const value$ = fromEvent(el, "keyup")
|
||||
.pipe(
|
||||
map(() => prepare(el.value)),
|
||||
startWith(""),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/* Intercept focus events */
|
||||
const focus$ = watchElementFocus(el)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return combineLatest([value$, focus$])
|
||||
.pipe(
|
||||
map(([value, focus]) => ({ value, focus })),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ import { mapTo } from "rxjs/operators"
|
||||
*/
|
||||
export function watchSearchReset(
|
||||
el: HTMLElement
|
||||
): Observable<boolean> {
|
||||
): Observable<void> {
|
||||
return fromEvent(el, "click")
|
||||
.pipe(
|
||||
mapTo(true)
|
||||
mapTo(undefined)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,34 +52,53 @@ interface Options {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup search result from source observable
|
||||
* Watch search result
|
||||
*
|
||||
* @param el - Search result element
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Search result state observable
|
||||
*/
|
||||
export function watchSearchResult(
|
||||
el: HTMLElement, agent: Agent, { result$, query$ }: Options
|
||||
): Observable<SearchResult[]> {
|
||||
const container = el.parentElement!
|
||||
|
||||
/* Compute whether there are more search results elements */
|
||||
const render$ = watchElementOffset(container, agent)
|
||||
.pipe(
|
||||
map(({ y }) => y >= container.scrollHeight - container.offsetHeight - 16),
|
||||
distinctUntilChanged(),
|
||||
filter(identity)
|
||||
)
|
||||
|
||||
// combine into search result observable...
|
||||
|
||||
/* Paint search results */
|
||||
return result$
|
||||
.pipe(
|
||||
tap(x => { console.log("watchSearchResult", x) }),
|
||||
paintSearchResultMeta(el, { query$ }),
|
||||
paintSearchResultList(el, { render$ })
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount search result from source observable
|
||||
*
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function setupSearchResult(
|
||||
agent: Agent, { result$, query$ }: Options
|
||||
export function mountSearchResult(
|
||||
agent: Agent, options: Options
|
||||
): OperatorFunction<HTMLElement, SearchResult[]> {
|
||||
return pipe(
|
||||
switchMap(el => {
|
||||
const parent = el.parentElement!
|
||||
|
||||
/* Compute whether more elements need to be rendered */
|
||||
const render$ = watchElementOffset(parent, agent)
|
||||
.pipe(
|
||||
map(({ y }) => y >= parent.scrollHeight - parent.offsetHeight - 16),
|
||||
distinctUntilChanged(),
|
||||
filter(identity)
|
||||
)
|
||||
|
||||
/* Paint search results */
|
||||
return result$
|
||||
.pipe(
|
||||
paintSearchResultMeta(el, { query$ }),
|
||||
paintSearchResultList(el, { render$ })
|
||||
)
|
||||
})
|
||||
switchMap(el => watchSearchResult(el, agent, options)),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ interface Options {
|
||||
export function paintSearchResultList(
|
||||
el: HTMLElement, { render$ }: Options
|
||||
): MonoTypeOperatorFunction<SearchResult[]> {
|
||||
const parent = el.parentElement!
|
||||
const container = el.parentElement!
|
||||
const list = getElement(".md-search-result__list", el)!
|
||||
return pipe(
|
||||
switchMap(result => render$
|
||||
@@ -79,7 +79,7 @@ export function paintSearchResultList(
|
||||
scan(index => {
|
||||
while (index < result.length) {
|
||||
addToSearchResultList(list, renderSearchResult(result[index++]))
|
||||
if (parent.scrollHeight - parent.offsetHeight > 16)
|
||||
if (container.scrollHeight - container.offsetHeight > 16)
|
||||
break
|
||||
}
|
||||
return index
|
||||
|
||||
@@ -26,16 +26,16 @@ import { map, shareReplay } from "rxjs/operators"
|
||||
import { switchMapIf } from "extensions"
|
||||
import { Agent, paintHidden } from "utilities"
|
||||
|
||||
import { Header, watchHeaderOffsetToTopOf } from "../header"
|
||||
import { HeaderState, watchViewportOffsetFromTopOf } from "../header"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Tabs
|
||||
* Tabs state
|
||||
*/
|
||||
export interface Tabs {
|
||||
export interface TabsState {
|
||||
hidden: boolean /* Whether the tabs are hidden */
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface Tabs {
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
header$: Observable<Header> /* Header observable */
|
||||
header$: Observable<HeaderState> /* Header state observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -55,32 +55,50 @@ interface Options {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup tabs from source observable
|
||||
* Watch tabs
|
||||
*
|
||||
* This function returns an observable that computes the visual parameters of
|
||||
* the tabs, currently only denoting whether the tabs are hidden or not.
|
||||
*
|
||||
* @param el - Tabs element
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Tabs state
|
||||
*/
|
||||
export function watchTabs(
|
||||
el: HTMLElement, agent: Agent, { header$ }: Options
|
||||
): Observable<TabsState> {
|
||||
|
||||
/* Watch and paint visibility */
|
||||
const hidden$ = watchViewportOffsetFromTopOf(el, agent, { header$ })
|
||||
.pipe(
|
||||
paintHidden(el, 8)
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return hidden$
|
||||
.pipe(
|
||||
map(hidden => ({ hidden }))
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount tabs from source observable
|
||||
*
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function setupTabs(
|
||||
agent: Agent, { header$ }: Options
|
||||
): OperatorFunction<HTMLElement, Tabs> {
|
||||
export function mountTabs(
|
||||
agent: Agent, options: Options
|
||||
): OperatorFunction<HTMLElement, TabsState> {
|
||||
const { media } = agent
|
||||
return pipe(
|
||||
switchMapIf(media.screen$, el => {
|
||||
|
||||
/* Watch and paint visibility */
|
||||
const hidden$ = watchHeaderOffsetToTopOf(el, agent, { header$ })
|
||||
.pipe(
|
||||
paintHidden(el, 8)
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return hidden$
|
||||
.pipe(
|
||||
map(hidden => ({ hidden }))
|
||||
)
|
||||
}),
|
||||
switchMapIf(media.screen$, el => watchTabs(el, agent, options)),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ import { map, shareReplay } from "rxjs/operators"
|
||||
import { switchMapIf } from "extensions"
|
||||
import { Agent, getElements } from "utilities"
|
||||
|
||||
import { Header } from "../../header"
|
||||
import { HeaderState } from "../../header"
|
||||
import {
|
||||
Main,
|
||||
Sidebar,
|
||||
MainState,
|
||||
SidebarState,
|
||||
paintSidebar,
|
||||
watchSidebar
|
||||
} from "../../main"
|
||||
@@ -44,10 +44,10 @@ import {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Table of contents
|
||||
* Table of contents state
|
||||
*/
|
||||
export interface TableOfContents {
|
||||
sidebar: Sidebar /* Sidebar */
|
||||
export interface TableOfContentsState {
|
||||
sidebar: SidebarState /* Sidebar state */
|
||||
anchors: AnchorList /* Anchor list */
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ export interface TableOfContents {
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
header$: Observable<Header> /* Header observable */
|
||||
main$: Observable<Main> /* Main observable */
|
||||
header$: Observable<HeaderState> /* Header state observable */
|
||||
main$: Observable<MainState> /* Main area state observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -68,39 +68,54 @@ interface Options {
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup table of contents from source observable
|
||||
* Watch table of contents
|
||||
*
|
||||
* @param el - Table of contents element
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Table of contents state observable
|
||||
*/
|
||||
export function watchTableOfContents(
|
||||
el: HTMLElement, agent: Agent, { header$, main$ }: Options
|
||||
): Observable<TableOfContentsState> {
|
||||
|
||||
/* Watch and paint sidebar */
|
||||
const sidebar$ = watchSidebar(el, agent, { main$ })
|
||||
.pipe(
|
||||
paintSidebar(el)
|
||||
)
|
||||
|
||||
/* Watch and paint anchor list (scroll spy) */
|
||||
const els = getElements<HTMLAnchorElement>(".md-nav__link", el)
|
||||
const anchors$ = watchAnchorList(els, agent, { header$ })
|
||||
.pipe(
|
||||
paintAnchorList(els)
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return combineLatest([sidebar$, anchors$])
|
||||
.pipe(
|
||||
map(([sidebar, anchors]) => ({ sidebar, anchors }))
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount table of contents from source observable
|
||||
*
|
||||
* @param agent - Agent
|
||||
* @param options - Options
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function setupTableOfContents(
|
||||
agent: Agent, { header$, main$ }: Options
|
||||
): OperatorFunction<HTMLElement, TableOfContents> {
|
||||
export function mountTableOfContents(
|
||||
agent: Agent, options: Options
|
||||
): OperatorFunction<HTMLElement, TableOfContentsState> {
|
||||
const { media } = agent
|
||||
return pipe(
|
||||
switchMapIf(media.tablet$, el => {
|
||||
|
||||
/* Watch and paint sidebar */
|
||||
const sidebar$ = watchSidebar(el, agent, { main$ })
|
||||
.pipe(
|
||||
paintSidebar(el)
|
||||
)
|
||||
|
||||
/* Watch and paint anchor list (scroll spy) */
|
||||
const els = getElements<HTMLAnchorElement>(".md-nav__link", el)
|
||||
const anchors$ = watchAnchorList(els, agent, { header$ })
|
||||
.pipe(
|
||||
paintAnchorList(els)
|
||||
)
|
||||
|
||||
/* Combine into a single hot observable */
|
||||
return combineLatest([sidebar$, anchors$])
|
||||
.pipe(
|
||||
map(([sidebar, anchors]) => ({ sidebar, anchors }))
|
||||
)
|
||||
}),
|
||||
switchMapIf(media.tablet$, el => watchTableOfContents(el, agent, options)),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
} from "actions"
|
||||
import { Agent, getElement } from "utilities"
|
||||
|
||||
import { Header } from "../../header"
|
||||
import { HeaderState } from "../../header"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@@ -69,7 +69,7 @@ export interface AnchorList {
|
||||
* Options
|
||||
*/
|
||||
interface Options {
|
||||
header$: Observable<Header> /* Header observable */
|
||||
header$: Observable<HeaderState> /* Header state observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
|
||||
@@ -21,4 +21,3 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./anchor"
|
||||
|
||||
@@ -55,25 +55,25 @@ export interface SearchIndexDocument {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search index options
|
||||
* Search index pipeline
|
||||
*/
|
||||
export interface SearchIndexOptions {
|
||||
pipeline: {
|
||||
trimmer: boolean /* Add trimmer to pipeline */
|
||||
stopwords: boolean /* Add stopword filter to pipeline */
|
||||
}
|
||||
export interface SearchIndexPipeline {
|
||||
trimmer: boolean /* Add trimmer to pipeline */
|
||||
stopwords: boolean /* Add stopword filter to pipeline */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Search index
|
||||
* Search index options
|
||||
*
|
||||
* This interfaces describes the format of the `search_index.json` file which
|
||||
* is automatically built by the MkDocs search plugin.
|
||||
*/
|
||||
export interface SearchIndex {
|
||||
export interface SearchIndexOptions {
|
||||
config: SearchIndexConfig /* Search index configuration */
|
||||
docs: SearchIndexDocument[] /* Search index documents */
|
||||
options?: SearchIndexOptions /* Search index options */
|
||||
pipeline?: SearchIndexPipeline /* Search index pipeline */
|
||||
index?: object | string /* Prebuilt or serialized index */
|
||||
}
|
||||
|
||||
@@ -87,25 +87,11 @@ export interface SearchResult {
|
||||
sections: SectionDocument[] /* Section documents */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Data
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Default options
|
||||
*/
|
||||
const defaultOptions: SearchIndexOptions = {
|
||||
pipeline: {
|
||||
trimmer: true,
|
||||
stopwords: true
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Class
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export class Search {
|
||||
export class SearchIndex {
|
||||
|
||||
/**
|
||||
* Search document mapping
|
||||
@@ -130,17 +116,19 @@ export class Search {
|
||||
/**
|
||||
* Create a search index
|
||||
*
|
||||
* @param index - Search index
|
||||
* @param options - Options
|
||||
*/
|
||||
public constructor({ config, docs, options, index }: SearchIndex) {
|
||||
public constructor({ config, docs, pipeline, index }: SearchIndexOptions) {
|
||||
this.documents = setupSearchDocumentMap(docs)
|
||||
this.highlight = setupSearchHighlighter(config)
|
||||
|
||||
/* If no index was given, create it */
|
||||
if (typeof index === "undefined") {
|
||||
this.index = lunr(function() {
|
||||
const { pipeline } = options || defaultOptions
|
||||
pipeline = pipeline || {
|
||||
trimmer: true,
|
||||
stopwords: true
|
||||
}
|
||||
|
||||
/* Remove stemmer, as it cripples search experience */
|
||||
this.pipeline.reset()
|
||||
@@ -194,8 +182,8 @@ export class Search {
|
||||
.reduce((results, result) => {
|
||||
const document = this.documents.get(result.ref)
|
||||
if (typeof document !== "undefined") {
|
||||
if ("article" in document) {
|
||||
const ref = document.article.location
|
||||
if ("parent" in document) {
|
||||
const ref = document.parent.location
|
||||
results.set(ref, [...results.get(ref) || [], result])
|
||||
} else {
|
||||
const ref = document.location
|
||||
@@ -228,7 +216,7 @@ export class Search {
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize index
|
||||
* Serialize search index
|
||||
*
|
||||
* @return String representation
|
||||
*/
|
||||
|
||||
@@ -32,14 +32,14 @@ import { SearchIndexDocument } from "../_"
|
||||
* A top-level article
|
||||
*/
|
||||
export interface ArticleDocument extends SearchIndexDocument {
|
||||
section: boolean /* Whether the section was linked */
|
||||
linked: boolean /* Whether the section was linked */
|
||||
}
|
||||
|
||||
/**
|
||||
* A section of an article
|
||||
*/
|
||||
export interface SectionDocument extends SearchIndexDocument {
|
||||
article: ArticleDocument /* Parent article */
|
||||
parent: ArticleDocument /* Parent article */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@@ -85,22 +85,22 @@ export function setupSearchDocumentMap(
|
||||
|
||||
/* Handle section */
|
||||
if (hash) {
|
||||
const article = documents.get(path) as ArticleDocument
|
||||
const parent = documents.get(path) as ArticleDocument
|
||||
|
||||
/* Ignore first section, override article */
|
||||
if (!article.section) {
|
||||
article.title = doc.title
|
||||
article.text = text
|
||||
article.section = true
|
||||
if (!parent.linked) {
|
||||
parent.title = doc.title
|
||||
parent.text = text
|
||||
parent.linked = true
|
||||
|
||||
/* Add subsequent section */
|
||||
} else {
|
||||
documents.set(location, { location, title, text, article })
|
||||
documents.set(location, { location, title, text, parent })
|
||||
}
|
||||
|
||||
/* Add article */
|
||||
} else {
|
||||
documents.set(location, { location, title, text, section: false })
|
||||
documents.set(location, { location, title, text, linked: false })
|
||||
}
|
||||
}
|
||||
return documents
|
||||
|
||||
@@ -21,4 +21,8 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./document"
|
||||
export {
|
||||
ArticleDocument,
|
||||
SearchDocument,
|
||||
SectionDocument
|
||||
} from "./document"
|
||||
|
||||
@@ -20,4 +20,4 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./result"
|
||||
|
||||
@@ -44,7 +44,7 @@ const css = {
|
||||
/**
|
||||
* Render a search result
|
||||
*
|
||||
* @param article - Search result
|
||||
* @param result - Search result
|
||||
*
|
||||
* @return HTML element
|
||||
*/
|
||||
23
src/assets/javascripts/templates/search/result/index.ts
Normal file
23
src/assets/javascripts/templates/search/result/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2019 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 "./_"
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2019 Martin Donath <martin.donath@squidfunk.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to
|
||||
* deal in the Software without restriction, including without limitation the
|
||||
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
* sell copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Observable, fromEvent, merge } from "rxjs"
|
||||
import { mapTo, shareReplay, startWith } from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch element focus
|
||||
*
|
||||
* @param el - Element
|
||||
*
|
||||
* @return Element focus observable
|
||||
*/
|
||||
export function watchElementFocus(
|
||||
el: HTMLElement
|
||||
): Observable<boolean> {
|
||||
const focus$ = fromEvent(el, "focus")
|
||||
const blur$ = fromEvent(el, "blur")
|
||||
|
||||
/* Map events to boolean state */
|
||||
return merge(
|
||||
focus$.pipe(mapTo(true)),
|
||||
blur$.pipe(mapTo(false))
|
||||
)
|
||||
.pipe(
|
||||
startWith(el === document.activeElement),
|
||||
shareReplay(1)
|
||||
)
|
||||
}
|
||||
@@ -21,4 +21,5 @@
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./focus"
|
||||
export * from "./offset"
|
||||
|
||||
@@ -68,7 +68,7 @@ export function watchWorker<T extends WorkerMessage>(
|
||||
worker: Worker, { send$ }: Options<T>
|
||||
): Observable<T> {
|
||||
|
||||
/* Observable for messages from web worker */
|
||||
/* Intercept messages from web worker */
|
||||
const recv$ = fromEvent(worker, "message")
|
||||
.pipe(
|
||||
pluck<Event, T>("data"),
|
||||
|
||||
@@ -27,6 +27,26 @@ import { pluck } from "rxjs/operators"
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Set toggle
|
||||
*
|
||||
* Simulating a click event seems to be the most cross-browser compatible way
|
||||
* of changing the value while also emitting a `change` event. Before, Material
|
||||
* used `CustomEvent` to programatically change the value of a toggle, but this
|
||||
* is a much simpler and cleaner solution.
|
||||
*
|
||||
* @param el - Toggle element
|
||||
* @param value - Toggle value
|
||||
*/
|
||||
export function setToggle(
|
||||
el: HTMLInputElement, value: boolean
|
||||
): void {
|
||||
if (el.checked !== value)
|
||||
el.click()
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch toggle
|
||||
*
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { SearchIndex, SearchResult } from "modules"
|
||||
import { SearchIndexOptions, SearchResult } from "modules"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@@ -43,7 +43,7 @@ export const enum SearchMessageType {
|
||||
*/
|
||||
export interface SearchSetupMessage {
|
||||
type: SearchMessageType.SETUP /* Message type */
|
||||
data: SearchIndex /* Message data */
|
||||
data: SearchIndexOptions /* Message data */
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Search } from "modules"
|
||||
import { SearchIndex } from "modules"
|
||||
|
||||
import { SearchMessage, SearchMessageType } from "../_"
|
||||
|
||||
@@ -31,7 +31,7 @@ import { SearchMessage, SearchMessageType } from "../_"
|
||||
/**
|
||||
* Search index
|
||||
*/
|
||||
let index: Search
|
||||
let index: SearchIndex
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@@ -49,7 +49,7 @@ export function handler(message: SearchMessage): SearchMessage {
|
||||
|
||||
/* Setup search index */
|
||||
case SearchMessageType.SETUP:
|
||||
index = new Search(message.data)
|
||||
index = new SearchIndex(message.data)
|
||||
return {
|
||||
type: SearchMessageType.DUMP,
|
||||
data: index.toString()
|
||||
|
||||
Reference in New Issue
Block a user