Added back keyboard handlers

This commit is contained in:
squidfunk 2021-02-12 16:41:33 +01:00
parent 44751785a0
commit e7850ca184
13 changed files with 177 additions and 37 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"assets/javascripts/bundle.js": "assets/javascripts/bundle.4fa4ebaf.min.js", "assets/javascripts/bundle.js": "assets/javascripts/bundle.d8c6976f.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.4fa4ebaf.min.js.map", "assets/javascripts/bundle.js.map": "assets/javascripts/bundle.d8c6976f.min.js.map",
"assets/javascripts/vendor.js": "assets/javascripts/vendor.e32ed4d0.min.js", "assets/javascripts/vendor.js": "assets/javascripts/vendor.e32ed4d0.min.js",
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.e32ed4d0.min.js.map", "assets/javascripts/vendor.js.map": "assets/javascripts/vendor.e32ed4d0.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.b9424174.min.js", "assets/javascripts/worker/search.js": "assets/javascripts/worker/search.b9424174.min.js",
@ -9,8 +9,8 @@
"assets/stylesheets/main.css.map": "assets/stylesheets/main.a86e6725.min.css.map", "assets/stylesheets/main.css.map": "assets/stylesheets/main.a86e6725.min.css.map",
"assets/stylesheets/palette.css": "assets/stylesheets/palette.e70b70b6.min.css", "assets/stylesheets/palette.css": "assets/stylesheets/palette.e70b70b6.min.css",
"assets/stylesheets/palette.css.map": "assets/stylesheets/palette.e70b70b6.min.css.map", "assets/stylesheets/palette.css.map": "assets/stylesheets/palette.e70b70b6.min.css.map",
"overrides/assets/javascripts/bundle.js": "overrides/assets/javascripts/bundle.9f9f98ea.min.js", "overrides/assets/javascripts/bundle.js": "overrides/assets/javascripts/bundle.63116035.min.js",
"overrides/assets/javascripts/bundle.js.map": "overrides/assets/javascripts/bundle.9f9f98ea.min.js.map", "overrides/assets/javascripts/bundle.js.map": "overrides/assets/javascripts/bundle.63116035.min.js.map",
"overrides/assets/javascripts/vendor.js": "overrides/assets/javascripts/vendor.1aa446d9.min.js", "overrides/assets/javascripts/vendor.js": "overrides/assets/javascripts/vendor.1aa446d9.min.js",
"overrides/assets/javascripts/vendor.js.map": "overrides/assets/javascripts/vendor.1aa446d9.min.js.map", "overrides/assets/javascripts/vendor.js.map": "overrides/assets/javascripts/vendor.1aa446d9.min.js.map",
"overrides/assets/stylesheets/main.css": "overrides/assets/stylesheets/main.8e98f424.min.css", "overrides/assets/stylesheets/main.css": "overrides/assets/stylesheets/main.8e98f424.min.css",

View File

@ -217,7 +217,7 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="{{ 'assets/javascripts/vendor.e32ed4d0.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/vendor.e32ed4d0.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.4fa4ebaf.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/bundle.d8c6976f.min.js' | url }}"></script>
{% for path in config["extra_javascript"] %} {% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script> <script src="{{ path | url }}"></script>
{% endfor %} {% endfor %}

View File

@ -1,2 +1,2 @@
!function(e,t){for(var n in t)e[n]=t[n]}(window,function(e){function t(t){for(var r,i,a=t[0],s=t[1],u=t[2],f=0,p=[];f<a.length;f++)i=a[f],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(r in s)Object.prototype.hasOwnProperty.call(s,r)&&(e[r]=s[r]);for(l&&l(t);p.length;)p.shift()();return c.push.apply(c,u||[]),n()}function n(){for(var e,t=0;t<c.length;t++){for(var n=c[t],r=!0,a=1;a<n.length;a++){var s=n[a];0!==o[s]&&(r=!1)}r&&(c.splice(t--,1),e=i(i.s=n[0]))}return e}var r={},o={0:0},c=[];function i(t){if(r[t])return r[t].exports;var n=r[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,i),n.l=!0,n.exports}i.m=e,i.c=r,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)i.d(n,r,function(t){return e[t]}.bind(null,r));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="";var a=window.webpackJsonp=window.webpackJsonp||[],s=a.push.bind(a);a.push=t,a=a.slice();for(var u=0;u<a.length;u++)t(a[u]);var l=s;return c.push([37,1]),n()}({37:function(e,t,n){"use strict";n.r(t);var r=n(20),o=n(3),c=n(38),i=n(29),a=n(25);n(32),n(39);function s(e,t=document){return t.querySelector(e)||void 0}function u(e,t=document){const n=s(e,t);if(void 0===n)throw new ReferenceError(`Missing element: expected "${e}" to be present`);return n}n(50);var l=n(51);var f=n(17),p=n(40),d=n(41),b=n(42),g=n(43),h=n(44);n(45),n(46);const v=new f.a;Object(p.a)(()=>Object(d.a)(new ResizeObserver(e=>{for(const t of e)v.next(t)}))).pipe(Object(i.a)(e=>b.a.pipe(Object(l.a)(e)).pipe(Object(g.a)(()=>e.disconnect()))),Object(h.a)(1));n(31);n(47);u("[data-md-toggle=drawer]"),u("[data-md-toggle=search]");n(52),n(53);n(48),n(49);function O(e,t){if("string"==typeof t||"number"==typeof t)e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(const n of t)O(e,n)}function j(e,t,...n){const r=document.createElement(e);if(t)for(const e of Object.keys(t))"boolean"!=typeof t[e]?r.setAttribute(e,t[e]):t[e]&&r.setAttribute(e,"");for(const e of n)O(r,e);return r}function m(e,t){return t.length?j("div",{class:""},j("span",null,function(e){if(e>999){return((e+1e-6)/1e3).toFixed(+((e-950)%1e3>99))+"k"}return e.toString()}(e.length)," results"),j("ul",{class:"tx-icon-search__list"},e.slice(0,10).map(e=>j("li",{class:"tx-icon-search__item"},j("span",{class:"twemoji"},j("img",{src:"https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/material/.icons/"+e,style:"width: 18px; height: 18px"}))," ",j("button",{class:"md-clipboard--inline","data-clipboard-text":":"+e.replace(/\.svg$/,"").replace(/\//g,"-")+":"},j("code",null,function(e,t){return`:${Object(r.wrap)(e.replace(/\.svg$/,"").replace(/\//g,"-"),t,{wrap:{tagOpen:"<b>",tagClose:"</b>"}})}:`}(e,t))))))):j("div",{class:""})}const y=u("#__config"),w=JSON.parse(y.textContent),x=Object(o.a)(fetch(w.base+"/overrides/assets/javascripts/icons.json").then(e=>e.json())),_=s("#icon-search");_&&x.pipe(Object(i.a)(e=>Object(c.a)(_,"keyup").pipe(Object(a.a)(()=>Object(r.filter)(e,_.value))))).subscribe(e=>{const t=u(".tx-icon-result");t.innerHTML="",t.appendChild(m(e,_.value))}),Object(c.a)(document.body,"click").subscribe(e=>{if(e.target instanceof HTMLElement){e.target.closest("a[href^=http]")instanceof HTMLLinkElement&&ga("send","event","outbound","click",y.href)}})}})); !function(e,t){for(var n in t)e[n]=t[n]}(window,function(e){function t(t){for(var r,i,a=t[0],s=t[1],u=t[2],f=0,p=[];f<a.length;f++)i=a[f],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&p.push(o[i][0]),o[i]=0;for(r in s)Object.prototype.hasOwnProperty.call(s,r)&&(e[r]=s[r]);for(l&&l(t);p.length;)p.shift()();return c.push.apply(c,u||[]),n()}function n(){for(var e,t=0;t<c.length;t++){for(var n=c[t],r=!0,a=1;a<n.length;a++){var s=n[a];0!==o[s]&&(r=!1)}r&&(c.splice(t--,1),e=i(i.s=n[0]))}return e}var r={},o={0:0},c=[];function i(t){if(r[t])return r[t].exports;var n=r[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,i),n.l=!0,n.exports}i.m=e,i.c=r,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)i.d(n,r,function(t){return e[t]}.bind(null,r));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="";var a=window.webpackJsonp=window.webpackJsonp||[],s=a.push.bind(a);a.push=t,a=a.slice();for(var u=0;u<a.length;u++)t(a[u]);var l=s;return c.push([37,1]),n()}({37:function(e,t,n){"use strict";n.r(t);var r=n(20),o=n(3),c=n(38),i=n(29),a=n(25);n(32),n(39);function s(e,t=document){return t.querySelector(e)||void 0}function u(e,t=document){const n=s(e,t);if(void 0===n)throw new ReferenceError(`Missing element: expected "${e}" to be present`);return n}n(50);var l=n(51);var f=n(17),p=n(40),d=n(41),b=n(42),g=n(43),h=n(44);n(45),n(46);const v=new f.a;Object(p.a)(()=>Object(d.a)(new ResizeObserver(e=>{for(const t of e)v.next(t)}))).pipe(Object(i.a)(e=>b.a.pipe(Object(l.a)(e)).pipe(Object(g.a)(()=>e.disconnect()))),Object(h.a)(1));n(31);u("[data-md-toggle=drawer]"),u("[data-md-toggle=search]");n(47);n(52),n(53);n(48),n(49);function O(e,t){if("string"==typeof t||"number"==typeof t)e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(const n of t)O(e,n)}function j(e,t,...n){const r=document.createElement(e);if(t)for(const e of Object.keys(t))"boolean"!=typeof t[e]?r.setAttribute(e,t[e]):t[e]&&r.setAttribute(e,"");for(const e of n)O(r,e);return r}function m(e,t){return t.length?j("div",{class:""},j("span",null,function(e){if(e>999){return((e+1e-6)/1e3).toFixed(+((e-950)%1e3>99))+"k"}return e.toString()}(e.length)," results"),j("ul",{class:"tx-icon-search__list"},e.slice(0,10).map(e=>j("li",{class:"tx-icon-search__item"},j("span",{class:"twemoji"},j("img",{src:"https://raw.githubusercontent.com/squidfunk/mkdocs-material/master/material/.icons/"+e,style:"width: 18px; height: 18px"}))," ",j("button",{class:"md-clipboard--inline","data-clipboard-text":":"+e.replace(/\.svg$/,"").replace(/\//g,"-")+":"},j("code",null,function(e,t){return`:${Object(r.wrap)(e.replace(/\.svg$/,"").replace(/\//g,"-"),t,{wrap:{tagOpen:"<b>",tagClose:"</b>"}})}:`}(e,t))))))):j("div",{class:""})}const y=u("#__config"),w=JSON.parse(y.textContent),x=Object(o.a)(fetch(w.base+"/overrides/assets/javascripts/icons.json").then(e=>e.json())),_=s("#icon-search");_&&x.pipe(Object(i.a)(e=>Object(c.a)(_,"keyup").pipe(Object(a.a)(()=>Object(r.filter)(e,_.value))))).subscribe(e=>{const t=u(".tx-icon-result");t.innerHTML="",t.appendChild(m(e,_.value))}),Object(c.a)(document.body,"click").subscribe(e=>{if(e.target instanceof HTMLElement){e.target.closest("a[href^=http]")instanceof HTMLLinkElement&&ga("send","event","outbound","click",y.href)}})}}));
//# sourceMappingURL=bundle.9f9f98ea.min.js.map //# sourceMappingURL=bundle.63116035.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -54,5 +54,5 @@
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{ 'overrides/assets/javascripts/vendor.1aa446d9.min.js' | url }}"></script> <script src="{{ 'overrides/assets/javascripts/vendor.1aa446d9.min.js' | url }}"></script>
<script src="{{ 'overrides/assets/javascripts/bundle.9f9f98ea.min.js' | url }}"></script> <script src="{{ 'overrides/assets/javascripts/bundle.63116035.min.js' | url }}"></script>
{% endblock %} {% endblock %}

View File

@ -23,20 +23,33 @@
import { Observable, fromEvent } from "rxjs" import { Observable, fromEvent } from "rxjs"
import { filter, map, share } from "rxjs/operators" import { filter, map, share } from "rxjs/operators"
import { getActiveElement } from "../element"
import { getToggle } from "../toggle"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Types * Types
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Key * Keyboard mode
*/ */
export interface Key { export type KeyboardMode =
| "global" /* Global */
| "search" /* Search is open */
/* ------------------------------------------------------------------------- */
/**
* Keyboard
*/
export interface Keyboard {
mode: KeyboardMode /* Keyboard mode */
type: string /* Key type */ type: string /* Key type */
claim(): void /* Key claim */ claim(): void /* Key claim */
} }
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Helper functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
@ -46,7 +59,7 @@ export interface Key {
* *
* @returns Test result * @returns Test result
*/ */
export function isSusceptibleToKeyboard(el: HTMLElement): boolean { function isSusceptibleToKeyboard(el: HTMLElement): boolean {
switch (el.tagName) { switch (el.tagName) {
/* Form elements */ /* Form elements */
@ -61,24 +74,35 @@ export function isSusceptibleToKeyboard(el: HTMLElement): boolean {
} }
} }
/* ------------------------------------------------------------------------- */ /* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/** /**
* Watch keyboard * Watch keyboard
* *
* @returns Keyboard observable * @returns Keyboard observable
*/ */
export function watchKeyboard(): Observable<Key> { export function watchKeyboard(): Observable<Keyboard> {
return fromEvent<KeyboardEvent>(window, "keydown") return fromEvent<KeyboardEvent>(window, "keydown")
.pipe( .pipe(
filter(ev => !(ev.metaKey || ev.ctrlKey)), filter(ev => !(ev.metaKey || ev.ctrlKey)),
map(ev => ({ map(ev => ({
mode: getToggle("search") ? "search" : "global",
type: ev.key, type: ev.key,
claim() { claim() {
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()
} }
})), } as Keyboard)),
filter(({ mode }) => {
if (mode === "global") {
const active = getActiveElement()
if (typeof active !== "undefined")
return !isSusceptibleToKeyboard(active)
}
return true
}),
share() share()
) )
} }

View File

@ -25,8 +25,14 @@ import { filter, sample, take } from "rxjs/operators"
import { configuration } from "~/_" import { configuration } from "~/_"
import { import {
Keyboard,
getActiveElement,
getElementOrThrow, getElementOrThrow,
requestJSON getElements,
requestJSON,
setElementFocus,
setElementSelection,
setToggle
} from "~/browser" } from "~/browser"
import { import {
SearchIndex, SearchIndex,
@ -50,6 +56,17 @@ export type Search =
| SearchQuery | SearchQuery
| SearchResult | SearchResult
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
keyboard$: Observable<Keyboard> /* Keyboard observable */
}
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Helper functions * Helper functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@ -73,17 +90,22 @@ function fetchSearchIndex(url: string) {
* Mount search * Mount search
* *
* @param el - Search element * @param el - Search element
* @param options - Options
* *
* @returns Search component observable * @returns Search component observable
*/ */
export function mountSearch( export function mountSearch(
el: HTMLElement el: HTMLElement, { keyboard$ }: MountOptions
): Observable<Component<Search>> { ): Observable<Component<Search>> {
const config = configuration() const config = configuration()
const worker = setupSearchWorker(config.search, fetchSearchIndex( const worker = setupSearchWorker(config.search, fetchSearchIndex(
`${config.base}/search/search_index.json` `${config.base}/search/search_index.json`
)) ))
/* Retrieve elements */
const query = getElementOrThrow("[data-md-component=search-query]", el)
const result = getElementOrThrow("[data-md-component=search-result]", el)
/* Re-emit query when search is ready */ /* Re-emit query when search is ready */
const { tx$, rx$ } = worker const { tx$, rx$ } = worker
tx$ tx$
@ -94,18 +116,80 @@ export function mountSearch(
) )
.subscribe(tx$.next.bind(tx$)) .subscribe(tx$.next.bind(tx$))
/* Mount search query component */ /* Set up search keyboard handlers */
const query$ = mountSearchQuery( keyboard$
getElementOrThrow("[data-md-component=search-query]", el), .pipe(
worker filter(({ mode }) => mode === "search")
) )
.subscribe(key => {
const active = getActiveElement()
switch (key.type) {
/* Mount search result and return component */ /* Enter: prevent form submission */
case "Enter":
if (active === query)
key.claim()
break
/* Escape or Tab: close search */
case "Escape":
case "Tab":
setToggle("search", false)
setElementFocus(query, false)
break
/* Vertical arrows: select previous or next search result */
case "ArrowUp":
case "ArrowDown":
if (typeof active === "undefined") {
setElementFocus(query)
} else {
const els = [query, ...getElements(
":not(details) > [href], summary, details[open] [href]",
result
)]
const i = Math.max(0, (
Math.max(0, els.indexOf(active)) + els.length + (
key.type === "ArrowUp" ? -1 : +1
)
) % els.length)
setElementFocus(els[i])
}
/* Prevent scrolling of page */
key.claim()
break
/* All other keys: hand to search query */
default:
if (query !== getActiveElement())
setElementFocus(query)
}
})
/* Set up global keyboard handlers */
keyboard$
.pipe(
filter(({ mode }) => mode === "global"),
)
.subscribe(key => {
switch (key.type) {
/* Open search and select query */
case "f":
case "s":
case "/":
setElementFocus(query)
setElementSelection(query)
key.claim()
break
}
})
/* Create and return component */
const query$ = mountSearchQuery(query as HTMLInputElement, worker)
return merge( return merge(
query$, query$,
mountSearchResult( mountSearchResult(result, worker, { query$ })
getElementOrThrow("[data-md-component=search-result]", el),
worker, { query$ }
)
) )
} }

View File

@ -23,6 +23,7 @@
import "focus-visible" import "focus-visible"
import { Subject, defer, merge } from "rxjs" import { Subject, defer, merge } from "rxjs"
import { import {
filter,
map, map,
mergeWith, mergeWith,
shareReplay, shareReplay,
@ -32,9 +33,11 @@ import {
import { feature } from "./_" import { feature } from "./_"
import { import {
at, at,
getElement,
getElementOrThrow, getElementOrThrow,
getElements, getElements,
watchDocument, watchDocument,
watchKeyboard,
watchLocation, watchLocation,
watchLocationTarget, watchLocationTarget,
watchMedia, watchMedia,
@ -64,7 +67,7 @@ import {
} from "./patches" } from "./patches"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Program * Application
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/* Yay, JavaScript is available */ /* Yay, JavaScript is available */
@ -75,6 +78,7 @@ document.documentElement.classList.add("js")
const document$ = watchDocument() const document$ = watchDocument()
const location$ = watchLocation() const location$ = watchLocation()
const target$ = watchLocationTarget() const target$ = watchLocationTarget()
const keyboard$ = watchKeyboard()
/* Set up media observables */ /* Set up media observables */
const viewport$ = watchViewport() const viewport$ = watchViewport()
@ -90,6 +94,32 @@ setupClipboardJS({ alert$ })
if (feature("navigation.instant")) if (feature("navigation.instant"))
setupInstantLoading({ document$, location$, viewport$ }) setupInstantLoading({ document$, location$, viewport$ })
/* Set up global keyboard handlers */
keyboard$
.pipe(
filter(({ mode }) => mode === "global")
)
.subscribe(key => {
switch (key.type) {
/* 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
}
})
/* Set up patches */ /* Set up patches */
patchIndeterminate({ document$ }) patchIndeterminate({ document$ })
patchScrollfix({ document$ }) patchScrollfix({ document$ })
@ -121,7 +151,7 @@ const control$ = merge(
/* Search */ /* Search */
...getElements("[data-md-component=search]") ...getElements("[data-md-component=search]")
.map(child => mountSearch(child)), .map(child => mountSearch(child, { keyboard$ })),
/* Repository information */ /* Repository information */
...getElements("[data-md-component=source]") ...getElements("[data-md-component=source]")
@ -162,16 +192,18 @@ const component$ = document$
mergeWith(control$) mergeWith(control$)
) )
/* Subscribe to all components */
component$.subscribe() component$.subscribe()
/* Export to window */ /* Export to window */
export { export {
document$, document$,
component$,
viewport$,
location$, location$,
target$, target$,
screen$, keyboard$,
viewport$,
tablet$, tablet$,
print$ screen$,
print$,
component$
} }