mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-08-03 19:19:42 -04:00
Unified components and observable structure
This commit is contained in:
@@ -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 */
|
||||
|
||||
186
src/assets/javascripts/integrations/instant/index.ts
Normal file
186
src/assets/javascripts/integrations/instant/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user