mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-08-04 03:25:13 -04:00
Improved keyboard handlers and added prev/next hotkeys
This commit is contained in:
@@ -79,7 +79,7 @@ import {
|
||||
patchScrollfix,
|
||||
patchSource
|
||||
} from "patches"
|
||||
import { takeIf, not, isConfig } from "utilities"
|
||||
import { isConfig } from "utilities"
|
||||
import { renderDialog } from "templates/dialog"
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@@ -108,7 +108,6 @@ export function initialize(config: unknown) {
|
||||
const document$ = watchDocument()
|
||||
const location$ = watchLocation()
|
||||
const hash$ = watchLocationHash()
|
||||
const keyboard$ = watchKeyboard()
|
||||
const viewport$ = watchViewport()
|
||||
const tablet$ = watchMedia("(min-width: 960px)")
|
||||
const screen$ = watchMedia("(min-width: 1220px)")
|
||||
@@ -187,7 +186,7 @@ export function initialize(config: unknown) {
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
setupKeyboard({ keyboard$ })
|
||||
const keyboard$ = setupKeyboard()
|
||||
|
||||
patchTables({ document$ })
|
||||
patchDetails({ document$, hash$ })
|
||||
@@ -215,10 +214,7 @@ export function initialize(config: unknown) {
|
||||
)
|
||||
})
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
// TODO: general keyboard handler...
|
||||
// put into main!?
|
||||
.subscribe()
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
@@ -279,8 +275,7 @@ export function initialize(config: unknown) {
|
||||
// TODO: experimental. necessary!?
|
||||
keyboard$
|
||||
.pipe(
|
||||
takeIf(not(toggle$.pipe(switchMap(watchToggle)))),
|
||||
filter(key => ["Tab"].includes(key.type)),
|
||||
filter(key => key.mode === "global" && ["Tab"].includes(key.type)),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => {
|
||||
|
||||
@@ -21,30 +21,46 @@
|
||||
*/
|
||||
|
||||
import { Observable } from "rxjs"
|
||||
import { switchMap, withLatestFrom } from "rxjs/operators"
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
share,
|
||||
switchMap,
|
||||
withLatestFrom
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { useComponent } from "components"
|
||||
import {
|
||||
Key,
|
||||
getActiveElement,
|
||||
getElement,
|
||||
getElements,
|
||||
isSusceptibleToKeyboard,
|
||||
setElementFocus,
|
||||
setToggle,
|
||||
useToggle,
|
||||
watchKeyboard,
|
||||
watchToggle
|
||||
} from "observables"
|
||||
import { not, takeIf } from "utilities"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Setup options
|
||||
* Keyboard mode
|
||||
*/
|
||||
interface SetupOptions {
|
||||
keyboard$: Observable<Key> /* Keyboard observable */
|
||||
export type KeyboardMode =
|
||||
| "global" /* Global */
|
||||
| "search" /* Search is open */
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Keyboard
|
||||
*/
|
||||
export interface Keyboard extends Key {
|
||||
mode: KeyboardMode /* Keyboard mode */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@@ -54,17 +70,44 @@ interface SetupOptions {
|
||||
/**
|
||||
* Setup keyboard
|
||||
*
|
||||
* This function will setup the keyboard handlers and ensure that keys are
|
||||
* correctly propagated. Currently there are two modes:
|
||||
*
|
||||
* - `global`: This mode is active when the search is closed. It is intended
|
||||
* to assign hotkeys to specific functions of the site. Currently the search,
|
||||
* previous and next page can be triggered.
|
||||
*
|
||||
* - `search`: This mode is active when the search is open. It maps certain
|
||||
* navigational keys to offer search results that can be entirely navigated
|
||||
* through keyboard input.
|
||||
*
|
||||
* The keyboard observable is returned and can be used to monitor the keyboard
|
||||
* in order toassign further hotkeys to custom functions.
|
||||
*
|
||||
* @return Keyboard observable
|
||||
*/
|
||||
export function setupKeyboard(
|
||||
{ keyboard$ }: SetupOptions
|
||||
): Observable<Key> {
|
||||
|
||||
/* Setup keyboard handlers in search mode */
|
||||
export function setupKeyboard(): Observable<Keyboard> {
|
||||
const toggle$ = useToggle("search")
|
||||
const search$ = toggle$
|
||||
.pipe(
|
||||
switchMap(watchToggle)
|
||||
)
|
||||
|
||||
/* Setup keyboard and determine mode */
|
||||
const keyboard$ = watchKeyboard()
|
||||
.pipe(
|
||||
withLatestFrom(search$),
|
||||
map(([key, toggle]): Keyboard => ({
|
||||
mode: toggle ? "search" : "global",
|
||||
...key
|
||||
})),
|
||||
share()
|
||||
)
|
||||
|
||||
/* Setup search keyboard handlers */
|
||||
keyboard$
|
||||
.pipe(
|
||||
takeIf(toggle$.pipe(switchMap(watchToggle))),
|
||||
filter(({ mode }) => mode === "search"),
|
||||
withLatestFrom(
|
||||
toggle$,
|
||||
useComponent("search-query"),
|
||||
@@ -114,23 +157,43 @@ export function setupKeyboard(
|
||||
}
|
||||
})
|
||||
|
||||
/* Setup general keyboard handlers */
|
||||
/* Setup global keyboard handlers */
|
||||
keyboard$
|
||||
.pipe(
|
||||
takeIf(not(toggle$.pipe(switchMap(watchToggle)))),
|
||||
filter(({ mode }) => {
|
||||
if (mode === "global") {
|
||||
const active = getActiveElement()
|
||||
if (typeof active !== "undefined")
|
||||
return !isSusceptibleToKeyboard(active)
|
||||
}
|
||||
return false
|
||||
}),
|
||||
withLatestFrom(useComponent("search-query"))
|
||||
)
|
||||
.subscribe(([key, query]) => {
|
||||
const active = getActiveElement()
|
||||
switch (key.type) {
|
||||
|
||||
/* [s]earch / [f]ind: open search */
|
||||
case "s":
|
||||
/* Open search */
|
||||
case "f":
|
||||
if (!(active && isSusceptibleToKeyboard(active))) {
|
||||
setElementFocus(query)
|
||||
key.claim()
|
||||
}
|
||||
case "s":
|
||||
setElementFocus(query)
|
||||
key.claim()
|
||||
break
|
||||
|
||||
/* 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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
switchMap
|
||||
} from "rxjs/operators"
|
||||
|
||||
import { getLocation } from "../../location"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
@@ -64,7 +66,7 @@ export function watchDocumentSwitch(
|
||||
): Observable<Document> {
|
||||
return location$
|
||||
.pipe(
|
||||
startWith(location.href),
|
||||
startWith(getLocation()),
|
||||
map(url => url.replace(/#[^#]+$/, "")),
|
||||
distinctUntilChanged(),
|
||||
skip(1),
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2020 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, defer, of } from "rxjs"
|
||||
import { map } from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Invert the value of a toggle observable
|
||||
*
|
||||
* @param toggle$ - Toggle observable
|
||||
*
|
||||
* @return Inverted toggle observable
|
||||
*/
|
||||
export function not(
|
||||
toggle$: Observable<boolean>
|
||||
): Observable<boolean> {
|
||||
return toggle$
|
||||
.pipe(
|
||||
map(active => !active)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the last value emitted by an observable in session storage
|
||||
*
|
||||
* If the key is not found in session storage, the factory is executed and the
|
||||
* latest value emitted will automatically be persisted to sessions storage.
|
||||
* Note that the values emitted by the returned observable must be serializable
|
||||
* as `JSON`, or data will be lost.
|
||||
*
|
||||
* @template T - Value type
|
||||
*
|
||||
* @param key - Cache key
|
||||
* @param factory - Observable factory
|
||||
*
|
||||
* @return Value observable
|
||||
*/
|
||||
export function cache<T>(
|
||||
key: string, factory: () => Observable<T>
|
||||
): Observable<T> {
|
||||
return defer(() => {
|
||||
const data = sessionStorage.getItem(key)
|
||||
if (data) {
|
||||
return of(JSON.parse(data) as T)
|
||||
|
||||
/* Retrieve value from observable factory and write to storage */
|
||||
} else {
|
||||
const value$ = factory()
|
||||
value$
|
||||
.subscribe(value => {
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(value))
|
||||
} catch (err) {
|
||||
/* Uncritical, just swallow */
|
||||
}
|
||||
})
|
||||
|
||||
/* Return value observable */
|
||||
return value$
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -20,5 +20,49 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./operators"
|
||||
import { Observable, defer, of } from "rxjs"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Cache the last value emitted by an observable in session storage
|
||||
*
|
||||
* If the key is not found in session storage, the factory is executed and the
|
||||
* latest value emitted will automatically be persisted to sessions storage.
|
||||
* Note that the values emitted by the returned observable must be serializable
|
||||
* as `JSON`, or data will be lost.
|
||||
*
|
||||
* @template T - Value type
|
||||
*
|
||||
* @param key - Cache key
|
||||
* @param factory - Observable factory
|
||||
*
|
||||
* @return Value observable
|
||||
*/
|
||||
export function cache<T>(
|
||||
key: string, factory: () => Observable<T>
|
||||
): Observable<T> {
|
||||
return defer(() => {
|
||||
const data = sessionStorage.getItem(key)
|
||||
if (data) {
|
||||
return of(JSON.parse(data) as T)
|
||||
|
||||
/* Retrieve value from observable factory and write to storage */
|
||||
} else {
|
||||
const value$ = factory()
|
||||
value$
|
||||
.subscribe(value => {
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(value))
|
||||
} catch (err) {
|
||||
/* Uncritical, just swallow */
|
||||
}
|
||||
})
|
||||
|
||||
/* Return value observable */
|
||||
return value$
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2016-2020 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 { MonoTypeOperatorFunction, Observable, pipe } from "rxjs"
|
||||
import { filter, map, withLatestFrom } from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Toggle emission with another observable
|
||||
*
|
||||
* While this could also be implemented using window operators, it may lead to
|
||||
* an unnecessary increase in bundle size, so we use operators we use anyway.
|
||||
*
|
||||
* @template T - Value type
|
||||
*
|
||||
* @param toggle$ - Toggle observable
|
||||
*
|
||||
* @return Operator function
|
||||
*/
|
||||
export function takeIf<T>(
|
||||
toggle$: Observable<boolean>
|
||||
): MonoTypeOperatorFunction<T> {
|
||||
return pipe(
|
||||
withLatestFrom(toggle$),
|
||||
filter(([, active]) => active),
|
||||
map(([value]) => value)
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user