Added support for keyboard handlers

This commit is contained in:
squidfunk
2020-02-02 16:19:01 +01:00
parent 9b9527f859
commit a074005b41
13 changed files with 150 additions and 31 deletions

View File

@@ -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())
}
}
/**

View File

@@ -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)
)
}

View File

@@ -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$
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(
switchMap(x => x === true ? fromEvent(window, "keydown") : NEVER),
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?

View File

@@ -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(),

View File

@@ -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
*

View File

@@ -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)
)
}