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

@ -57,6 +57,7 @@
"declaration-colon-space-after": null,
"declaration-no-important": true,
"declaration-block-single-line-max-declarations": 0,
"function-calc-no-unspaced-operator": null,
"function-url-no-scheme-relative": true,
"function-url-quotes": "always",
"font-family-name-quotes": "always-where-recommended",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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.62128196.min.css' | url }}">
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.2a4617e2.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.85839339.min.js' | url }}"></script>
<script src="{{ 'assets/javascripts/bundle.acd49e06.min.js' | url }}"></script>
{% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script>
{% endfor %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,5 +16,5 @@
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{ 'overrides/assets/javascripts/bundle.6950b4a3.min.js' | url }}"></script>
<script src="{{ 'overrides/assets/javascripts/bundle.7c4664dd.min.js' | url }}"></script>
{% endblock %}

View File

@ -70,13 +70,13 @@ theme:
primary: indigo
accent: indigo
toggle:
icon: material/toggle-switch-off-outline
icon: material/toggle-switch
name: Switch to dark mode
- scheme: slate
primary: red
accent: red
toggle:
icon: material/toggle-switch
icon: material/toggle-switch-off-outline
name: Switch to light mode
font:
text: Roboto

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

View File

@ -146,26 +146,23 @@
top: calc(var(--md-tooltip-y) + 1.2ch);
left:
clamp(
#{px2rem(0px)},
calc(var(--md-tooltip-x) + #{px2rem(12px)}),
calc(100vw - var(--md-tooltip-width) - 2 * #{px2rem(16px)})
calc(
var(--md-tooltip-0, 0) +
#{px2rem(16px)}
),
var(--md-tooltip-x),
calc(
100vw -
var(--md-tooltip-width) +
calc(
var(--md-tooltip-0, 0) +
#{px2rem(16px)}
) -
2 * #{px2rem(16px)}
)
);
font-family: var(--md-text-font-family);
// [mobile -]: Align with body copy
@include break-to-device(mobile) {
// Top-level code block
.md-content__inner > :is(pre, .highlight) & {
left:
clamp(
#{px2rem(16px)},
calc(var(--md-tooltip-x) + #{px2rem(12px)}),
calc(100vw - var(--md-tooltip-width) - #{px2rem(16px)})
);
}
}
// Code annotation tooltip when not focused
:not(:focus-within) > & {
user-select: none;
@ -189,8 +186,8 @@
// alignment of text following a code annotation.
&::after {
position: absolute;
top: 0.1ch;
left: -0.2ch;
top: 0.025em;
left: -0.126em;
z-index: -1;
width: max(2.2ch, 100% + 1.2ch);
height: 2.2ch;

View File

@ -108,7 +108,6 @@ declare global {
var viewport$: Observable<Viewport> /* Viewport obsevable */
var tablet$: Observable<boolean> /* Media tablet observable */
var screen$: Observable<boolean> /* Media screen observable */
var hover$: Observable<boolean> /* Media hover observable */
var print$: Observable<boolean> /* Media print observable */
var alert$: Subject<string> /* Alert subject */
var component$: Observable<Component>/* Component observable */