Improved rendering of code annotations + keep focus for clipboard button

This commit is contained in:
squidfunk
2021-12-04 11:33:21 +01:00
parent 122be2ec46
commit fb751c108c
18 changed files with 92 additions and 130 deletions

View File

@@ -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())
)
}

View File

@@ -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 */

View File

@@ -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)

View File

@@ -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" && (

View File

@@ -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$

View File

@@ -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$)
}
}