mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-08-03 11:18:36 -04:00
Merge of features tied to 'Trinidad Scorpion' funding goal
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 }))
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
56
src/assets/javascripts/templates/tabbed/index.tsx
Normal file
56
src/assets/javascripts/templates/tabbed/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user