Unified components and observable structure

This commit is contained in:
squidfunk
2020-03-05 17:17:15 +01:00
parent 6e6868b8cd
commit 9738c12cc9
149 changed files with 11475 additions and 1095 deletions

View File

@@ -24,7 +24,7 @@ import * as ClipboardJS from "clipboard"
import { NEVER, Observable, Subject, fromEventPattern } from "rxjs"
import { mapTo, share, tap } from "rxjs/operators"
import { getElements } from "observables"
import { getElements } from "browser"
import { renderClipboard } from "templates"
import { translate } from "utilities"
@@ -63,11 +63,11 @@ export function setupClipboard(
/* Inject 'copy-to-clipboard' buttons */
document$.subscribe(() => {
const blocks = getElements("pre > code")
for (const [index, block] of blocks.entries()) {
blocks.forEach((block, index) => {
const parent = block.parentElement!
parent.id = `__code_${index}`
parent.insertBefore(renderClipboard(parent.id), block)
}
})
})
/* Initialize and setup clipboard */

View File

@@ -0,0 +1,186 @@
/*
* Copyright (c) 2016-2020 Martin Donath <martin.donath@squidfunk.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { Observable, Subject, fromEvent, merge } from "rxjs"
import {
bufferCount,
debounceTime,
distinctUntilChanged,
distinctUntilKeyChanged,
filter,
map,
pluck,
sample,
share,
withLatestFrom
} from "rxjs/operators"
import {
Viewport,
ViewportOffset,
getElement,
isLocationAnchor,
setLocationHash,
setViewportOffset
} from "browser"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* History state
*/
interface State {
url: URL /* State URL */
offset?: ViewportOffset /* State viewport offset */
}
/* ------------------------------------------------------------------------- */
/**
* Setup options
*/
interface SetupOptions {
document$: Observable<Document> /* Document observable */
viewport$: Observable<Viewport> /* Viewport observable */
link$: Observable<HTMLAnchorElement> /* Internal link observable */
location$: Subject<URL> /* Location subject */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Setup instant loading
*
* @param options - Options
*
* @return TODO ?
*/
export function setupInstantLoading(
{ document$, viewport$, link$, location$ }: SetupOptions
) { // TODO: add return type
const state$ = link$
.pipe(
map(el => ({ url: new URL(el.href) })),
share<State>()
)
/* Intercept internal links to dispatch */
const push$ = state$
.pipe(
distinctUntilChanged((prev, next) => prev.url.href === next.url.href),
filter(({ url }) => !isLocationAnchor(url)),
share()
)
/* Intercept popstate events (history back and forward) */
const pop$ = fromEvent<PopStateEvent>(window, "popstate")
.pipe(
filter(ev => ev.state !== null),
map<PopStateEvent, State>(ev => ({
url: new URL(location.href),
offset: ev.state
})),
share()
)
/* Emit location change */
merge(push$, pop$)
.pipe(
pluck("url")
)
.subscribe(location$)
/* History: dispatch internal link */
push$.subscribe(({ url }) => {
history.pushState({}, "", url.toString())
})
/* History: debounce update of viewport offset */
viewport$
.pipe(
debounceTime(250),
distinctUntilKeyChanged("offset")
)
.subscribe(({ offset }) => {
history.replaceState(offset, "")
})
/* Apply viewport offset from history */
merge(state$, pop$)
.pipe(
bufferCount(2, 1),
filter(([prev, next]) => {
return prev.url.pathname === next.url.pathname
&& !isLocationAnchor(next.url)
}),
map(([, state]) => state)
)
.subscribe(({ offset }) => {
setViewportOffset(offset || { y: 0 })
})
/* Intercept actual instant loading */
const instant$ = merge(push$, pop$)
.pipe(
sample(document$)
)
// TODO: from here on, everything is beta.... ###############################
instant$.subscribe(({ url, offset }) => {
if (url.hash && !offset) {
console.log("set hash!")
setLocationHash(url.hash) // must delay, if search is open!
} else {
setViewportOffset(offset || { y: 0 })
}
})
instant$
.pipe(
withLatestFrom(document$)
)
.subscribe(([, { title, head }]) => {
document.dispatchEvent(new CustomEvent("DOMContentSwitch"))
document.title = title
/* Replace meta tags */
for (const selector of [
`link[rel="canonical"]`,
`meta[name="author"]`,
`meta[name="description"]`
]) {
const next = getElement(selector, head)
const prev = getElement(selector, document.head)
if (
typeof next !== "undefined" &&
typeof prev !== "undefined"
) {
prev.replaceWith(next)
}
}
})
}

View File

@@ -25,24 +25,22 @@ import {
filter,
map,
share,
switchMap,
withLatestFrom
} from "rxjs/operators"
import { useComponent } from "components"
import {
Key,
getActiveElement,
getElement,
getElements,
getToggle,
isSusceptibleToKeyboard,
setElementFocus,
setElementSelection,
setToggle,
useToggle,
watchKeyboard,
watchToggle
} from "observables"
watchKeyboard
} from "browser"
import { useComponent } from "components"
/* ----------------------------------------------------------------------------
* Types
@@ -88,18 +86,10 @@ export interface Keyboard extends Key {
* @return Keyboard observable
*/
export function setupKeyboard(): Observable<Keyboard> {
const toggle$ = useToggle("search")
const search$ = toggle$
.pipe(
switchMap(watchToggle)
)
/* Setup keyboard and determine mode */
const keyboard$ = watchKeyboard()
.pipe(
withLatestFrom(search$),
map(([key, toggle]): Keyboard => ({
mode: toggle ? "search" : "global",
map<Key, Keyboard>(key => ({
mode: getToggle("search") ? "search" : "global",
...key
})),
share()
@@ -110,12 +100,11 @@ export function setupKeyboard(): Observable<Keyboard> {
.pipe(
filter(({ mode }) => mode === "search"),
withLatestFrom(
toggle$,
useComponent("search-query"),
useComponent("search-result")
)
)
.subscribe(([key, toggle, query, result]) => {
.subscribe(([key, query, result]) => {
const active = getActiveElement()
switch (key.type) {
@@ -128,7 +117,7 @@ export function setupKeyboard(): Observable<Keyboard> {
/* Escape or Tab: close search */
case "Escape":
case "Tab":
setToggle(toggle, false)
setToggle("search", false)
setElementFocus(query, false)
break

View File

@@ -20,8 +20,6 @@
* IN THE SOFTWARE.
*/
import * as escapeRegExp from "escape-string-regexp"
import { SearchIndexConfig } from "../_"
import { SearchDocument } from "../document"
@@ -78,7 +76,9 @@ export function setupSearchHighlighter(
/* Create search term match expression */
const match = new RegExp(`(^|${config.separator})(${
escapeRegExp(query).replace(separator, "|")
query
.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") // TODO: taken from escape-string-regexp
.replace(separator, "|")
})`, "img")
/* Highlight document */