mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-30 18:24:35 +01:00
Added support for keyboard handlers
This commit is contained in:
parent
9b9527f859
commit
a074005b41
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -954,9 +954,9 @@ hr {
|
||||
transition: background 0.25s;
|
||||
outline: 0;
|
||||
overflow: hidden; }
|
||||
.md-search-result__link[data-md-state="active"], .md-search-result__link:hover {
|
||||
.md-search-result__link:focus, .md-search-result__link:hover {
|
||||
background-color: rgba(83, 109, 254, 0.1); }
|
||||
.md-search-result__link[data-md-state="active"] .md-search-result__article::before, .md-search-result__link:hover .md-search-result__article::before {
|
||||
.md-search-result__link:focus .md-search-result__article::before, .md-search-result__link:hover .md-search-result__article::before {
|
||||
opacity: 0.7; }
|
||||
.md-search-result__link:last-child .md-search-result__teaser {
|
||||
margin-bottom: 0.6rem; }
|
||||
|
2
material/assets/stylesheets/app.min.css
vendored
2
material/assets/stylesheets/app.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -74,6 +74,8 @@ extra:
|
||||
link: https://twitter.com/squidfunk
|
||||
- icon: brands/linkedin
|
||||
link: https://www.linkedin.com/in/squidfunk/
|
||||
- icon: brands/instagram
|
||||
link: https://instagram.com/squidfunk
|
||||
|
||||
# Extensions
|
||||
markdown_extensions:
|
||||
|
@ -35,11 +35,22 @@ import { translate } from "utilities"
|
||||
export function setSearchResultMeta(
|
||||
el: HTMLElement, value: number
|
||||
): void {
|
||||
el.textContent = value > 1
|
||||
? translate("search.result.other", value.toString())
|
||||
: value === 1
|
||||
? translate("search.result.one")
|
||||
: translate("search.result.none")
|
||||
switch (value) {
|
||||
|
||||
/* No results */
|
||||
case 0:
|
||||
el.textContent = translate("search.result.none")
|
||||
break
|
||||
|
||||
/* One result */
|
||||
case 1:
|
||||
el.textContent = translate("search.result.one")
|
||||
break
|
||||
|
||||
/* Multiple result */
|
||||
default:
|
||||
el.textContent = translate("search.result.other", value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
import {
|
||||
EMPTY,
|
||||
MonoTypeOperatorFunction,
|
||||
Observable,
|
||||
OperatorFunction,
|
||||
combineLatest,
|
||||
@ -30,8 +31,10 @@ import {
|
||||
} from "rxjs"
|
||||
import {
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
takeUntil
|
||||
takeUntil,
|
||||
withLatestFrom
|
||||
} from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
@ -65,3 +68,22 @@ export function switchMapIf<T, U>(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle emission with another observable
|
||||
*
|
||||
* @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)
|
||||
)
|
||||
}
|
||||
|
@ -45,6 +45,9 @@ import {
|
||||
switchMapTo,
|
||||
take,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
distinctUntilChanged,
|
||||
distinctUntilKeyChanged,
|
||||
} from "rxjs/operators"
|
||||
|
||||
import {
|
||||
@ -71,7 +74,8 @@ import {
|
||||
setToggle,
|
||||
getElements,
|
||||
watchMedia,
|
||||
translate
|
||||
translate,
|
||||
watchElementFocus
|
||||
} from "./utilities"
|
||||
import {
|
||||
PackerMessage,
|
||||
@ -83,7 +87,7 @@ import {
|
||||
isSearchResultMessage
|
||||
} from "./workers"
|
||||
import { renderSource } from "templates"
|
||||
import { switchMapIf, not } from "extensions"
|
||||
import { switchMapIf, not, takeIf } from "extensions"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
@ -239,7 +243,7 @@ function setupWorkers(config: Config) {
|
||||
* Yes, this is a super hacky implementation. Needs clean up.
|
||||
*/
|
||||
function repository() {
|
||||
const el = getElement<HTMLAnchorElement>("[data-md-source][href]")
|
||||
const el = getElement<HTMLAnchorElement>(".md-source[href]") // TODO: dont use classes
|
||||
console.log(el)
|
||||
if (!el)
|
||||
return EMPTY
|
||||
@ -326,7 +330,7 @@ export function initialize(config: unknown) {
|
||||
// TODO: WIP repo rendering
|
||||
repository().subscribe(facts => {
|
||||
if (facts.length) {
|
||||
const sources = getElements("[data-md-source] .md-source__repository")
|
||||
const sources = getElements(".md-source__repository")
|
||||
sources.forEach(repo => {
|
||||
repo.dataset.mdState = "done"
|
||||
repo.appendChild(
|
||||
@ -396,6 +400,7 @@ export function initialize(config: unknown) {
|
||||
type: SearchMessageType.QUERY,
|
||||
data: query.value
|
||||
})), // TODO. ugly...
|
||||
distinctUntilKeyChanged("data")
|
||||
// distinctUntilKeyChanged("data")
|
||||
)
|
||||
.subscribe(searchMessage$)
|
||||
@ -432,7 +437,10 @@ export function initialize(config: unknown) {
|
||||
// TODO: naming?
|
||||
const resultComponent$ = component("search-result")
|
||||
.pipe(
|
||||
mountSearchResult(agent, { result$, query$: query$.pipe(pluck("value")) })
|
||||
mountSearchResult(agent, { result$, query$: query$.pipe(
|
||||
distinctUntilKeyChanged("value"),
|
||||
pluck("value")
|
||||
) })
|
||||
) // temporary fix
|
||||
|
||||
const tabs$ = component("tabs")
|
||||
@ -451,7 +459,7 @@ export function initialize(config: unknown) {
|
||||
const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")!
|
||||
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
|
||||
|
||||
const a$ = watchToggle(search)
|
||||
const searchActive$ = watchToggle(search)
|
||||
.pipe(
|
||||
delay(400)
|
||||
)
|
||||
@ -461,15 +469,90 @@ export function initialize(config: unknown) {
|
||||
switchMap(watchSearchReset)
|
||||
)
|
||||
|
||||
/* Listener: focus query if search is open and character is typed */
|
||||
// TODO: combine with watchElementFocus
|
||||
const keysIfSearchActive$ = a$
|
||||
.pipe(
|
||||
switchMap(x => x === true ? fromEvent(window, "keydown") : NEVER),
|
||||
const key$ = fromEvent<KeyboardEvent>(window, "keydown").pipe(
|
||||
filter(ev => !(ev.metaKey || ev.ctrlKey))
|
||||
)
|
||||
|
||||
// search mode is active!
|
||||
key$.pipe(
|
||||
takeIf(searchActive$)
|
||||
// switchMapIf(searchActive$, x => {
|
||||
// console.log("search mode!", x)
|
||||
// return EMPTY
|
||||
// })
|
||||
)
|
||||
.subscribe(x => console.log("search mode", x))
|
||||
|
||||
// filter arrow keys if search is active!
|
||||
searchActive$.subscribe(console.log)
|
||||
|
||||
// shortcodes
|
||||
key$
|
||||
.pipe(
|
||||
takeIf(not(searchActive$))
|
||||
)
|
||||
.subscribe(ev => {
|
||||
if (
|
||||
document.activeElement && (
|
||||
["TEXTAREA", "SELECT", "INPUT"].includes(
|
||||
document.activeElement.tagName
|
||||
) ||
|
||||
document.activeElement instanceof HTMLElement &&
|
||||
document.activeElement.isContentEditable
|
||||
)
|
||||
) {
|
||||
// do nothing...
|
||||
} else {
|
||||
if (ev.keyCode === 70 || ev.keyCode === 83) {
|
||||
setToggle(search, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
// check which element is focused...
|
||||
// note that all links have tabindex=-1
|
||||
key$
|
||||
.pipe(
|
||||
takeIf(searchActive$),
|
||||
|
||||
/* Abort if meta key (macOS) or ctrl key (Windows) is pressed */
|
||||
tap(ev => {
|
||||
if (ev.key === "Enter") {
|
||||
if (document.activeElement === getElement("[data-md-component=search-query]")) {
|
||||
ev.preventDefault()
|
||||
// intercept hash change after search closed
|
||||
} else {
|
||||
setToggle(search, false)
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
|
||||
const active = getElements("[data-md-component=search-query], [data-md-component=search-result] [href]")
|
||||
const i = Math.max(0, active.findIndex(el => el === document.activeElement))
|
||||
const x = Math.max(0, (i + active.length + (ev.keyCode === 38 ? -1 : +1)) % active.length)
|
||||
active[x].focus()
|
||||
|
||||
/* Prevent scrolling of page */
|
||||
ev.preventDefault()
|
||||
ev.stopPropagation()
|
||||
|
||||
} else if (ev.key === "Escape" || ev.key === "Tab") {
|
||||
setToggle(search, false)
|
||||
getElement("[data-md-component=search-query]")!.blur()
|
||||
|
||||
} else {
|
||||
if (search.checked && document.activeElement !== getElement("[data-md-component=search-query]")) {
|
||||
getElement("[data-md-component=search-query]")!.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
// TODO: close search on hashchange
|
||||
// anchor jump -> always close drawer + search
|
||||
|
||||
// focus search on reset, on toggle and on keypress if open
|
||||
merge(a$.pipe(filter(identity)), reset$, keysIfSearchActive$)
|
||||
merge(searchActive$.pipe(filter(identity)), reset$)
|
||||
.pipe(
|
||||
switchMapTo(component<HTMLInputElement>("search-query")),
|
||||
tap(el => el.focus()) // TODO: only if element isnt focused! setFocus? setToggle?
|
||||
|
@ -56,8 +56,8 @@ export interface AgentLocation {
|
||||
* Agent media
|
||||
*/
|
||||
export interface AgentMedia {
|
||||
screen$: Observable<boolean> /* Media observable for screen */
|
||||
tablet$: Observable<boolean> /* Media observable for tablet */
|
||||
screen$: Observable<boolean> /* Media observable for screen */
|
||||
}
|
||||
|
||||
/**
|
||||
@ -102,8 +102,8 @@ export function setupAgent(): Agent {
|
||||
hash$: watchLocationHash()
|
||||
},
|
||||
media: {
|
||||
screen$: watchMedia("(min-width: 1220px)"),
|
||||
tablet$: watchMedia("(min-width: 960px)")
|
||||
tablet$: watchMedia("(min-width: 960px)"),
|
||||
screen$: watchMedia("(min-width: 1220px)")
|
||||
},
|
||||
viewport: {
|
||||
offset$: watchViewportOffset(),
|
||||
|
@ -76,7 +76,7 @@ export function watchDocument(): Observable<Document> {
|
||||
* This function returns an observables that fetches a document if the provided
|
||||
* location observable emits a new value (i.e. URL). If the emitted URL points
|
||||
* to the same page, the request is effectively ignored (e.g. when only the
|
||||
* fragment identifier changes)
|
||||
* fragment identifier changes).
|
||||
*
|
||||
* @param options - Options
|
||||
*
|
||||
|
@ -21,7 +21,7 @@
|
||||
*/
|
||||
|
||||
import { Observable, fromEvent } from "rxjs"
|
||||
import { map } from "rxjs/operators"
|
||||
import { map, startWith } from "rxjs/operators"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
@ -59,6 +59,7 @@ export function watchToggle(
|
||||
): Observable<boolean> {
|
||||
return fromEvent(el, "change")
|
||||
.pipe(
|
||||
map(() => el.checked)
|
||||
map(() => el.checked),
|
||||
startWith(el.checked)
|
||||
)
|
||||
}
|
||||
|
@ -523,8 +523,8 @@ $md-toggle__search--checked:
|
||||
outline: 0;
|
||||
overflow: hidden;
|
||||
|
||||
// Active or hovered link
|
||||
&[data-md-state="active"],
|
||||
// Focused or hovered link
|
||||
&:focus,
|
||||
&:hover {
|
||||
background-color: transparentize($md-color-accent, 0.9);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user