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;
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; }

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
- icon: brands/linkedin
link: https://www.linkedin.com/in/squidfunk/
- icon: brands/instagram
link: https://instagram.com/squidfunk
# Extensions
markdown_extensions:

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

View File

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