mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2026-07-24 06:43:50 -04:00
Improved rendering of code annotations + keep focus for clipboard button
This commit is contained in:
@@ -37,6 +37,11 @@ import { getActiveElement } from "../_"
|
||||
/**
|
||||
* Watch element focus
|
||||
*
|
||||
* Previously, this function used `focus` and `blur` events to determine whether
|
||||
* an element is focused, but this doesn't work if there are focusable elements
|
||||
* within the elements itself. A better solutions it to use `focusin/out` events
|
||||
* events which bubble up the tree and allow for more fine-grained control.
|
||||
*
|
||||
* @param el - Element
|
||||
*
|
||||
* @returns Element focus observable
|
||||
@@ -45,11 +50,16 @@ export function watchElementFocus(
|
||||
el: HTMLElement
|
||||
): Observable<boolean> {
|
||||
return merge(
|
||||
fromEvent<FocusEvent>(el, "focus"),
|
||||
fromEvent<FocusEvent>(el, "blur")
|
||||
fromEvent(document.body, "focusin"),
|
||||
fromEvent(document.body, "focusout")
|
||||
)
|
||||
.pipe(
|
||||
map(({ type }) => type === "focus"),
|
||||
map(() => {
|
||||
const active = getActiveElement()
|
||||
return typeof active !== "undefined"
|
||||
? el.contains(active)
|
||||
: false
|
||||
}),
|
||||
startWith(el === getActiveElement())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ const keyboard$ = watchKeyboard()
|
||||
const viewport$ = watchViewport()
|
||||
const tablet$ = watchMedia("(min-width: 960px)")
|
||||
const screen$ = watchMedia("(min-width: 1220px)")
|
||||
const hover$ = watchMedia("(hover)")
|
||||
const print$ = watchPrint()
|
||||
|
||||
/* Retrieve search index, if search is enabled */
|
||||
@@ -199,7 +198,7 @@ const content$ = defer(() => merge(
|
||||
|
||||
/* Content */
|
||||
...getComponentElements("content")
|
||||
.map(el => mountContent(el, { target$, hover$, print$ })),
|
||||
.map(el => mountContent(el, { target$, print$ })),
|
||||
|
||||
/* Search highlighting */
|
||||
...getComponentElements("content")
|
||||
@@ -254,7 +253,6 @@ window.keyboard$ = keyboard$ /* Keyboard observable */
|
||||
window.viewport$ = viewport$ /* Viewport observable */
|
||||
window.tablet$ = tablet$ /* Media tablet observable */
|
||||
window.screen$ = screen$ /* Media screen observable */
|
||||
window.hover$ = hover$ /* Media hover observable */
|
||||
window.print$ = print$ /* Media print observable */
|
||||
window.alert$ = alert$ /* Alert subject */
|
||||
window.component$ = component$ /* Component observable */
|
||||
|
||||
@@ -57,7 +57,6 @@ export type Content =
|
||||
*/
|
||||
interface MountOptions {
|
||||
target$: Observable<HTMLElement> /* Location target observable */
|
||||
hover$: Observable<boolean> /* Media hover observable */
|
||||
print$: Observable<boolean> /* Media print observable */
|
||||
}
|
||||
|
||||
@@ -77,13 +76,13 @@ interface MountOptions {
|
||||
* @returns Content component observable
|
||||
*/
|
||||
export function mountContent(
|
||||
el: HTMLElement, { target$, hover$, print$ }: MountOptions
|
||||
el: HTMLElement, { target$, print$ }: MountOptions
|
||||
): Observable<Component<Content>> {
|
||||
return merge(
|
||||
|
||||
/* Code blocks */
|
||||
...getElements("pre > code", el)
|
||||
.map(child => mountCodeBlock(child, { hover$, print$ })),
|
||||
.map(child => mountCodeBlock(child, { print$ })),
|
||||
|
||||
/* Data tables */
|
||||
...getElements("table:not([class])", el)
|
||||
|
||||
@@ -33,8 +33,7 @@ import {
|
||||
switchMap,
|
||||
takeLast,
|
||||
takeUntil,
|
||||
tap,
|
||||
withLatestFrom
|
||||
tap
|
||||
} from "rxjs"
|
||||
|
||||
import { feature } from "~/_"
|
||||
@@ -69,7 +68,6 @@ export interface CodeBlock {
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
hover$: Observable<boolean> /* Media hover observable */
|
||||
print$: Observable<boolean> /* Media print observable */
|
||||
}
|
||||
|
||||
@@ -87,13 +85,13 @@ let sequence = 0
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Find the code annotations belonging to a code block
|
||||
* Find candidate list element directly following a code block
|
||||
*
|
||||
* @param el - Code block element
|
||||
*
|
||||
* @returns Code annotation list or nothing
|
||||
* @returns List element or nothing
|
||||
*/
|
||||
function findAnnotationList(el: HTMLElement): HTMLElement | undefined {
|
||||
function findCandidateList(el: HTMLElement): HTMLElement | undefined {
|
||||
if (el.nextElementSibling) {
|
||||
const sibling = el.nextElementSibling as HTMLElement
|
||||
if (sibling.tagName === "OL")
|
||||
@@ -101,7 +99,7 @@ function findAnnotationList(el: HTMLElement): HTMLElement | undefined {
|
||||
|
||||
/* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
|
||||
else if (sibling.tagName === "P" && !sibling.children.length)
|
||||
return findAnnotationList(sibling)
|
||||
return findCandidateList(sibling)
|
||||
}
|
||||
|
||||
/* Everything else */
|
||||
@@ -151,20 +149,17 @@ export function watchCodeBlock(
|
||||
* @returns Code block and annotation component observable
|
||||
*/
|
||||
export function mountCodeBlock(
|
||||
el: HTMLElement, { hover$, ...options }: MountOptions
|
||||
el: HTMLElement, options: MountOptions
|
||||
): Observable<Component<CodeBlock | Annotation>> {
|
||||
const { matches: hover } = matchMedia("(hover)")
|
||||
return defer(() => {
|
||||
const push$ = new Subject<CodeBlock>()
|
||||
push$
|
||||
.pipe(
|
||||
withLatestFrom(hover$)
|
||||
)
|
||||
.subscribe(([{ scrollable: scroll }, hover]) => {
|
||||
if (scroll && hover)
|
||||
el.setAttribute("tabindex", "0")
|
||||
else
|
||||
el.removeAttribute("tabindex")
|
||||
})
|
||||
push$.subscribe(({ scrollable }) => {
|
||||
if (scrollable && hover)
|
||||
el.setAttribute("tabindex", "0")
|
||||
else
|
||||
el.removeAttribute("tabindex")
|
||||
})
|
||||
|
||||
/* Render button for Clipboard.js integration */
|
||||
if (ClipboardJS.isSupported()) {
|
||||
@@ -177,11 +172,12 @@ export function mountCodeBlock(
|
||||
}
|
||||
|
||||
/* Handle code annotations */
|
||||
const container =
|
||||
el.closest(".highlighttable") ||
|
||||
el.closest(".highlight")
|
||||
const container = el.closest([
|
||||
":not(td.code) > .highlight", /* Code blocks */
|
||||
".highlighttable" /* Code blocks with line numbers */
|
||||
].join(", "))
|
||||
if (container instanceof HTMLElement) {
|
||||
const list = findAnnotationList(container)
|
||||
const list = findCandidateList(container)
|
||||
|
||||
/* Mount code annotations, if enabled */
|
||||
if (typeof list !== "undefined" && (
|
||||
|
||||
@@ -31,12 +31,14 @@ import {
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap
|
||||
tap,
|
||||
throttleTime
|
||||
} from "rxjs"
|
||||
|
||||
import {
|
||||
ElementOffset,
|
||||
getElement,
|
||||
getElementSize,
|
||||
watchElementContentOffset,
|
||||
watchElementFocus,
|
||||
watchElementOffset
|
||||
@@ -76,10 +78,13 @@ export function watchAnnotation(
|
||||
watchElementContentOffset(container)
|
||||
]))
|
||||
.pipe(
|
||||
map(([{ x, y }, scroll]) => ({
|
||||
x: x - scroll.x,
|
||||
y: y - scroll.y
|
||||
}))
|
||||
map(([{ x, y }, scroll]) => {
|
||||
const { width } = getElementSize(el)
|
||||
return ({
|
||||
x: x - scroll.x + width / 2,
|
||||
y: y - scroll.y
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
/* Actively watch code annotation on focus */
|
||||
@@ -122,7 +127,18 @@ export function mountAnnotation(
|
||||
}
|
||||
})
|
||||
|
||||
/* Blur open annotation on click (= close) */
|
||||
/* Track relative origin of tooltip */
|
||||
push$
|
||||
.pipe(
|
||||
throttleTime(500),
|
||||
map(() => container.getBoundingClientRect()),
|
||||
map(({ x }) => x)
|
||||
)
|
||||
.subscribe(origin => {
|
||||
el.style.setProperty("--md-tooltip-0", `${-origin}px`)
|
||||
})
|
||||
|
||||
/* Close open annotation on click */
|
||||
const index = getElement(":scope > :last-child", el)
|
||||
const blur$ = fromEvent(index, "mousedown", { once: true })
|
||||
push$
|
||||
|
||||
@@ -21,7 +21,12 @@
|
||||
*/
|
||||
|
||||
import ClipboardJS from "clipboard"
|
||||
import { Observable, Subject } from "rxjs"
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
mapTo,
|
||||
tap
|
||||
} from "rxjs"
|
||||
|
||||
import { translation } from "~/_"
|
||||
import {
|
||||
@@ -92,6 +97,13 @@ export function setupClipboardJS(
|
||||
})
|
||||
.on("success", ev => subscriber.next(ev))
|
||||
})
|
||||
.subscribe(() => alert$.next(translation("clipboard.copied")))
|
||||
.pipe(
|
||||
tap(ev => {
|
||||
const trigger = ev.trigger as HTMLElement
|
||||
trigger.focus()
|
||||
}),
|
||||
mapTo(translation("clipboard.copied"))
|
||||
)
|
||||
.subscribe(alert$)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user