mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-24 23:04:32 -04:00
Added back keyboard handlers
This commit is contained in:
@@ -23,20 +23,33 @@
|
||||
import { Observable, fromEvent } from "rxjs"
|
||||
import { filter, map, share } from "rxjs/operators"
|
||||
|
||||
import { getActiveElement } from "../element"
|
||||
import { getToggle } from "../toggle"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Key
|
||||
* Keyboard mode
|
||||
*/
|
||||
export interface Key {
|
||||
export type KeyboardMode =
|
||||
| "global" /* Global */
|
||||
| "search" /* Search is open */
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Keyboard
|
||||
*/
|
||||
export interface Keyboard {
|
||||
mode: KeyboardMode /* Keyboard mode */
|
||||
type: string /* Key type */
|
||||
claim(): void /* Key claim */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
@@ -46,7 +59,7 @@ export interface Key {
|
||||
*
|
||||
* @returns Test result
|
||||
*/
|
||||
export function isSusceptibleToKeyboard(el: HTMLElement): boolean {
|
||||
function isSusceptibleToKeyboard(el: HTMLElement): boolean {
|
||||
switch (el.tagName) {
|
||||
|
||||
/* Form elements */
|
||||
@@ -61,24 +74,35 @@ export function isSusceptibleToKeyboard(el: HTMLElement): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch keyboard
|
||||
*
|
||||
* @returns Keyboard observable
|
||||
*/
|
||||
export function watchKeyboard(): Observable<Key> {
|
||||
export function watchKeyboard(): Observable<Keyboard> {
|
||||
return fromEvent<KeyboardEvent>(window, "keydown")
|
||||
.pipe(
|
||||
filter(ev => !(ev.metaKey || ev.ctrlKey)),
|
||||
map(ev => ({
|
||||
mode: getToggle("search") ? "search" : "global",
|
||||
type: ev.key,
|
||||
claim() {
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
}
|
||||
})),
|
||||
} as Keyboard)),
|
||||
filter(({ mode }) => {
|
||||
if (mode === "global") {
|
||||
const active = getActiveElement()
|
||||
if (typeof active !== "undefined")
|
||||
return !isSusceptibleToKeyboard(active)
|
||||
}
|
||||
return true
|
||||
}),
|
||||
share()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,8 +25,14 @@ import { filter, sample, take } from "rxjs/operators"
|
||||
|
||||
import { configuration } from "~/_"
|
||||
import {
|
||||
Keyboard,
|
||||
getActiveElement,
|
||||
getElementOrThrow,
|
||||
requestJSON
|
||||
getElements,
|
||||
requestJSON,
|
||||
setElementFocus,
|
||||
setElementSelection,
|
||||
setToggle
|
||||
} from "~/browser"
|
||||
import {
|
||||
SearchIndex,
|
||||
@@ -50,6 +56,17 @@ export type Search =
|
||||
| SearchQuery
|
||||
| SearchResult
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
keyboard$: Observable<Keyboard> /* Keyboard observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -73,17 +90,22 @@ function fetchSearchIndex(url: string) {
|
||||
* Mount search
|
||||
*
|
||||
* @param el - Search element
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Search component observable
|
||||
*/
|
||||
export function mountSearch(
|
||||
el: HTMLElement
|
||||
el: HTMLElement, { keyboard$ }: MountOptions
|
||||
): Observable<Component<Search>> {
|
||||
const config = configuration()
|
||||
const worker = setupSearchWorker(config.search, fetchSearchIndex(
|
||||
`${config.base}/search/search_index.json`
|
||||
))
|
||||
|
||||
/* Retrieve elements */
|
||||
const query = getElementOrThrow("[data-md-component=search-query]", el)
|
||||
const result = getElementOrThrow("[data-md-component=search-result]", el)
|
||||
|
||||
/* Re-emit query when search is ready */
|
||||
const { tx$, rx$ } = worker
|
||||
tx$
|
||||
@@ -94,18 +116,80 @@ export function mountSearch(
|
||||
)
|
||||
.subscribe(tx$.next.bind(tx$))
|
||||
|
||||
/* Mount search query component */
|
||||
const query$ = mountSearchQuery(
|
||||
getElementOrThrow("[data-md-component=search-query]", el),
|
||||
worker
|
||||
)
|
||||
/* Set up search keyboard handlers */
|
||||
keyboard$
|
||||
.pipe(
|
||||
filter(({ mode }) => mode === "search")
|
||||
)
|
||||
.subscribe(key => {
|
||||
const active = getActiveElement()
|
||||
switch (key.type) {
|
||||
|
||||
/* Mount search result and return component */
|
||||
/* Enter: prevent form submission */
|
||||
case "Enter":
|
||||
if (active === query)
|
||||
key.claim()
|
||||
break
|
||||
|
||||
/* Escape or Tab: close search */
|
||||
case "Escape":
|
||||
case "Tab":
|
||||
setToggle("search", false)
|
||||
setElementFocus(query, false)
|
||||
break
|
||||
|
||||
/* Vertical arrows: select previous or next search result */
|
||||
case "ArrowUp":
|
||||
case "ArrowDown":
|
||||
if (typeof active === "undefined") {
|
||||
setElementFocus(query)
|
||||
} else {
|
||||
const els = [query, ...getElements(
|
||||
":not(details) > [href], summary, details[open] [href]",
|
||||
result
|
||||
)]
|
||||
const i = Math.max(0, (
|
||||
Math.max(0, els.indexOf(active)) + els.length + (
|
||||
key.type === "ArrowUp" ? -1 : +1
|
||||
)
|
||||
) % els.length)
|
||||
setElementFocus(els[i])
|
||||
}
|
||||
|
||||
/* Prevent scrolling of page */
|
||||
key.claim()
|
||||
break
|
||||
|
||||
/* All other keys: hand to search query */
|
||||
default:
|
||||
if (query !== getActiveElement())
|
||||
setElementFocus(query)
|
||||
}
|
||||
})
|
||||
|
||||
/* Set up global keyboard handlers */
|
||||
keyboard$
|
||||
.pipe(
|
||||
filter(({ mode }) => mode === "global"),
|
||||
)
|
||||
.subscribe(key => {
|
||||
switch (key.type) {
|
||||
|
||||
/* Open search and select query */
|
||||
case "f":
|
||||
case "s":
|
||||
case "/":
|
||||
setElementFocus(query)
|
||||
setElementSelection(query)
|
||||
key.claim()
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
/* Create and return component */
|
||||
const query$ = mountSearchQuery(query as HTMLInputElement, worker)
|
||||
return merge(
|
||||
query$,
|
||||
mountSearchResult(
|
||||
getElementOrThrow("[data-md-component=search-result]", el),
|
||||
worker, { query$ }
|
||||
)
|
||||
mountSearchResult(result, worker, { query$ })
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import "focus-visible"
|
||||
import { Subject, defer, merge } from "rxjs"
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
mergeWith,
|
||||
shareReplay,
|
||||
@@ -32,9 +33,11 @@ import {
|
||||
import { feature } from "./_"
|
||||
import {
|
||||
at,
|
||||
getElement,
|
||||
getElementOrThrow,
|
||||
getElements,
|
||||
watchDocument,
|
||||
watchKeyboard,
|
||||
watchLocation,
|
||||
watchLocationTarget,
|
||||
watchMedia,
|
||||
@@ -64,7 +67,7 @@ import {
|
||||
} from "./patches"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Program
|
||||
* Application
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Yay, JavaScript is available */
|
||||
@@ -75,6 +78,7 @@ document.documentElement.classList.add("js")
|
||||
const document$ = watchDocument()
|
||||
const location$ = watchLocation()
|
||||
const target$ = watchLocationTarget()
|
||||
const keyboard$ = watchKeyboard()
|
||||
|
||||
/* Set up media observables */
|
||||
const viewport$ = watchViewport()
|
||||
@@ -90,6 +94,32 @@ setupClipboardJS({ alert$ })
|
||||
if (feature("navigation.instant"))
|
||||
setupInstantLoading({ document$, location$, viewport$ })
|
||||
|
||||
/* Set up global keyboard handlers */
|
||||
keyboard$
|
||||
.pipe(
|
||||
filter(({ mode }) => mode === "global")
|
||||
)
|
||||
.subscribe(key => {
|
||||
switch (key.type) {
|
||||
|
||||
/* Go to previous page */
|
||||
case "p":
|
||||
case ",":
|
||||
const prev = getElement("[href][rel=prev]")
|
||||
if (typeof prev !== "undefined")
|
||||
prev.click()
|
||||
break
|
||||
|
||||
/* Go to next page */
|
||||
case "n":
|
||||
case ".":
|
||||
const next = getElement("[href][rel=next]")
|
||||
if (typeof next !== "undefined")
|
||||
next.click()
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
/* Set up patches */
|
||||
patchIndeterminate({ document$ })
|
||||
patchScrollfix({ document$ })
|
||||
@@ -121,7 +151,7 @@ const control$ = merge(
|
||||
|
||||
/* Search */
|
||||
...getElements("[data-md-component=search]")
|
||||
.map(child => mountSearch(child)),
|
||||
.map(child => mountSearch(child, { keyboard$ })),
|
||||
|
||||
/* Repository information */
|
||||
...getElements("[data-md-component=source]")
|
||||
@@ -162,16 +192,18 @@ const component$ = document$
|
||||
mergeWith(control$)
|
||||
)
|
||||
|
||||
/* Subscribe to all components */
|
||||
component$.subscribe()
|
||||
|
||||
/* Export to window */
|
||||
export {
|
||||
document$,
|
||||
component$,
|
||||
viewport$,
|
||||
location$,
|
||||
target$,
|
||||
screen$,
|
||||
keyboard$,
|
||||
viewport$,
|
||||
tablet$,
|
||||
print$
|
||||
screen$,
|
||||
print$,
|
||||
component$
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user