Fixed bug in main area observable

This commit is contained in:
squidfunk
2020-02-13 23:42:12 +01:00
parent 1f64cd481d
commit c035df94fd
14 changed files with 154 additions and 114 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.53f51356.min.js", "assets/javascripts/bundle.js": "assets/javascripts/bundle.df0a4fb1.min.js",
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.53f51356.min.js.map", "assets/javascripts/bundle.js.map": "assets/javascripts/bundle.df0a4fb1.min.js.map",
"assets/javascripts/worker/packer.js": "assets/javascripts/worker/packer.c14659e8.min.js", "assets/javascripts/worker/packer.js": "assets/javascripts/worker/packer.c14659e8.min.js",
"assets/javascripts/worker/packer.js.map": "assets/javascripts/worker/packer.c14659e8.min.js.map", "assets/javascripts/worker/packer.js.map": "assets/javascripts/worker/packer.c14659e8.min.js.map",
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.ce66ce8d.min.js", "assets/javascripts/worker/search.js": "assets/javascripts/worker/search.ce66ce8d.min.js",

View File

@@ -190,7 +190,7 @@
{% endblock %} {% endblock %}
</div> </div>
{% block scripts %} {% block scripts %}
<script src="{{ 'assets/javascripts/bundle.53f51356.min.js' | url }}"></script> <script src="{{ 'assets/javascripts/bundle.df0a4fb1.min.js' | url }}"></script>
<script id="__lang" type="application/json"> <script id="__lang" type="application/json">
{%- set translations = {} -%} {%- set translations = {} -%}
{%- for key in [ {%- for key in [

View File

@@ -20,8 +20,6 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
export * from "./_"
export * from "./header"
export * from "./hero" export * from "./hero"
export * from "./tabs" export * from "./tabs"
export * from "./toc" export * from "./toc"

View File

@@ -21,7 +21,12 @@
*/ */
import { Observable, OperatorFunction, pipe } from "rxjs" import { Observable, OperatorFunction, pipe } from "rxjs"
import { shareReplay, switchMap } from "rxjs/operators" import {
distinctUntilKeyChanged,
shareReplay,
switchMap,
tap
} from "rxjs/operators"
import { import {
Header, Header,
@@ -62,11 +67,20 @@ export function mountMain(
return pipe( return pipe(
switchMap(el => useComponent("header") switchMap(el => useComponent("header")
.pipe( .pipe(
switchMap(header => watchMain(el, { header$, viewport$ }) switchMap(header => {
.pipe( const main$ = watchMain(el, { header$, viewport$ })
paintHeaderShadow(header)
) /* Paint header shadow */
) main$
.pipe(
distinctUntilKeyChanged("active"),
paintHeaderShadow(header)
)
.subscribe()
/* Return main area */
return main$
})
) )
), ),
shareReplay(1) shareReplay(1)

View File

@@ -27,7 +27,7 @@ import "../stylesheets/app.scss"
import "../stylesheets/app-palette.scss" import "../stylesheets/app-palette.scss"
import * as Clipboard from "clipboard" import * as Clipboard from "clipboard"
import { identity, values } from "ramda" import { identity, not, values } from "ramda"
import { import {
EMPTY, EMPTY,
merge, merge,
@@ -39,7 +39,9 @@ import {
filter, filter,
map, map,
switchMap, switchMap,
tap tap,
withLatestFrom,
switchMapTo
} from "rxjs/operators" } from "rxjs/operators"
import { import {
@@ -48,7 +50,6 @@ import {
mountTabs, mountTabs,
} from "./components" } from "./components"
import { import {
watchHeader,
getElement, getElement,
watchToggle, watchToggle,
getElements, getElements,
@@ -58,17 +59,21 @@ import {
watchViewport, watchViewport,
watchKeyboard, watchKeyboard,
watchToggleMap, watchToggleMap,
useToggle useToggle,
getActiveElement,
mayReceiveKeyboardEvents,
watchMain
} from "./observables" } from "./observables"
import { setupSearchWorker } from "./workers" import { setupSearchWorker } from "./workers"
import { renderSource } from "templates" import { renderSource } from "templates"
import { not, takeIf } from "utilities" import { takeIf } from "utilities"
import { renderClipboard } from "templates/clipboard" import { renderClipboard } from "templates/clipboard"
import { fetchGitHubStats } from "modules/source/github" import { fetchGitHubStats } from "modules/source/github"
import { renderTable } from "templates/table" import { renderTable } from "templates/table"
import { setToggle } from "actions" import { setToggle } from "actions"
import { import {
Component, Component,
mountHeader,
mountMain, mountMain,
mountNavigation, mountNavigation,
mountSearch, mountSearch,
@@ -210,26 +215,13 @@ export function initialize(config: unknown) {
if (!isConfig(config)) if (!isConfig(config))
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`) throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
// TODO: WIP repo rendering
repository().subscribe(facts => {
if (facts.length) {
const sources = getElements(".md-source__repository")
sources.forEach(repo => {
repo.dataset.mdState = "done"
repo.appendChild(
renderSource(facts)
)
})
}
})
// pass config here!? // pass config here!?
const document$ = watchDocument() const document$ = watchDocument()
const hash$ = watchLocationHash() const hash$ = watchLocationHash()
const viewport$ = watchViewport() const viewport$ = watchViewport()
const tablet$ = watchMedia("(min-width: 960px)") const tablet$ = watchMedia("(min-width: 960px)")
const screen$ = watchMedia("(min-width: 1220px)") const screen$ = watchMedia("(min-width: 1220px)")
const key$ = watchKeyboard() const keyboard$ = watchKeyboard()
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
@@ -239,7 +231,7 @@ export function initialize(config: unknown) {
/* Create header observable */ /* Create header observable */
const header$ = useComponent("header") const header$ = useComponent("header")
.pipe( .pipe(
switchMap(watchHeader) // TODO: should also be the mount... mountHeader()
) )
const main$ = useComponent("main") const main$ = useComponent("main")
@@ -260,7 +252,6 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
// DONE (partly)
const navigation$ = useComponent("navigation") const navigation$ = useComponent("navigation")
.pipe( .pipe(
mountNavigation({ main$, viewport$, screen$ }) mountNavigation({ main$, viewport$, screen$ })
@@ -281,44 +272,63 @@ export function initialize(config: unknown) {
mountHero({ header$, viewport$, screen$ }) mountHero({ header$, viewport$, screen$ })
) )
/* ----------------------------------------------------------------------- */
function openSearchOnHotKey() {
const toggle$ = useToggle("search")
const search$ = toggle$
.pipe(
switchMap(watchToggle)
)
const query$ = useComponent<HTMLInputElement>("search-query")
search$
.pipe(
filter(not),
switchMapTo(keyboard$),
filter(key => ["KeyS", "KeyF"].includes(key.code)),
switchMapTo(toggle$)
)
.subscribe(toggle => {
const el = getActiveElement()
if (!(el && mayReceiveKeyboardEvents(el)))
setToggle(toggle, true)
})
search$
.pipe(
filter(identity),
switchMapTo(keyboard$),
filter(key => ["Escape", "Tab"].includes(key.code)),
switchMapTo(toggle$),
withLatestFrom(query$)
)
.subscribe(([toggle, el]) => {
setToggle(toggle, false)
el.blur()
})
} // TODO: handle ALL cases in one switch case statement!
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")! const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
const searchActive$ = useToggle("search").pipe( const searchActive$ = useToggle("search").pipe(
switchMap(el => watchToggle(el)), switchMap(el => watchToggle(el)),
delay(400) delay(400)
) )
// shortcodes
key$ openSearchOnHotKey()
.pipe(
takeIf(not(searchActive$))
)
.subscribe(key => {
if (
document.activeElement && (
["TEXTAREA", "SELECT", "INPUT"].includes(
document.activeElement.tagName
) ||
document.activeElement instanceof HTMLElement &&
document.activeElement.isContentEditable
)
) {
// do nothing...
} else {
if (key.type === "KeyS" || key.type === "KeyF") {
setToggle(search, true)
}
}
})
// check which element is focused...
// note that all links have tabindex=-1 // note that all links have tabindex=-1
key$ keyboard$
.pipe( .pipe(
takeIf(searchActive$), takeIf(searchActive$),
/* Abort if meta key (macOS) or ctrl key (Windows) is pressed */ /* Abort if meta key (macOS) or ctrl key (Windows) is pressed */
tap(key => { tap(key => {
console.log("jo", key) console.log("jo", key)
if (key.type === "Enter") { if (key.code === "Enter") {
if (document.activeElement === getElement("[data-md-component=search-query]")) { if (document.activeElement === getElement("[data-md-component=search-query]")) {
key.claim() key.claim()
// intercept hash change after search closed // intercept hash change after search closed
@@ -327,18 +337,20 @@ export function initialize(config: unknown) {
} }
} }
if (key.type === "ArrowUp" || key.type === "ArrowDown") { if (key.code === "ArrowUp" || key.code === "ArrowDown") {
const active = getElements("[data-md-component=search-query], [data-md-component=search-result] [href]") 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 i = Math.max(0, active.findIndex(el => el === document.activeElement))
const x = Math.max(0, (i + active.length + (key.type === "ArrowUp" ? -1 : +1)) % active.length) const x = Math.max(0, (i + active.length + (key.code === "ArrowUp" ? -1 : +1)) % active.length)
active[x].focus() active[x].focus()
// pass keyboard to search result!?
/* Prevent scrolling of page */ /* Prevent scrolling of page */
key.claim() key.claim()
} else if (key.type === "Escape" || key.type === "Tab") { // } else if (key.code === "Escape" || key.code === "Tab") {
setToggle(search, false) // setToggle(search, false)
getElement("[data-md-component=search-query]")!.blur() // getElement("[data-md-component=search-query]")!.blur()
} else { } else {
if (search.checked && document.activeElement !== getElement("[data-md-component=search-query]")) { if (search.checked && document.activeElement !== getElement("[data-md-component=search-query]")) {
@@ -400,6 +412,19 @@ export function initialize(config: unknown) {
// }) // })
} }
// TODO: WIP repo rendering
repository().subscribe(facts => {
if (facts.length) {
const sources = getElements(".md-source__repository")
sources.forEach(repo => {
repo.dataset.mdState = "done"
repo.appendChild(
renderSource(facts)
)
})
}
})
/* Wrap all data tables for better overflow scrolling */ /* Wrap all data tables for better overflow scrolling */
const tables = getElements<HTMLTableElement>("table:not([class])") const tables = getElements<HTMLTableElement>("table:not([class])")
const placeholder = document.createElement("table") const placeholder = document.createElement("table")
@@ -445,38 +470,6 @@ export function initialize(config: unknown) {
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
// get headerHEIGHT! only if header is sticky!
// // lockHeader at...
// const direction$ = agent.viewport.offset$.pipe(
// bufferCount(2, 1), // determine scroll direction
// map(([{ y: y0 }, { y: y1 }]) => y1 > y0),
// distinctUntilChanged(),
// )
// document.body.style.minHeight = "100vh"
// // if true => then + HEADER. otherwise not
// let last = 0
// combineLatest([direction$, header$]).pipe(
// tap(([direction, { height }]) => { // TODO: only if sticky!
// const offset = 48
// console.log(window.pageYOffset, height, last)
// if (Math.abs(window.pageYOffset - last) < height + offset) { // TODO: add sensitivity offset!
// return
// }
// if (direction) {
// document.body.style.height = `${window.pageYOffset + offset + height}px`
// } else {
// document.body.style.height = `${window.pageYOffset - offset}px` // offset
// }
// last = window.pageYOffset
// })
// )
// .subscribe()
// // toggle
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
const state = { const state = {

View File

@@ -55,3 +55,16 @@ export function getElements<T extends HTMLElement>(
): T[] { ): T[] {
return Array.from(node.querySelectorAll<T>(selector)) return Array.from(node.querySelectorAll<T>(selector))
} }
/* ------------------------------------------------------------------------- */
/**
* Retrieve the currently active element
*
* @return Element
*/
export function getActiveElement(): HTMLElement | undefined {
return document.activeElement instanceof HTMLElement
? document.activeElement
: undefined
}

View File

@@ -23,6 +23,8 @@
import { Observable, fromEvent, merge } from "rxjs" import { Observable, fromEvent, merge } from "rxjs"
import { mapTo, shareReplay, startWith } from "rxjs/operators" import { mapTo, shareReplay, startWith } from "rxjs/operators"
import { getActiveElement } from "../_"
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
@@ -46,7 +48,7 @@ export function watchElementFocus(
blur$.pipe(mapTo(false)) blur$.pipe(mapTo(false))
) )
.pipe( .pipe(
startWith(el === document.activeElement), startWith(el === getActiveElement()),
shareReplay(1) shareReplay(1)
) )
} }

View File

@@ -31,7 +31,7 @@ import { filter, map, share } from "rxjs/operators"
* Key * Key
*/ */
export interface Key { export interface Key {
type: string /* Key type */ code: string /* Key code */
claim(): void /* Key claim */ claim(): void /* Key claim */
} }
@@ -48,6 +48,30 @@ const keydown$ = fromEvent<KeyboardEvent>(window, "keydown")
* Functions * Functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/**
* Check whether an element may receive keyboard input
*
* @param el - Element
*
* @return Test result
*/
export function mayReceiveKeyboardEvents(el: HTMLElement) {
switch (el.tagName) {
/* Form elements */
case "INPUT":
case "SELECT":
case "TEXTAREA":
return true
/* Everything else */
default:
return el.isContentEditable
}
}
/* ------------------------------------------------------------------------- */
/** /**
* Watch keyboard * Watch keyboard
* *
@@ -58,7 +82,7 @@ export function watchKeyboard(): Observable<Key> {
.pipe( .pipe(
filter(ev => !(ev.shiftKey || ev.metaKey || ev.ctrlKey)), filter(ev => !(ev.shiftKey || ev.metaKey || ev.ctrlKey)),
map(ev => ({ map(ev => ({
type: ev.code, code: ev.code,
claim() { claim() {
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()

View File

@@ -25,12 +25,7 @@ import {
animationFrameScheduler, animationFrameScheduler,
pipe pipe
} from "rxjs" } from "rxjs"
import { import { finalize, observeOn, tap } from "rxjs/operators"
distinctUntilKeyChanged,
finalize,
observeOn,
tap
} from "rxjs/operators"
import { resetHeaderShadow, setHeaderShadow } from "actions" import { resetHeaderShadow, setHeaderShadow } from "actions"
@@ -51,7 +46,6 @@ export function paintHeaderShadow(
el: HTMLElement el: HTMLElement
): MonoTypeOperatorFunction<Main> { ): MonoTypeOperatorFunction<Main> {
return pipe( return pipe(
distinctUntilKeyChanged("active"),
/* Defer repaint to next animation frame */ /* Defer repaint to next animation frame */
observeOn(animationFrameScheduler), observeOn(animationFrameScheduler),

View File

@@ -86,6 +86,8 @@ export function watchToggleMap(
): Observable<ToggleMap> { ): Observable<ToggleMap> {
toggles$ = document$ toggles$ = document$
.pipe( .pipe(
/* Ignore document switches */
take(1), take(1),
/* Build toggle map */ /* Build toggle map */