Merge of features tied to 'Trinidad Scorpion' funding goal

This commit is contained in:
squidfunk
2022-06-02 17:20:33 +02:00
parent a3f457ecb1
commit e2556d84d6
89 changed files with 13701 additions and 290 deletions

View File

@@ -31,6 +31,7 @@ import { getElement, getLocation } from "~/browser"
*/
export type Flag =
| "content.code.annotate" /* Code annotations */
| "content.tabs.link" /* Link content tabs */
| "header.autohide" /* Hide header */
| "navigation.expand" /* Automatic expansion */
| "navigation.indexes" /* Section pages */

View File

@@ -54,9 +54,9 @@ export interface Mermaid {}
let mermaid$: Observable<void>
/**
* Global index for Mermaid integration
* Global sequence number for diagrams
*/
let index = 0
let sequence = 0
/* ----------------------------------------------------------------------------
* Helper functions
@@ -101,7 +101,7 @@ export function mountMermaid(
/* Render diagram */
mermaid$.subscribe(() => {
el.classList.add("mermaid") // Hack: mitigate https://bit.ly/3CiN6Du
const id = `__mermaid_${index++}`
const id = `__mermaid_${sequence++}`
const host = h("div", { class: "mermaid" })
mermaid.mermaidAPI.render(id, el.textContent, (svg: string) => {

View File

@@ -32,6 +32,7 @@ import {
fromEvent,
map,
merge,
skip,
startWith,
subscribeOn,
takeLast,
@@ -39,13 +40,18 @@ import {
tap
} from "rxjs"
import { feature } from "~/_"
import {
getElement,
getElementContentOffset,
getElementContentSize,
getElementOffset,
getElementSize,
getElements,
watchElementContentOffset,
watchElementSize
} from "~/browser"
import { renderTabbedControl } from "~/templates"
import { Component } from "../../_"
@@ -75,18 +81,15 @@ export function watchContentTabs(
el: HTMLElement
): Observable<ContentTabs> {
const inputs = getElements<HTMLInputElement>(":scope > input", el)
const active = inputs.find(input => input.checked) || inputs[0]
const initial = inputs.find(input => input.checked) || inputs[0]
return merge(...inputs.map(input => fromEvent(input, "change")
.pipe(
map(() => ({
active: getElement(`label[for=${input.id}]`)
}) as ContentTabs)
map(() => getElement<HTMLLabelElement>(`label[for=${input.id}]`))
)
))
.pipe(
startWith({
active: getElement(`label[for=${active.id}]`)
} as ContentTabs)
startWith(getElement<HTMLLabelElement>(`label[for=${initial.id}]`)),
map(active => ({ active }))
)
}
@@ -105,18 +108,29 @@ export function watchContentTabs(
export function mountContentTabs(
el: HTMLElement
): Observable<Component<ContentTabs>> {
/* Render content tab previous button for pagination */
const prev = renderTabbedControl("prev")
el.append(prev)
/* Render content tab next button for pagination */
const next = renderTabbedControl("next")
el.append(next)
/* Mount component on subscription */
const container = getElement(".tabbed-labels", el)
return defer(() => {
const push$ = new Subject<ContentTabs>()
const done$ = push$.pipe(takeLast(1))
combineLatest([push$, watchElementSize(el)])
.pipe(
auditTime(1, animationFrameScheduler),
takeUntil(push$.pipe(takeLast(1)))
takeUntil(done$)
)
.subscribe({
/* Handle emission */
next([{ active }]) {
next([{ active }, size]) {
const offset = getElementOffset(active)
const { width } = getElementSize(active)
@@ -124,11 +138,12 @@ export function mountContentTabs(
el.style.setProperty("--md-indicator-x", `${offset.x}px`)
el.style.setProperty("--md-indicator-width", `${width}px`)
/* Smoothly scroll container */
container.scrollTo({
behavior: "smooth",
left: offset.x
})
/* Scroll container to active content tab */
const content = getElementContentOffset(container)
if (offset.x < content.x)
prev.click()
else if (offset.x + width > content.x + size.width)
next.click()
},
/* Handle complete */
@@ -138,6 +153,57 @@ export function mountContentTabs(
}
})
/* Hide content tab buttons on borders */
combineLatest([
watchElementContentOffset(container),
watchElementSize(container)
])
.pipe(
takeUntil(done$)
)
.subscribe(([offset, size]) => {
const content = getElementContentSize(container)
prev.hidden = offset.x < 16
next.hidden = offset.x > content.width - size.width - 16
})
/* Paginate content tab container on click */
merge(
fromEvent(prev, "click").pipe(map(() => -1)),
fromEvent(next, "click").pipe(map(() => +1))
)
.pipe(
takeUntil(done$)
)
.subscribe(direction => {
const { width } = getElementSize(container)
container.scrollBy({
left: width * direction,
behavior: "smooth"
})
})
/* Set up linking of content tabs, if enabled */
if (feature("content.tabs.link"))
push$.pipe(skip(1))
.subscribe(({ active }) => {
const tab = active.innerText.trim()
for (const set of getElements("[data-tabs]"))
for (const input of getElements<HTMLInputElement>(
":scope > input", set
)) {
const label = getElement(`label[for=${input.id}]`)
if (label.innerText.trim() === tab) {
input.click()
break
}
}
/* Persist active tabs in local storage */
const tabs = __md_get<string[]>("__tabs") || []
__md_set("__tabs", [...new Set([tab, ...tabs])])
})
/* Create and return component */
return watchContentTabs(el)
.pipe(

View File

@@ -113,11 +113,8 @@ export function mountDialog(
return defer(() => {
const push$ = new Subject<Dialog>()
push$.subscribe(({ message, active }) => {
el.classList.toggle("md-dialog--active", active)
inner.textContent = message
if (active)
el.setAttribute("data-md-state", "open")
else
el.removeAttribute("data-md-state")
})
/* Create and return component */

View File

@@ -175,16 +175,15 @@ export function mountHeader(
): Observable<Component<Header>> {
return defer(() => {
const push$ = new Subject<Main>()
const done$ = push$.pipe(takeLast(1))
push$
.pipe(
distinctUntilKeyChanged("active"),
combineLatestWith(header$)
)
.subscribe(([{ active }, { hidden }]) => {
if (active)
el.setAttribute("data-md-state", hidden ? "hidden" : "shadow")
else
el.removeAttribute("data-md-state")
el.classList.toggle("md-header--hidden", hidden)
el.classList.toggle("md-header--shadow", active && !hidden)
})
/* Link to main area */
@@ -193,7 +192,7 @@ export function mountHeader(
/* Create and return component */
return header$
.pipe(
takeUntil(push$.pipe(takeLast(1))),
takeUntil(done$),
map(state => ({ ref: el, ...state }))
)
})

View File

@@ -116,10 +116,7 @@ export function mountHeaderTitle(
return defer(() => {
const push$ = new Subject<HeaderTitle>()
push$.subscribe(({ active }) => {
if (active)
el.setAttribute("data-md-state", "active")
else
el.removeAttribute("data-md-state")
el.classList.toggle("md-header__title--active", active)
})
/* Obtain headline, if any */

View File

@@ -32,6 +32,7 @@ import {
fromEvent,
map,
merge,
share,
shareReplay,
startWith,
take,
@@ -155,6 +156,7 @@ export function mountSearchQuery(
el: HTMLInputElement, { tx$, rx$ }: SearchWorker
): Observable<Component<SearchQuery, HTMLInputElement>> {
const push$ = new Subject<SearchQuery>()
const done$ = push$.pipe(takeLast(1))
/* Handle value changes */
push$
@@ -184,7 +186,7 @@ export function mountSearchQuery(
/* Handle reset */
fromEvent(el.form!, "reset")
.pipe(
takeUntil(push$.pipe(takeLast(1)))
takeUntil(done$)
)
.subscribe(() => el.focus())
@@ -193,6 +195,7 @@ export function mountSearchQuery(
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
map(state => ({ ref: el, ...state })),
share()
)
}

View File

@@ -113,7 +113,7 @@ export function mountSource(
const push$ = new Subject<Source>()
push$.subscribe(({ facts }) => {
inner.appendChild(renderSourceFacts(facts))
inner.setAttribute("data-md-state", "done")
inner.classList.add("md-source__repository--active")
})
/* Create and return component */

View File

@@ -120,15 +120,12 @@ export function mountTabs(
/* Handle emission */
next({ hidden }) {
if (hidden)
el.setAttribute("data-md-state", "hidden")
else
el.removeAttribute("data-md-state")
el.classList.toggle("md-tabs--hidden", hidden)
},
/* Handle complete */
complete() {
el.removeAttribute("data-md-state")
el.classList.remove("md-tabs--hidden")
}
})

View File

@@ -269,19 +269,18 @@ export function mountTableOfContents(
): Observable<Component<TableOfContents>> {
return defer(() => {
const push$ = new Subject<TableOfContents>()
const done$ = push$.pipe(takeLast(1))
push$.subscribe(({ prev, next }) => {
/* Look forward */
for (const [anchor] of next) {
anchor.removeAttribute("data-md-state")
anchor.classList.remove(
"md-nav__link--active"
)
anchor.classList.remove("md-nav__link--passed")
anchor.classList.remove("md-nav__link--active")
}
/* Look backward */
for (const [index, [anchor]] of prev.entries()) {
anchor.setAttribute("data-md-state", "blur")
anchor.classList.add("md-nav__link--passed")
anchor.classList.toggle(
"md-nav__link--active",
index === prev.length - 1
@@ -293,7 +292,7 @@ export function mountTableOfContents(
if (feature("navigation.tracking"))
viewport$
.pipe(
takeUntil(push$.pipe(takeLast(1))),
takeUntil(done$),
distinctUntilKeyChanged("offset"),
debounceTime(250),
skip(1),

View File

@@ -134,16 +134,16 @@ export function mountBackToTop(
el: HTMLElement, { viewport$, header$, main$, target$ }: MountOptions
): Observable<Component<BackToTop>> {
const push$ = new Subject<BackToTop>()
const done$ = push$.pipe(takeLast(1))
push$.subscribe({
/* Handle emission */
next({ hidden }) {
el.classList.toggle("md-top--hidden", hidden)
if (hidden) {
el.setAttribute("data-md-state", "hidden")
el.setAttribute("tabindex", "-1")
el.blur()
} else {
el.removeAttribute("data-md-state")
el.removeAttribute("tabindex")
}
},
@@ -151,7 +151,7 @@ export function mountBackToTop(
/* Handle complete */
complete() {
el.style.top = ""
el.setAttribute("data-md-state", "hidden")
el.classList.add("md-top--hidden")
el.removeAttribute("tabindex")
}
})
@@ -159,7 +159,7 @@ export function mountBackToTop(
/* Watch header height */
header$
.pipe(
takeUntil(push$.pipe(endWith(0), takeLast(1))),
takeUntil(done$),
distinctUntilKeyChanged("height")
)
.subscribe(({ height }) => {

View File

@@ -56,6 +56,7 @@ export interface SearchIndexDocument {
title: string /* Document title */
text: string /* Document text */
tags?: string[] /* Document tags */
boost?: number /* Document boost */
}
/* ------------------------------------------------------------------------- */
@@ -207,7 +208,7 @@ export class Search {
/* Index documents */
for (const doc of docs)
this.add(doc)
this.add(doc, { boost: doc.boost })
})
}

View File

@@ -63,7 +63,8 @@ export function patchIndeterminate(
document$
.pipe(
switchMap(() => getElements<HTMLInputElement>(
"[data-md-state=indeterminate]"
// @todo `data-md-state` is deprecated and removed in v9
".md-toggle--indeterminate, [data-md-state=indeterminate]"
)),
tap(el => {
el.indeterminate = true
@@ -71,14 +72,14 @@ export function patchIndeterminate(
}),
mergeMap(el => fromEvent(el, "change")
.pipe(
takeWhile(() => el.hasAttribute("data-md-state")),
takeWhile(() => el.classList.contains("md-toggle--indeterminate")),
map(() => el)
)
),
withLatestFrom(tablet$)
)
.subscribe(([el, tablet]) => {
el.removeAttribute("data-md-state")
el.classList.remove("md-toggle--indeterminate")
if (tablet)
el.checked = false
})

View File

@@ -76,11 +76,11 @@ export function patchScrolllock(
)
.subscribe(([active, { offset: { y }}]) => {
if (active) {
document.body.setAttribute("data-md-state", "lock")
document.body.setAttribute("data-md-scrolllock", "")
document.body.style.top = `-${y}px`
} else {
const value = -1 * parseInt(document.body.style.top, 10)
document.body.removeAttribute("data-md-state")
document.body.removeAttribute("data-md-scrolllock")
document.body.style.top = ""
if (value)
window.scrollTo(0, value)

View File

@@ -24,5 +24,6 @@ export * from "./annotation"
export * from "./clipboard"
export * from "./search"
export * from "./source"
export * from "./tabbed"
export * from "./table"
export * from "./version"

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2016-2022 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 { h } from "~/utilities"
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Tabbed control type
*/
type TabbedControlType =
| "prev"
| "next"
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Render control for content tabs
*
* @param type - Control type
*
* @returns Element
*/
export function renderTabbedControl(
type: TabbedControlType
): HTMLElement {
const classes = `tabbed-control tabbed-control--${type}`
return (
<div class={classes} hidden>
<button class="tabbed-button" tabIndex={-1}></button>
</div>
)
}

View File

@@ -54,7 +54,7 @@ function renderVersion(version: Version): HTMLElement {
const url = new URL(`../${version.version}/`, config.base)
return (
<li class="md-version__item">
<a href={url.toString()} class="md-version__link">
<a href={`${url}`} class="md-version__link">
{version.title}
</a>
</li>

View File

@@ -100,11 +100,16 @@ export function h<T extends h.JSX.Element>(
/* Set attributes, if any */
if (attributes)
for (const attr of Object.keys(attributes))
for (const attr of Object.keys(attributes)) {
if (typeof attributes[attr] === "undefined")
continue
/* Set default attribute or boolean */
if (typeof attributes[attr] !== "boolean")
el.setAttribute(attr, attributes[attr])
else if (attributes[attr])
else
el.setAttribute(attr, "")
}
/* Append child nodes */
for (const child of children)