Added more inline comments and simplified component mounting

This commit is contained in:
squidfunk
2021-02-13 17:03:15 +01:00
parent 96797b471f
commit aacb5ca5a7
45 changed files with 155 additions and 84 deletions

View File

@@ -31,8 +31,11 @@ import { getElementOrThrow, getLocation } from "~/browser"
*/
export type Flag =
| "header.autohide" /* Hide header */
| "navigation.tabs" /* Tabs navigation */
| "navigation.expand" /* Automatic expansion */
| "navigation.instant" /* Instant loading */
| "navigation.sections" /* Sections navigation */
| "navigation.tabs" /* Tabs navigation */
| "toc.integrate" /* Integrated table of contents */
/* ------------------------------------------------------------------------- */

View File

@@ -67,19 +67,19 @@ export function watchPrint(): Observable<void> {
/* ------------------------------------------------------------------------- */
/**
* Toggle an observable with another one
* Toggle an observable with a media observable
*
* @template T - Data type
*
* @param toggle$ - Toggle observable
* @param query$ - Media observable
* @param factory - Observable factory
*
* @returns Toggled observable
*/
export function at<T>(
toggle$: Observable<boolean>, factory: () => Observable<T>
query$: Observable<boolean>, factory: () => Observable<T>
): Observable<T> {
return toggle$
return query$
.pipe(
switchMap(active => active ? factory() : NEVER)
)

View File

@@ -20,6 +20,8 @@
* IN THE SOFTWARE.
*/
import { getElementOrThrow, getElements } from "~/browser"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
@@ -31,12 +33,14 @@ export type ComponentType =
| "announce" /* Announcement bar */
| "container" /* Container */
| "content" /* Content */
| "dialog" /* Dialog */
| "header" /* Header */
| "header-title" /* Header title */
| "main" /* Main area */
| "search" /* Search */
| "search-query" /* Search input */
| "search-result" /* Search results */
| "sidebar" /* Sidebar */
| "skip" /* Skip link */
| "source" /* Repository information */
| "tabs" /* Navigation tabs */
@@ -55,3 +59,39 @@ export type Component<
T & {
ref: U /* Component reference */
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Retrieve the element for a given component or throw a reference error
*
* @template T - Element type
*
* @param type - Component type
* @param node - Node of reference
*
* @returns Element
*/
export function getComponentElement<T extends HTMLElement>(
type: ComponentType, node: ParentNode = document
): T {
return getElementOrThrow(`[data-md-component=${type}]`, node)
}
/**
* Retrieve all elements for a given component
*
* @template T - Element type
*
* @param type - Component type
* @param node - Node of reference
*
* @returns Elements
*/
export function getComponentElements<T extends HTMLElement>(
type: ComponentType, node: ParentNode = document
): T[] {
return getElements(`[data-md-component=${type}]`, node)
}

View File

@@ -61,6 +61,9 @@ interface MountOptions {
/**
* Mount content
*
* This function mounts all components that are found in the content of the
* actual article, including code blocks, data tables and details.
*
* @param el - Content element
* @param options - Options
*

View File

@@ -111,8 +111,8 @@ export function watchCodeBlock(
/**
* Mount code block
*
* This function ensures that overflowing code blocks are focusable by keyboard,
* so they can be scrolled without a mouse to improve on accessibility.
* This function ensures that an overflowing code block is focusable through
* keyboard, so it can be scrolled without a mouse to improve on accessibility.
*
* @param el - Code block element
* @param options - Options

View File

@@ -88,8 +88,8 @@ export function watchDetails(
/**
* Mount details
*
* This function ensures that `details` tags are opened prior to printing, so
* the whole content of the page is included and on anchor jumps.
* This function ensures that `details` tags are opened on anchor jumps and
* prior to printing, so the whole content of the page is visible.
*
* @param el - Details element
* @param options - Options

View File

@@ -52,6 +52,9 @@ const sentinel = createElement("table")
/**
* Mount data table
*
* This function wraps a data table in another scrollable container, so they
* can be scrolled on smaller screen sizes and won't break the layout.
*
* @param el - Data table element
*
* @returns Data table component observable

View File

@@ -105,6 +105,9 @@ export function watchDialog(
/**
* Mount dialog
*
* This function makes the dialog in the right corner appear when a new alert
* is emitted through the subject that is passed as part of the options.
*
* @param el - Dialog element
* @param options - Options
*

View File

@@ -34,12 +34,15 @@ import {
combineLatestWith,
distinctUntilChanged,
distinctUntilKeyChanged,
endWith,
filter,
finalize,
map,
observeOn,
shareReplay,
startWith,
switchMap
switchMap,
tap
} from "rxjs/operators"
import { feature } from "~/_"
@@ -118,7 +121,7 @@ function isHidden({ viewport$ }: WatchOptions): Observable<boolean> {
.pipe(
filter(([{ offset }, [, y]]) => Math.abs(y - offset.y) > 100),
map(([, [direction]]) => direction),
distinctUntilChanged(),
distinctUntilChanged()
)
/* Compute threshold for autohiding */
@@ -127,7 +130,7 @@ function isHidden({ viewport$ }: WatchOptions): Observable<boolean> {
.pipe(
map(([{ offset }, search]) => offset.y > 400 && !search),
distinctUntilChanged(),
switchMap(active => active ? hidden$ : NEVER),
switchMap(active => active ? hidden$ : of(false)),
startWith(false)
)
}
@@ -173,10 +176,8 @@ export function watchHeader(
/**
* Mount header
*
* The header must be connected to the main area observable outside of the
* operator function, as the header will persist in-between document switches
* while the main area is replaced. However, the header observable must be
* passed to this function, so we connect both via a long-living subject.
* This function manages the different states of the header, i.e. whether it's
* hidden or rendered with a shadow. This depends heavily on the main area.
*
* @param el - Header element
* @param options - Options

View File

@@ -20,7 +20,12 @@
* IN THE SOFTWARE.
*/
import { NEVER, Observable, Subject, animationFrameScheduler } from "rxjs"
import {
NEVER,
Observable,
Subject,
animationFrameScheduler
} from "rxjs"
import {
distinctUntilKeyChanged,
finalize,
@@ -104,6 +109,9 @@ export function watchHeaderTitle(
/**
* Mount header title
*
* This function swaps the header title from the site title to the title of the
* current page when the user scrolls past the first headline.
*
* @param el - Header title element
* @param options - Options
*

View File

@@ -27,7 +27,6 @@ import { configuration } from "~/_"
import {
Keyboard,
getActiveElement,
getElementOrThrow,
getElements,
requestJSON,
setElementFocus,
@@ -41,7 +40,7 @@ import {
setupSearchWorker
} from "~/integrations"
import { Component } from "../../_"
import { Component, getComponentElement } from "../../_"
import { SearchQuery, mountSearchQuery } from "../query"
import { SearchResult, mountSearchResult } from "../result"
@@ -89,6 +88,9 @@ function fetchSearchIndex(url: string) {
/**
* Mount search
*
* This function sets up the search functionality, including the underlying
* web worker and all keyboard bindings.
*
* @param el - Search element
* @param options - Options
*
@@ -103,8 +105,8 @@ export function mountSearch(
))
/* Retrieve elements */
const query = getElementOrThrow("[data-md-component=search-query]", el)
const result = getElementOrThrow("[data-md-component=search-result]", el)
const query = getComponentElement("search-query", el)
const result = getComponentElement("search-result", el)
/* Re-emit query when search is ready */
const { tx$, rx$ } = worker

View File

@@ -64,6 +64,9 @@ let fetch$: Observable<Source>
/**
* Watch repository information
*
* This function will try to read the repository facts from session storage,
* and if unsuccessful, fetch them from the underlying provider.
*
* @param el - Repository information element
*
* @returns Repository information observable

View File

@@ -95,6 +95,9 @@ export function watchTabs(
/**
* Mount navigation tabs
*
* This function hides the navigation tabs when scrolling past the threshold
* and makes them reappear in a nice CSS animation when scrolling back up.
*
* @param el - Navigation tabs element
* @param options - Options
*

View File

@@ -34,8 +34,6 @@ import { feature } from "./_"
import {
at,
getElement,
getElementOrThrow,
getElements,
setToggle,
watchDocument,
watchKeyboard,
@@ -46,6 +44,8 @@ import {
watchViewport
} from "./browser"
import {
getComponentElement,
getComponentElements,
mountContent,
mountDialog,
mountHeader,
@@ -129,16 +129,11 @@ keyboard$
patchIndeterminate({ document$ })
patchScrollfix({ document$ })
/* Set up header observable */
const header$ = watchHeader(
getElementOrThrow("[data-md-component=header]"),
{ viewport$ }
)
/* Set up main area observable */
/* Set up header and main area observable */
const header$ = watchHeader(getComponentElement("header"), { viewport$ })
const main$ = document$
.pipe(
map(() => getElementOrThrow("[data-md-component=main]")),
map(() => getComponentElement("main")),
switchMap(el => watchMain(el, { viewport$, header$ })),
shareReplay(1)
)
@@ -147,23 +142,23 @@ const main$ = document$
const control$ = merge(
/* Dialog */
...getElements("[data-md-component=dialog]")
...getComponentElements("dialog")
.map(child => mountDialog(child, { alert$ })),
/* Header */
...getElements("[data-md-component=header]")
...getComponentElements("header")
.map(child => mountHeader(child, { viewport$, header$, main$ })),
/* Search */
...getElements("[data-md-component=search]")
...getComponentElements("search")
.map(child => mountSearch(child, { keyboard$ })),
/* Repository information */
...getElements("[data-md-component=source]")
...getComponentElements("source")
.map(child => mountSource(child as HTMLAnchorElement)),
/* Navigation tabs */
...getElements("[data-md-component=tabs]")
...getComponentElements("tabs")
.map(child => mountTabs(child, { viewport$, header$ })),
)
@@ -171,22 +166,22 @@ const control$ = merge(
const content$ = defer(() => merge(
/* Content */
...getElements("[data-md-component=content]")
...getComponentElements("content")
.map(child => mountContent(child, { target$, viewport$, print$ })),
/* Header title */
...getElements("[data-md-component=header-title]")
...getComponentElements("header-title")
.map(child => mountHeaderTitle(child, { viewport$, header$ })),
/* Sidebar */
...getElements("[data-md-component=sidebar]")
...getComponentElements("sidebar")
.map(child => child.getAttribute("data-md-type") === "navigation"
? at(screen$, () => mountSidebar(child, { viewport$, header$, main$ }))
: at(tablet$, () => mountSidebar(child, { viewport$, header$, main$ }))
),
/* Table of contents */
...getElements("[data-md-component=toc]")
...getComponentElements("toc")
.map(child => mountTableOfContents(child, { viewport$, header$ })),
))
@@ -194,7 +189,8 @@ const content$ = defer(() => merge(
const component$ = document$
.pipe(
switchMap(() => content$),
mergeWith(control$)
mergeWith(control$),
shareReplay(1)
)
/* Subscribe to all components */

View File

@@ -51,7 +51,6 @@ import {
ViewportOffset,
createElement,
getElement,
getElementOrThrow,
getElements,
replaceElement,
request,
@@ -60,6 +59,7 @@ import {
setLocationHash,
setViewportOffset
} from "~/browser"
import { getComponentElement } from "~/components"
/* ----------------------------------------------------------------------------
* Types
@@ -261,14 +261,12 @@ export function setupInstantLoading(
setViewportOffset(offset || { y: 0 })
})
/* Replace components */
/* Replace meta tags and components */
document$
.pipe(
skip(1)
)
.subscribe(replacement => {
/* Replace meta tags and components */
for (const selector of [
/* Meta tags */
@@ -298,7 +296,7 @@ export function setupInstantLoading(
document$
.pipe(
skip(1),
map(() => getElementOrThrow("[data-md-component=container]")),
map(() => getComponentElement("container")),
switchMap(el => of(...getElements("script", el))),
concatMap(el => {
const script = createElement("script")

View File

@@ -42,6 +42,9 @@ interface PatchOptions {
/**
* Patch indeterminate checkboxes
*
* This function will replace the indeterminate "pseudo state" with the actual
* indeterminate state, which is used to keep navigation always expanded.
*
* @param options - Options
*/
export function patchIndeterminate(

View File

@@ -34,7 +34,7 @@ import { h } from "~/utilities"
*
* @returns Element
*/
export function renderClipboardButton(id: string) {
export function renderClipboardButton(id: string): HTMLElement {
return (
<button
class="md-clipboard md-icon"

View File

@@ -47,7 +47,7 @@ const enum Flag {
/**
* Render a search document
*
* @param section - Search document
* @param document - Search document
* @param flag - Render flags
*
* @returns Element
@@ -106,8 +106,8 @@ function renderSearchDocument(
* @returns Element
*/
export function renderSearchResult(
result: SearchResult, threshold: number = Infinity
) {
result: SearchResult, threshold = Infinity
): HTMLElement {
const docs = [...result]
/* Find and extract parent article */

View File

@@ -34,7 +34,7 @@ import { h } from "~/utilities"
*
* @returns Element
*/
export function renderSourceFacts(facts: SourceFacts) {
export function renderSourceFacts(facts: SourceFacts): HTMLElement {
return (
<ul class="md-source__facts">
{facts.map(fact => (

View File

@@ -33,7 +33,7 @@ import { h } from "utilities"
*
* @returns Element
*/
export function renderTable(table: HTMLElement) {
export function renderTable(table: HTMLElement): HTMLElement {
return (
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">

View File

@@ -20,5 +20,5 @@
* IN THE SOFTWARE.
*/
export * from "./jsx"
export * from "./h"
export * from "./string"