1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2024-11-24 07:30:12 +01:00

Restructured content observables

This commit is contained in:
squidfunk 2021-12-05 16:20:50 +01:00
parent 86a25e802d
commit bac1301710
24 changed files with 263 additions and 289 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

@ -34,7 +34,7 @@
{% endif %}
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.c31396f4.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.9f2461a2.min.css' | url }}">
{% if config.theme.palette %}
{% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.9204c3b2.min.css' | url }}">
@ -213,7 +213,7 @@
</script>
{% endblock %}
{% block scripts %}
<script src="{{ 'assets/javascripts/bundle.7ca7dfaa.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.195ba817.min.js' | url }}"></script>
{% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script>
{% endfor %}

View File

@ -25,11 +25,8 @@ import { Observable, merge } from "rxjs"
import { getElements } from "~/browser"
import { Component } from "../../_"
import {
Annotation,
CodeBlock,
mountCodeBlock
} from "../code"
import { Annotation } from "../annotation"
import { CodeBlock, mountCodeBlock } from "../code"
import { Details, mountDetails } from "../details"
import { DataTable, mountDataTable } from "../table"
import { ContentTabs, mountContentTabs } from "../tabs"

View File

@ -45,18 +45,18 @@ import {
watchElementOffset
} from "~/browser"
import { Component } from "../../../../_"
import { Component } from "../../../_"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Code annotation
* Annotation
*/
export interface Annotation {
active: boolean /* Code annotation is visible */
offset: ElementOffset /* Code annotation offset */
active: boolean /* Annotation is active */
offset: ElementOffset /* Annotation offset */
}
/* ----------------------------------------------------------------------------
@ -64,12 +64,12 @@ export interface Annotation {
* ------------------------------------------------------------------------- */
/**
* Watch code annotation
* Watch annotation
*
* @param el - Code annotation element
* @param container - Containing code block element
* @param el - Annotation element
* @param container - Containing element
*
* @returns Code annotation observable
* @returns Annotation observable
*/
export function watchAnnotation(
el: HTMLElement, container: HTMLElement
@ -88,7 +88,7 @@ export function watchAnnotation(
})
)
/* Actively watch code annotation on focus */
/* Actively watch annotation on focus */
return watchElementFocus(el)
.pipe(
switchMap(active => offset$
@ -101,12 +101,12 @@ export function watchAnnotation(
}
/**
* Mount code annotation
* Mount annotation
*
* @param el - Code annotation element
* @param container - Containing code block element
* @param el - Annotation element
* @param container - Containing element
*
* @returns Code annotation component observable
* @returns Annotation component observable
*/
export function mountAnnotation(
el: HTMLElement, container: HTMLElement

View File

@ -38,7 +38,7 @@ import {
} from "~/browser"
import { renderAnnotation } from "~/templates"
import { Component } from "../../../../_"
import { Component } from "../../../_"
import {
Annotation,
mountAnnotation
@ -60,11 +60,11 @@ interface MountOptions {
* ------------------------------------------------------------------------- */
/**
* Find all code annotation markers in the given code block
* Find all annotation markers in the given code block
*
* @param container - Containing code block element
* @param container - Containing element
*
* @returns Code annotation markers
* @returns Annotation markers
*/
function findAnnotationMarkers(container: HTMLElement): Text[] {
const markers: Text[] = []
@ -97,18 +97,18 @@ function swap(source: HTMLElement, target: HTMLElement): void {
* ------------------------------------------------------------------------- */
/**
* Mount code annotation list
* Mount annotation list
*
* This function analyzes the given container code block and checks for markers
* referring to elements in the given code annotation list. If no markers are
* found, the list is left untouched. Otherwise, list elements are rendered as
* code annotations inside the code block.
* This function analyzes the containing code block and checks for markers
* referring to elements in the given annotation list. If no markers are found,
* the list is left untouched. Otherwise, list elements are rendered as
* annotations inside the code block.
*
* @param el - Code annotation list element
* @param container - Containing code block element
* @param el - Annotation list element
* @param container - Containing element
* @param options - Options
*
* @returns Code annotation list component observable
* @returns Annotation component observable
*/
export function mountAnnotationList(
el: HTMLElement, container: HTMLElement, { print$ }: MountOptions
@ -138,7 +138,7 @@ export function mountAnnotationList(
.subscribe(active => {
el.hidden = !active
/* Move annotation contents back into list */
/* Show annotations in code block or list (print) */
for (const [id, annotation] of annotations) {
const inner = getElement(".md-typeset", annotation)
const child = getElement(`li:nth-child(${id})`, el)

View File

@ -1,216 +0,0 @@
/*
* Copyright (c) 2016-2021 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 ClipboardJS from "clipboard"
import {
EMPTY,
Observable,
Subject,
defer,
distinctUntilChanged,
distinctUntilKeyChanged,
finalize,
map,
mergeWith,
switchMap,
takeLast,
takeUntil,
tap
} from "rxjs"
import { feature } from "~/_"
import {
getElementContentSize,
watchElementSize
} from "~/browser"
import { renderClipboardButton } from "~/templates"
import { Component } from "../../../_"
import {
Annotation,
mountAnnotationList
} from "../annotation"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Code block
*/
export interface CodeBlock {
scrollable: boolean /* Code block overflows */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
print$: Observable<boolean> /* Media print observable */
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Global sequence number for Clipboard.js integration
*/
let sequence = 0
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Find candidate list element directly following a code block
*
* @param el - Code block element
*
* @returns List element or nothing
*/
function findCandidateList(el: HTMLElement): HTMLElement | undefined {
if (el.nextElementSibling) {
const sibling = el.nextElementSibling as HTMLElement
if (sibling.tagName === "OL")
return sibling
/* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
else if (sibling.tagName === "P" && !sibling.children.length)
return findCandidateList(sibling)
}
/* Everything else */
return undefined
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch code block
*
* This function monitors size changes of the viewport, as well as switches of
* content tabs with embedded code blocks, as both may trigger overflow.
*
* @param el - Code block element
*
* @returns Code block observable
*/
export function watchCodeBlock(
el: HTMLElement
): Observable<CodeBlock> {
return watchElementSize(el)
.pipe(
map(({ width }) => {
const content = getElementContentSize(el)
return {
scrollable: content.width > width
}
}),
distinctUntilKeyChanged("scrollable")
)
}
/**
* Mount code block
*
* This function ensures that an overflowing code block is focusable through
* keyboard, so it can be scrolled without a mouse to improve on accessibility.
* Furthermore, if code annotations are enabled, they are mounted if and only
* if the code block is currently visible, e.g., not in a hidden content tab.
*
* @param el - Code block element
* @param options - Options
*
* @returns Code block and annotation component observable
*/
export function mountCodeBlock(
el: HTMLElement, options: MountOptions
): Observable<Component<CodeBlock | Annotation>> {
const { matches: hover } = matchMedia("(hover)")
return defer(() => {
const push$ = new Subject<CodeBlock>()
push$.subscribe(({ scrollable }) => {
if (scrollable && hover)
el.setAttribute("tabindex", "0")
else
el.removeAttribute("tabindex")
})
/* Render button for Clipboard.js integration */
if (ClipboardJS.isSupported()) {
const parent = el.closest("pre")!
parent.id = `__code_${++sequence}`
parent.insertBefore(
renderClipboardButton(parent.id),
el
)
}
/* Handle code annotations */
const container = el.closest([
":not(td.code) > .highlight", /* Code blocks */
".highlighttable" /* Code blocks with line numbers */
].join(", "))
if (container instanceof HTMLElement) {
const list = findCandidateList(container)
/* Mount code annotations, if enabled */
if (typeof list !== "undefined" && (
container.classList.contains("annotate") ||
feature("content.code.annotate")
)) {
const annotations$ = mountAnnotationList(list, el, options)
/* Create and return component */
return watchCodeBlock(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state })),
mergeWith(watchElementSize(container)
.pipe(
takeUntil(push$.pipe(takeLast(1))),
map(({ width, height }) => width && height),
distinctUntilChanged(),
switchMap(active => active ? annotations$ : EMPTY)
)
)
)
}
}
/* Create and return component */
return watchCodeBlock(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@ -20,5 +20,197 @@
* IN THE SOFTWARE.
*/
export * from "./_"
export * from "./annotation"
import ClipboardJS from "clipboard"
import {
EMPTY,
Observable,
Subject,
defer,
distinctUntilChanged,
distinctUntilKeyChanged,
finalize,
map,
mergeWith,
switchMap,
takeLast,
takeUntil,
tap
} from "rxjs"
import { feature } from "~/_"
import {
getElementContentSize,
watchElementSize
} from "~/browser"
import { renderClipboardButton } from "~/templates"
import { Component } from "../../_"
import {
Annotation,
mountAnnotationList
} from "../annotation"
/* ----------------------------------------------------------------------------
* Types
* ------------------------------------------------------------------------- */
/**
* Code block
*/
export interface CodeBlock {
scrollable: boolean /* Code block overflows */
}
/* ----------------------------------------------------------------------------
* Helper types
* ------------------------------------------------------------------------- */
/**
* Mount options
*/
interface MountOptions {
print$: Observable<boolean> /* Media print observable */
}
/* ----------------------------------------------------------------------------
* Data
* ------------------------------------------------------------------------- */
/**
* Global sequence number for Clipboard.js integration
*/
let sequence = 0
/* ----------------------------------------------------------------------------
* Helper functions
* ------------------------------------------------------------------------- */
/**
* Find candidate list element directly following a code block
*
* @param el - Code block element
*
* @returns List element or nothing
*/
function findCandidateList(el: HTMLElement): HTMLElement | undefined {
if (el.nextElementSibling) {
const sibling = el.nextElementSibling as HTMLElement
if (sibling.tagName === "OL")
return sibling
/* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
else if (sibling.tagName === "P" && !sibling.children.length)
return findCandidateList(sibling)
}
/* Everything else */
return undefined
}
/* ----------------------------------------------------------------------------
* Functions
* ------------------------------------------------------------------------- */
/**
* Watch code block
*
* This function monitors size changes of the viewport, as well as switches of
* content tabs with embedded code blocks, as both may trigger overflow.
*
* @param el - Code block element
*
* @returns Code block observable
*/
export function watchCodeBlock(
el: HTMLElement
): Observable<CodeBlock> {
return watchElementSize(el)
.pipe(
map(({ width }) => {
const content = getElementContentSize(el)
return {
scrollable: content.width > width
}
}),
distinctUntilKeyChanged("scrollable")
)
}
/**
* Mount code block
*
* This function ensures that an overflowing code block is focusable through
* keyboard, so it can be scrolled without a mouse to improve on accessibility.
* Furthermore, if code annotations are enabled, they are mounted if and only
* if the code block is currently visible, e.g., not in a hidden content tab.
*
* @param el - Code block element
* @param options - Options
*
* @returns Code block and annotation component observable
*/
export function mountCodeBlock(
el: HTMLElement, options: MountOptions
): Observable<Component<CodeBlock | Annotation>> {
const { matches: hover } = matchMedia("(hover)")
return defer(() => {
const push$ = new Subject<CodeBlock>()
push$.subscribe(({ scrollable }) => {
if (scrollable && hover)
el.setAttribute("tabindex", "0")
else
el.removeAttribute("tabindex")
})
/* Render button for Clipboard.js integration */
if (ClipboardJS.isSupported()) {
const parent = el.closest("pre")!
parent.id = `__code_${++sequence}`
parent.insertBefore(
renderClipboardButton(parent.id),
el
)
}
/* Handle code annotations */
const container = el.closest([
":not(td.code) > .highlight", /* Code blocks */
".highlighttable" /* Code blocks with line numbers */
].join(", "))
if (container instanceof HTMLElement) {
const list = findCandidateList(container)
/* Mount code annotations, if enabled */
if (typeof list !== "undefined" && (
container.classList.contains("annotate") ||
feature("content.code.annotate")
)) {
const annotations$ = mountAnnotationList(list, el, options)
/* Create and return component */
return watchCodeBlock(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state })),
mergeWith(watchElementSize(container)
.pipe(
takeUntil(push$.pipe(takeLast(1))),
map(({ width, height }) => width && height),
distinctUntilChanged(),
switchMap(active => active ? annotations$ : EMPTY)
)
)
)
}
}
/* Create and return component */
return watchCodeBlock(el)
.pipe(
tap(state => push$.next(state)),
finalize(() => push$.complete()),
map(state => ({ ref: el, ...state }))
)
})
}

View File

@ -43,7 +43,7 @@ import { Component } from "../../_"
*/
export interface Details {
action: "open" | "close" /* Details state */
scroll?: boolean /* Scroll into view */
reveal?: boolean /* Details is revealed */
}
/* ----------------------------------------------------------------------------
@ -89,7 +89,7 @@ export function watchDetails(
.pipe(
map(target => target.closest("details:not([open])")!),
filter(details => el === details),
mapTo<Details>({ action: "open", scroll: true })
mapTo<Details>({ action: "open", reveal: true })
),
/* Open details on print and close afterwards */
@ -120,12 +120,12 @@ export function mountDetails(
): Observable<Component<Details>> {
return defer(() => {
const push$ = new Subject<Details>()
push$.subscribe(({ action, scroll }) => {
push$.subscribe(({ action, reveal }) => {
if (action === "open")
el.setAttribute("open", "")
else
el.removeAttribute("open")
if (scroll)
if (reveal)
el.scrollIntoView()
})

View File

@ -21,6 +21,7 @@
*/
export * from "./_"
export * from "./annotation"
export * from "./code"
export * from "./details"
export * from "./table"

View File

@ -46,7 +46,7 @@ import { Component } from "../_"
*/
export interface Dialog {
message: string /* Dialog message */
open: boolean /* Dialog is visible */
active: boolean /* Dialog is active */
}
/* ----------------------------------------------------------------------------
@ -89,7 +89,7 @@ export function watchDialog(
of(false).pipe(delay(2000))
)
.pipe(
map(open => ({ message, open }))
map(active => ({ message, active }))
)
)
)
@ -112,9 +112,9 @@ export function mountDialog(
const inner = getElement(".md-typeset", el)
return defer(() => {
const push$ = new Subject<Dialog>()
push$.subscribe(({ message, open }) => {
push$.subscribe(({ message, active }) => {
inner.textContent = message
if (open)
if (active)
el.setAttribute("data-md-state", "open")
else
el.removeAttribute("data-md-state")

View File

@ -59,7 +59,7 @@ import { Main } from "../../main"
export interface Header {
height: number /* Header visible height */
sticky: boolean /* Header stickyness */
hidden: boolean /* User scrolled past threshold */
hidden: boolean /* Header is hidden */
}
/* ----------------------------------------------------------------------------

View File

@ -49,7 +49,7 @@ import { Header } from "../_"
* Header
*/
export interface HeaderTitle {
active: boolean /* User scrolled past first headline */
active: boolean /* Header title is active */
}
/* ----------------------------------------------------------------------------

View File

@ -46,7 +46,7 @@ import { Header } from "../header"
export interface Main {
offset: number /* Main area top offset */
height: number /* Main area visible height */
active: boolean /* User scrolled past header */
active: boolean /* Main area is active */
}
/* ----------------------------------------------------------------------------

View File

@ -53,7 +53,7 @@ import { Main } from "../main"
*/
export interface Sidebar {
height: number /* Sidebar height */
locked: boolean /* User scrolled past header */
locked: boolean /* Sidebar is locked */
}
/* ----------------------------------------------------------------------------

View File

@ -50,7 +50,7 @@ import { Header } from "../header"
* Navigation tabs
*/
export interface Tabs {
hidden: boolean /* User scrolled past tabs */
hidden: boolean /* Navigation tabs are hidden */
}
/* ----------------------------------------------------------------------------

View File

@ -49,7 +49,7 @@ import { Main } from "../main"
* Back-to-top button
*/
export interface BackToTop {
hidden: boolean /* User scrolled up */
hidden: boolean /* Back-to-top button is hidden */
}
/* ----------------------------------------------------------------------------

View File

@ -27,7 +27,7 @@ import { h } from "~/utilities"
* ------------------------------------------------------------------------- */
/**
* Render an empty code annotation
* Render an empty annotation
*
* @param id - Annotation identifier
*

View File

@ -41,8 +41,8 @@
@import "main/icons";
@import "main/typeset";
@import "main/layout/base";
@import "main/layout/banner";
@import "main/layout/base";
@import "main/layout/clipboard";
@import "main/layout/content";
@import "main/layout/dialog";

View File

@ -40,7 +40,7 @@
}
// ----------------------------------------------------------------------------
// Rules: layout
// Rules
// ----------------------------------------------------------------------------
// Tooltip variables
@ -137,12 +137,12 @@
// ----------------------------------------------------------------------------
// Code annotation
// Annotation
.md-annotation {
white-space: normal;
outline: none;
// Code annotation is not hidden (e.g. when copying)
// Annotation is not hidden (e.g. when copying)
&:not([hidden]) {
display: inline-block;
}
@ -152,19 +152,19 @@
z-index: 2;
}
// Code annotation wrapper (= tooltip)
// Annotation wrapper (= tooltip)
&__inner {
top: calc(var(--md-tooltip-y) + 1.2ch);
font-family: var(--md-text-font-family);
// Code annotation tooltip when not focused
// Annotation tooltip when not focused
:not(:focus-within) > & {
user-select: none;
pointer-events: none;
}
}
// Code annotation index
// Annotation index
&__index {
position: relative;
z-index: 0;
@ -174,10 +174,10 @@
transition: z-index 250ms;
user-select: none;
// Code annotation marker the marker must be positioned absolutely behind
// Annotation marker the marker must be positioned absolutely behind
// the index, because it shouldn't impact the rendering of a code block.
// Otherwise, small rounding differences in browsers can sometimes mess up
// alignment of text following a code annotation.
// alignment of text following a Annotation.
&::after {
position: absolute;
top: 0.025em;
@ -201,12 +201,12 @@
animation: none;
}
// Code annotation marker on focus/hover
// Annotation marker on focus/hover
:is(:focus-within, :hover) > & {
background-color: var(--md-accent-fg-color);
}
// Code annotation marker on focus
// Annotation marker on focus
:focus-within > & {
transition:
color 250ms,
@ -220,12 +220,12 @@
}
}
// Code annotation index on focus/hover
// Annotation index on focus/hover
:is(:focus-within, :hover) > & {
color: var(--md-accent-bg-color);
}
// Code annotation index on focus
// Annotation index on focus
:focus-within > & {
transition: none;
animation: none;