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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -954,9 +954,9 @@ hr {
transition: background 0.25s; transition: background 0.25s;
outline: 0; outline: 0;
overflow: hidden; } 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); } 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; } opacity: 0.7; }
.md-search-result__link:last-child .md-search-result__teaser { .md-search-result__link:last-child .md-search-result__teaser {
margin-bottom: 0.6rem; } margin-bottom: 0.6rem; }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -74,6 +74,8 @@ extra:
link: https://twitter.com/squidfunk link: https://twitter.com/squidfunk
- icon: brands/linkedin - icon: brands/linkedin
link: https://www.linkedin.com/in/squidfunk/ link: https://www.linkedin.com/in/squidfunk/
- icon: brands/instagram
link: https://instagram.com/squidfunk
# Extensions # Extensions
markdown_extensions: markdown_extensions:

View File

@ -35,11 +35,22 @@ import { translate } from "utilities"
export function setSearchResultMeta( export function setSearchResultMeta(
el: HTMLElement, value: number el: HTMLElement, value: number
): void { ): void {
el.textContent = value > 1 switch (value) {
? translate("search.result.other", value.toString())
: value === 1 /* No results */
? translate("search.result.one") case 0:
: translate("search.result.none") 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 { import {
EMPTY, EMPTY,
MonoTypeOperatorFunction,
Observable, Observable,
OperatorFunction, OperatorFunction,
combineLatest, combineLatest,
@ -30,8 +31,10 @@ import {
} from "rxjs" } from "rxjs"
import { import {
filter, filter,
map,
switchMap, switchMap,
takeUntil takeUntil,
withLatestFrom
} from "rxjs/operators" } 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, switchMapTo,
take, take,
tap, tap,
withLatestFrom,
distinctUntilChanged,
distinctUntilKeyChanged,
} from "rxjs/operators" } from "rxjs/operators"
import { import {
@ -71,7 +74,8 @@ import {
setToggle, setToggle,
getElements, getElements,
watchMedia, watchMedia,
translate translate,
watchElementFocus
} from "./utilities" } from "./utilities"
import { import {
PackerMessage, PackerMessage,
@ -83,7 +87,7 @@ import {
isSearchResultMessage isSearchResultMessage
} from "./workers" } from "./workers"
import { renderSource } from "templates" import { renderSource } from "templates"
import { switchMapIf, not } from "extensions" import { switchMapIf, not, takeIf } from "extensions"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
@ -239,7 +243,7 @@ function setupWorkers(config: Config) {
* Yes, this is a super hacky implementation. Needs clean up. * Yes, this is a super hacky implementation. Needs clean up.
*/ */
function repository() { function repository() {
const el = getElement<HTMLAnchorElement>("[data-md-source][href]") const el = getElement<HTMLAnchorElement>(".md-source[href]") // TODO: dont use classes
console.log(el) console.log(el)
if (!el) if (!el)
return EMPTY return EMPTY
@ -326,7 +330,7 @@ export function initialize(config: unknown) {
// TODO: WIP repo rendering // TODO: WIP repo rendering
repository().subscribe(facts => { repository().subscribe(facts => {
if (facts.length) { if (facts.length) {
const sources = getElements("[data-md-source] .md-source__repository") const sources = getElements(".md-source__repository")
sources.forEach(repo => { sources.forEach(repo => {
repo.dataset.mdState = "done" repo.dataset.mdState = "done"
repo.appendChild( repo.appendChild(
@ -396,6 +400,7 @@ export function initialize(config: unknown) {
type: SearchMessageType.QUERY, type: SearchMessageType.QUERY,
data: query.value data: query.value
})), // TODO. ugly... })), // TODO. ugly...
distinctUntilKeyChanged("data")
// distinctUntilKeyChanged("data") // distinctUntilKeyChanged("data")
) )
.subscribe(searchMessage$) .subscribe(searchMessage$)
@ -432,7 +437,10 @@ export function initialize(config: unknown) {
// TODO: naming? // TODO: naming?
const resultComponent$ = component("search-result") const resultComponent$ = component("search-result")
.pipe( .pipe(
mountSearchResult(agent, { result$, query$: query$.pipe(pluck("value")) }) mountSearchResult(agent, { result$, query$: query$.pipe(
distinctUntilKeyChanged("value"),
pluck("value")
) })
) // temporary fix ) // temporary fix
const tabs$ = component("tabs") const tabs$ = component("tabs")
@ -451,7 +459,7 @@ export function initialize(config: unknown) {
const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")! const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")!
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")! const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
const a$ = watchToggle(search) const searchActive$ = watchToggle(search)
.pipe( .pipe(
delay(400) delay(400)
) )
@ -461,15 +469,90 @@ export function initialize(config: unknown) {
switchMap(watchSearchReset) switchMap(watchSearchReset)
) )
/* Listener: focus query if search is open and character is typed */ const key$ = fromEvent<KeyboardEvent>(window, "keydown").pipe(
// TODO: combine with watchElementFocus filter(ev => !(ev.metaKey || ev.ctrlKey))
const keysIfSearchActive$ = a$ )
// 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( .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 // 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( .pipe(
switchMapTo(component<HTMLInputElement>("search-query")), switchMapTo(component<HTMLInputElement>("search-query")),
tap(el => el.focus()) // TODO: only if element isnt focused! setFocus? setToggle? tap(el => el.focus()) // TODO: only if element isnt focused! setFocus? setToggle?

View File

@ -56,8 +56,8 @@ export interface AgentLocation {
* Agent media * Agent media
*/ */
export interface AgentMedia { export interface AgentMedia {
screen$: Observable<boolean> /* Media observable for screen */
tablet$: Observable<boolean> /* Media observable for tablet */ tablet$: Observable<boolean> /* Media observable for tablet */
screen$: Observable<boolean> /* Media observable for screen */
} }
/** /**
@ -102,8 +102,8 @@ export function setupAgent(): Agent {
hash$: watchLocationHash() hash$: watchLocationHash()
}, },
media: { media: {
screen$: watchMedia("(min-width: 1220px)"), tablet$: watchMedia("(min-width: 960px)"),
tablet$: watchMedia("(min-width: 960px)") screen$: watchMedia("(min-width: 1220px)")
}, },
viewport: { viewport: {
offset$: watchViewportOffset(), 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 * 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 * 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 * to the same page, the request is effectively ignored (e.g. when only the
* fragment identifier changes) * fragment identifier changes).
* *
* @param options - Options * @param options - Options
* *

View File

@ -21,7 +21,7 @@
*/ */
import { Observable, fromEvent } from "rxjs" import { Observable, fromEvent } from "rxjs"
import { map } from "rxjs/operators" import { map, startWith } from "rxjs/operators"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
@ -59,6 +59,7 @@ export function watchToggle(
): Observable<boolean> { ): Observable<boolean> {
return fromEvent(el, "change") return fromEvent(el, "change")
.pipe( .pipe(
map(() => el.checked) map(() => el.checked),
startWith(el.checked)
) )
} }

View File

@ -523,8 +523,8 @@ $md-toggle__search--checked:
outline: 0; outline: 0;
overflow: hidden; overflow: hidden;
// Active or hovered link // Focused or hovered link
&[data-md-state="active"], &:focus,
&:hover { &:hover {
background-color: transparentize($md-color-accent, 0.9); background-color: transparentize($md-color-accent, 0.9);