1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2025-02-25 21:58:40 +01:00

Fixed unmapped components

This commit is contained in:
squidfunk 2019-12-22 17:30:55 +01:00
parent 37b3870133
commit f39c9f9e68
6 changed files with 143 additions and 301 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

@ -104,7 +104,7 @@
</svg> </svg>
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off"> <input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off"> <input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" data-md-component="overlay" for="__drawer"></label> <label class="md-overlay" for="__drawer"></label>
{% if page.toc | first is defined %} {% if page.toc | first is defined %}
<a href="{{ (page.toc | first).url }}" tabindex="1" class="md-skip"> <a href="{{ (page.toc | first).url }}" tabindex="1" class="md-skip">
{{ lang.t('skip.link.title') }} {{ lang.t('skip.link.title') }}

View File

@ -101,7 +101,7 @@ export function watchComponentMap(
switch (name) { switch (name) {
/* Top-level components: update */ /* Top-level components: update */
case "title": case "header-title":
case "container": case "container":
if (name in prev && typeof prev[name] !== "undefined") { if (name in prev && typeof prev[name] !== "undefined") {
prev[name]!.replaceWith(next[name]!) prev[name]!.replaceWith(next[name]!)

View File

@ -20,66 +20,56 @@
* IN THE SOFTWARE. * IN THE SOFTWARE.
*/ */
// TODO: remove this later on // TODO: remove this after we finished refactoring
// tslint:disable // tslint:disable
import { identity } from "ramda" import { identity, values } from "ramda"
import { import {
EMPTY, EMPTY,
MonoTypeOperatorFunction,
NEVER,
Observable, Observable,
Subject, Subject,
defer,
forkJoin, forkJoin,
fromEvent,
merge, merge,
of, of
pipe,
} from "rxjs" } from "rxjs"
import { ajax } from "rxjs/ajax"
import { import {
combineAll,
delay, delay,
distinctUntilKeyChanged,
filter, filter,
map, map,
pluck, pluck,
shareReplay,
switchMap, switchMap,
switchMapTo, switchMapTo,
take, take,
tap, tap,
} from "rxjs/operators" } from "rxjs/operators"
import {} from "components"
import { AjaxResponse, ajax } from "rxjs/ajax"
import { import {
Component, Component,
paintHeaderShadow, paintHeaderShadow,
setupHero, mountHero,
setupMain, mountMain,
setupNavigation, mountNavigation,
setupSearchResult, mountSearchResult,
mountTableOfContents,
mountTabs,
switchComponent, switchComponent,
watchComponentMap, watchComponentMap,
watchHeader, watchHeader,
watchSearchReset, watchSearchQuery,
watchSearchReset
} from "./components" } from "./components"
import { SearchIndex, SearchResult } from "./modules/search" import { SearchIndexOptions } from "./modules"
import { import {
getElement, getElement,
setupAgent, setupAgent,
watchDocument,
watchLocation,
watchLocationHash,
watchMedia,
watchToggle, watchToggle,
watchViewportOffset, watchWorker,
watchViewportSize, setToggle
watchWorker
} from "./utilities" } from "./utilities"
import { import {
PackerMessage,
PackerMessageType,
SearchMessage, SearchMessage,
SearchMessageType, SearchMessageType,
SearchSetupMessage, SearchSetupMessage,
@ -87,73 +77,71 @@ import {
isSearchResultMessage isSearchResultMessage
} from "./workers" } from "./workers"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/** /**
* Configuration * Configuration
*/ */
export interface Config { export interface Config {
base: string /* Base URL */ base: string /* Base URL */
worker: { worker: {
search: string /* Web worker URL */ search: string /* Search worker URL */
packer: string /* Web worker URL */ packer: string /* Packer worker URL */
} }
} }
import { /* ----------------------------------------------------------------------------
PackerMessage, * TODO: where do we put this stuff?
PackerMessageType * ------------------------------------------------------------------------- */
} from "./workers/packer"
import { setupTabs } from "components/tabs" document.documentElement.classList.remove("no-js")
import { setupTableOfContents } from "components/toc/_" document.documentElement.classList.add("js")
const names: Component[] = [
"container", /* Container */
"header", /* Header */
"header-title", /* Header title */
"hero", /* Hero */
"main", /* Main area */
"navigation", /* Navigation */
"search", /* Search */
"search-query", /* Search input */
"search-reset", /* Search reset */
"search-result", /* Search results */
"tabs", /* Tabs */
"toc" /* Table of contents */
]
/* ---------------------------------------------------------------------------- /* ----------------------------------------------------------------------------
* Functions * Helper functions
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/** /**
* Ensure that the given value is a valid configuration * Ensure that the given value is a valid configuration
* *
* We could use `jsonschema` or any other schema validation framework, but that
* would just add more bloat to the bundle, so we'll keep it plain and simple.
*
* @param config - Configuration * @param config - Configuration
* *
* @return Test result * @return Test result
*/ */
export function isConfig(config: any): config is Config { function isConfig(config: any): config is Config {
return typeof config === "object" return typeof config === "object"
&& typeof config.base === "string" && typeof config.base === "string"
&& typeof config.worker === "object"
&& typeof config.worker.search === "string"
&& typeof config.worker.packer === "string"
} }
// TBD
// TODO: put this somewhere else... (merge with config!) JSON schema!?
const names: Component[] = [
"header", /* Header */
"title", /* Header title */
"search", /* Search */
"query", /* Search input */
"reset", /* Search reset */
"result", /* Search results */
"container", /* Container */
"main", /* Main area */
"hero", /* Hero */
"tabs", /* Tabs */
"navigation", /* Navigation */
"toc" /* Table of contents */
]
// modernizr for the poor
document.documentElement.classList.remove("no-js")
document.documentElement.classList.add("js")
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/** /**
* *
* Rogue control characters must be filtered before handing the query to the * Rogue control characters must be filtered before handing the query to the
* search index, as lunr will throw otherwise. * search index, as lunr will throw otherwise.
*/ */
function prepareQuery(value: string): string { function prepare(value: string): string {
const newvalue = value const newvalue = value
.replace(/(?:^|\s+)[*+-:^~]+(?=\s+|$)/g, "") .replace(/(?:^|\s+)[*+-:^~]+(?=\s+|$)/g, "")
.trim() .trim()
@ -161,29 +149,17 @@ function prepareQuery(value: string): string {
return newvalue ? newvalue.replace(/\s+|$/g, "* ") : "" return newvalue ? newvalue.replace(/\s+|$/g, "* ") : ""
} }
/** function setupWorkers(config: Config) {
* Initialize Material for MkDocs
*
* @param config - Configuration
*/
export function initialize(config: unknown) {
if (!isConfig(config))
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
const agent = setupAgent()
const worker = new Worker(config.worker.search) const worker = new Worker(config.worker.search)
const packer = new Worker(config.worker.packer) const packer = new Worker(config.worker.packer)
// const query = message.data.trim().replace(/\s+|$/g, "* ") // TODO: do this outside of the worker
const packerMessage$ = new Subject<PackerMessage>() const packerMessage$ = new Subject<PackerMessage>()
const packer$ = watchWorker(packer, { send$: packerMessage$ }) const packer$ = watchWorker(packer, { send$: packerMessage$ })
// send a message, then switchMapTo worker! // send a message, then switchMapTo worker!
packer$.subscribe(message => { packer$.subscribe(message => {
console.log("PACKER.MSG", message.data.length) // console.log("PACKER.MSG", message.data.length)
// is always packed! // is always packed!
if (message.type === PackerMessageType.BINARY && message.data[0] !== "{") if (message.type === PackerMessageType.BINARY && message.data[0] !== "{")
localStorage.setItem("index", message.data) localStorage.setItem("index", message.data)
@ -195,25 +171,6 @@ export function initialize(config: unknown) {
const search$ = watchWorker(worker, { send$: searchMessage$ }) const search$ = watchWorker(worker, { send$: searchMessage$ })
// paintSearchResult <-- must paint META AND LIST!
// list must be painted based on scroll offset...
/* Render search results */
// search$
// .pipe(
// filter(isSearchResultMessage),
// pluck("data")
// )
// .subscribe(result => {
// const list = getElement(".md-search-result__list")!
// list.innerHTML = ""
// for (const el of result.map(renderSearchResult)) // TODO: perform entire lazy render!!!!
// list.appendChild(el)
// })
// scroll!
// watchSearchResult
/* Link search to packer */ /* Link search to packer */
search$ search$
.pipe( .pipe(
@ -232,7 +189,7 @@ export function initialize(config: unknown) {
responseType: "json", responseType: "json",
withCredentials: true withCredentials: true
}) })
.pipe<SearchIndex>( .pipe<SearchIndexOptions>(
pluck("response") pluck("response")
// take(1) // take(1)
) )
@ -264,48 +221,34 @@ export function initialize(config: unknown) {
searchMessage$.next(message) // TODO: this shall not complete searchMessage$.next(message) // TODO: this shall not complete
}) })
// filter singular "+" or "-",as it will result in a lunr.js error return [search$, searchMessage$] as const
}
// data$ /* ----------------------------------------------------------------------------
// .pipe( * Functions
// map<SearchIndex, SearchMessage>(data => ({ * ------------------------------------------------------------------------- */
// type: SearchMessageType.SETUP,
// data
// }))
// )
// .subscribe(message => {
// searchMessage$.next(message) // TODO: this shall not complete
// })
/* ----------------------------------------------------------------------- */ /**
* Initialize Material for MkDocs
*
* @param config - Configuration
*/
export function initialize(config: unknown) {
if (!isConfig(config))
throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)
/* Create viewport observables */ // pass config here!?
const offset$ = watchViewportOffset() const agent = setupAgent() // TODO: add a config parameter here to configure media queries
const size$ = watchViewportSize()
/* Create media observables */ const [
const screen$ = watchMedia("(min-width: 1220px)") searchWorkerRecv$,
const tablet$ = watchMedia("(min-width: 960px)") searchMessage$
] = setupWorkers(config)
/* Create location observables */
const location$ = watchLocation()
const fragment$ = watchLocationHash()
/* Create document observables */
const load$ = watchDocument()
// Complete set of AgentObservables...
// component map!
//
// const switch$ = watchDocumentSwitch({ location$ })
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
/* Create component map observable */ /* Create component map observable */
const components$ = watchComponentMap(names, { document$: load$ }) const components$ = watchComponentMap(names, { document$: agent.document.load$ })
const component = <T extends HTMLElement>(name: Component): Observable<T> => { const component = <T extends HTMLElement>(name: Component): Observable<T> => {
return components$ return components$
.pipe( .pipe(
@ -319,233 +262,136 @@ export function initialize(config: unknown) {
switchMap(watchHeader) switchMap(watchHeader)
) )
// DONE
const main$ = component("main")
.pipe(
setupMain(agent, { header$ })
)
// setupHeader(agent) ??
// setupSearch
// ----------------------------------------------------------------------------
/* Create header shadow toggle */ /* Create header shadow toggle */
component("header") component("header")
.pipe( .pipe(
switchMap(el => main$ switchMap(el => main$
.pipe( .pipe(
paintHeaderShadow(el) paintHeaderShadow(el) // technically, this could be done in paintMain
) )
) )
) )
.subscribe() .subscribe()
// ----------------------------------------------------------------------------
// watchSearchResult // emit, if at bottom... // watchSearchResult // emit, if at bottom...
// receive results as a second observable!? filter stuff, paint // receive results as a second observable!? filter stuff, paint
const result$ = search$ const result$ = searchWorkerRecv$ // move worker initialization into mountSearch ?
.pipe( .pipe(
tap(m => console.log("message from worker", m)),
filter(isSearchResultMessage), filter(isSearchResultMessage),
pluck("data") pluck("data")
) )
const query$ = component<HTMLInputElement>("query") // handleSearchResult <-- operator
const query$ = component<HTMLInputElement>("search-query")
.pipe( .pipe(
switchMap(el => fromEvent(el, "keyup") switchMap(el => watchSearchQuery(el, { prepare }))
.pipe(
map(() => prepareQuery(el.value))
)
)
) )
// DONE query$
component("result") .pipe<SearchMessage>(
.pipe( map(query => ({ // put this into some function...
setupSearchResult(agent, { result$, query$ }) type: SearchMessageType.QUERY,
data: query.value
})), // TODO. ugly...
// distinctUntilKeyChanged("data")
) )
.subscribe() .subscribe(searchMessage$)
// create the message subject internally... and link it to the worker...?
// watchSearchWorker(worker, agent, { query$ }) // message internally...
query$ query$
.pipe( .pipe(
map(data => ({ // put this into some function... tap(query => {
type: SearchMessageType.QUERY, if (query.focus)
data setToggle(search, true)
})), // TODO. ugly...
distinctUntilKeyChanged("data")
)
.subscribe(x => {
searchMessage$.next(x as any) // TODO
}) })
// Focus on search input
component("query")
.pipe(
switchMap(el => fromEvent(el, "focus")
.pipe(
tap(() => {
if (!search.checked)
search.click() // move this inside the search query stuff? not important...
})
)
) // not super nice...
) )
.subscribe() .subscribe()
// // WIP: instant loading
// load$
// .pipe(
// switchMap(({ body }) => fromEvent(body, "click")),
// switchMap(ev => {
// if (ev.target instanceof HTMLElement) {
// const el = ev.target.closest("a") || undefined
// if (el) {
// if (!/^(https?:|#)/.test(el.getAttribute("href")!)) {
// ev.preventDefault()
// }
// const href = el.href
// history.pushState({}, "", href) // TODO: reference necessary!?
// return of(href)
// }
// }
// return EMPTY
// })
// // try to reduce the jiggle upon instant page load. ideally, the location
// // should directly be resolved and the respective document loaded, but
// // we must scroll to the top at first and wait at least 250ms.
// //
// // Furthermore, this doesn't include the back/next buttons of the browser
// // which must be delayed
// // tap(url => {
// // if (!/#/.test(url))
// // scrollTo({ top: 0 })
// // }) // only when loading something we havent loaded!
// // delay(250)
// )
// .subscribe(location$)
// location$.subscribe(x => {
// console.log("L", x)
// })
// switch$.subscribe(x => {
// console.log("S", x)
// })
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
component("navigation") const main$ = component("main")
.pipe( .pipe(
setupNavigation(agent, { main$ }) mountMain(agent, { header$ })
) )
.subscribe()
component("toc") const navigation$ = component("navigation")
.pipe( .pipe(
setupTableOfContents(agent, { header$, main$ }) mountNavigation(agent, { main$ })
) )
.subscribe()
component("tabs") const toc$ = component("toc")
.pipe( .pipe(
setupTabs(agent, { header$ }) mountTableOfContents(agent, { header$, main$ })
) )
.subscribe()
component("hero") // TODO: naming?
const resultComponent$ = component("search-result")
.pipe( .pipe(
setupHero(agent, { header$ }) mountSearchResult(agent, { result$, query$: query$.pipe(pluck("value")) })
) // temporary fix
const tabs$ = component("tabs")
.pipe(
mountTabs(agent, { header$ })
) )
.subscribe()
// /* Create header title toggle */ const hero$ = component("hero")
// component("main") .pipe(
// .pipe( mountHero(agent, { header$ })
// delay(1000), // initial delay )
// switchMap(el => typeof getElement("h1", el) !== "undefined"
// ? watchBottomOffset(getElement("h1", el)!, { size$, offset$, header$ })
// .pipe(
// map(({ y }) => y >= 0),
// withLatestFrom(component("title")),
// tap(([active, title]) => {
// title.dataset.mdState = active ? "active" : ""
// })
// )
// : NEVER
// )
// )
// .subscribe()
// TODO: replace title as inner text
/* ----------------------------------------------------------------------- */ /* ----------------------------------------------------------------------- */
const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")! const drawer = getElement<HTMLInputElement>("[data-md-toggle=drawer]")!
const search = getElement<HTMLInputElement>("[data-md-toggle=search]")! const search = getElement<HTMLInputElement>("[data-md-toggle=search]")!
// watchToggle
// --> watchSearchQuery?
// watchSearch
// watchSearchReset
// toggles stay the same...
const a$ = watchToggle(search) const a$ = watchToggle(search)
.pipe( .pipe(
filter(identity), filter(identity),
delay(400) delay(400)
) )
// watchSearchReset() const reset$ = component("search-reset")
const b$ = component("reset")
.pipe( .pipe(
switchMap(watchSearchReset) switchMap(watchSearchReset)
) )
function focusQuery(): MonoTypeOperatorFunction<HTMLElement> { merge(a$, reset$)
return pipe(
tap(el => el.focus())
)
}
merge(a$, b$)
.pipe( .pipe(
switchMapTo(component("query")), switchMapTo(component<HTMLInputElement>("search-query")),
focusQuery() tap(el => el.focus())
) )
.subscribe() .subscribe()
/* Wrap all data tables for better overflow scrolling */ /* ----------------------------------------------------------------------- */
// const tables = getElements<HTMLTableElement>("table:not([class])")
// tables.forEach(table => {
// console.log("x", table)
// table.parentNode!.insertBefore(renderTable(table), table)
// table.replaceWith(renderTable(table) as any)
// // table.parentElement!.replaceChild(, table)
// })
return { const state = {
// agent, // agent.viewport.offset$ search: {
// component, // component.toc$ query$,
result$: resultComponent$,
reset$,
},
main$,
navigation$,
toc$,
tabs$,
hero$
} }
/* Return observable factories */ const { search: temp, ...rest } = state
return { merge(...values(rest), ...values(temp))
.subscribe() // potential memleak <-- use takeUntil
/* User interface */ return {
watchDocument: () => load$, agent,
// watchDocumentSwitch: () => switch$, state
watchLocation: () => location$,
watchLocationFragment: () => fragment$,
watchMediaScreen: () => screen$,
watchMediaTablet: () => tablet$,
watchViewportOffset: () => offset$,
watchViewportSize: () => size$
} }
} }

View File

@ -238,11 +238,7 @@
/> />
<!-- Overlay for expanded drawer --> <!-- Overlay for expanded drawer -->
<label <label class="md-overlay" for="__drawer"></label>
class="md-overlay"
data-md-component="overlay"
for="__drawer"
></label>
<!-- Link to skip to content --> <!-- Link to skip to content -->
{% if page.toc | first is defined %} {% if page.toc | first is defined %}