1
0
mirror of https://github.com/squidfunk/mkdocs-material.git synced 2024-11-27 17:00:54 +01:00

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-colon-space-after": null,
"declaration-no-important": true, "declaration-no-important": true,
"declaration-block-single-line-max-declarations": 0, "declaration-block-single-line-max-declarations": 0,
"function-calc-no-unspaced-operator": null,
"function-url-no-scheme-relative": true, "function-url-no-scheme-relative": true,
"function-url-quotes": "always", "function-url-quotes": "always",
"font-family-name-quotes": "always-where-recommended", "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 %} {% endif %}
{% endblock %} {% endblock %}
{% block styles %} {% 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 %} {% if config.theme.palette %}
{% set palette = config.theme.palette %} {% set palette = config.theme.palette %}
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.9204c3b2.min.css' | url }}"> <link rel="stylesheet" href="{{ 'assets/stylesheets/palette.9204c3b2.min.css' | url }}">
@ -213,7 +213,7 @@
</script> </script>
{% endblock %} {% endblock %}
{% block scripts %} {% 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"] %} {% for path in config["extra_javascript"] %}
<script src="{{ path | url }}"></script> <script src="{{ path | url }}"></script>
{% endfor %} {% 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 %} {% endblock %}
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script src="{{ 'overrides/assets/javascripts/bundle.6950b4a3.min.js' | url }}"></script> <script src="{{ 'overrides/assets/javascripts/bundle.7c4664dd.min.js' | url }}"></script>
{% endblock %} {% endblock %}

View File

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

View File

@ -37,6 +37,11 @@ import { getActiveElement } from "../_"
/** /**
* Watch element focus * 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 * @param el - Element
* *
* @returns Element focus observable * @returns Element focus observable
@ -45,11 +50,16 @@ export function watchElementFocus(
el: HTMLElement el: HTMLElement
): Observable<boolean> { ): Observable<boolean> {
return merge( return merge(
fromEvent<FocusEvent>(el, "focus"), fromEvent(document.body, "focusin"),
fromEvent<FocusEvent>(el, "blur") fromEvent(document.body, "focusout")
) )
.pipe( .pipe(
map(({ type }) => type === "focus"), map(() => {
const active = getActiveElement()
return typeof active !== "undefined"
? el.contains(active)
: false
}),
startWith(el === getActiveElement()) startWith(el === getActiveElement())
) )
} }

View File

@ -97,7 +97,6 @@ const keyboard$ = watchKeyboard()
const viewport$ = watchViewport() const viewport$ = watchViewport()
const tablet$ = watchMedia("(min-width: 960px)") const tablet$ = watchMedia("(min-width: 960px)")
const screen$ = watchMedia("(min-width: 1220px)") const screen$ = watchMedia("(min-width: 1220px)")
const hover$ = watchMedia("(hover)")
const print$ = watchPrint() const print$ = watchPrint()
/* Retrieve search index, if search is enabled */ /* Retrieve search index, if search is enabled */
@ -199,7 +198,7 @@ const content$ = defer(() => merge(
/* Content */ /* Content */
...getComponentElements("content") ...getComponentElements("content")
.map(el => mountContent(el, { target$, hover$, print$ })), .map(el => mountContent(el, { target$, print$ })),
/* Search highlighting */ /* Search highlighting */
...getComponentElements("content") ...getComponentElements("content")
@ -254,7 +253,6 @@ window.keyboard$ = keyboard$ /* Keyboard observable */
window.viewport$ = viewport$ /* Viewport observable */ window.viewport$ = viewport$ /* Viewport observable */
window.tablet$ = tablet$ /* Media tablet observable */ window.tablet$ = tablet$ /* Media tablet observable */
window.screen$ = screen$ /* Media screen observable */ window.screen$ = screen$ /* Media screen observable */
window.hover$ = hover$ /* Media hover observable */
window.print$ = print$ /* Media print observable */ window.print$ = print$ /* Media print observable */
window.alert$ = alert$ /* Alert subject */ window.alert$ = alert$ /* Alert subject */
window.component$ = component$ /* Component observable */ window.component$ = component$ /* Component observable */

View File

@ -57,7 +57,6 @@ export type Content =
*/ */
interface MountOptions { interface MountOptions {
target$: Observable<HTMLElement> /* Location target observable */ target$: Observable<HTMLElement> /* Location target observable */
hover$: Observable<boolean> /* Media hover observable */
print$: Observable<boolean> /* Media print observable */ print$: Observable<boolean> /* Media print observable */
} }
@ -77,13 +76,13 @@ interface MountOptions {
* @returns Content component observable * @returns Content component observable
*/ */
export function mountContent( export function mountContent(
el: HTMLElement, { target$, hover$, print$ }: MountOptions el: HTMLElement, { target$, print$ }: MountOptions
): Observable<Component<Content>> { ): Observable<Component<Content>> {
return merge( return merge(
/* Code blocks */ /* Code blocks */
...getElements("pre > code", el) ...getElements("pre > code", el)
.map(child => mountCodeBlock(child, { hover$, print$ })), .map(child => mountCodeBlock(child, { print$ })),
/* Data tables */ /* Data tables */
...getElements("table:not([class])", el) ...getElements("table:not([class])", el)

View File

@ -33,8 +33,7 @@ import {
switchMap, switchMap,
takeLast, takeLast,
takeUntil, takeUntil,
tap, tap
withLatestFrom
} from "rxjs" } from "rxjs"
import { feature } from "~/_" import { feature } from "~/_"
@ -69,7 +68,6 @@ export interface CodeBlock {
* Mount options * Mount options
*/ */
interface MountOptions { interface MountOptions {
hover$: Observable<boolean> /* Media hover observable */
print$: Observable<boolean> /* Media print 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 * @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) { if (el.nextElementSibling) {
const sibling = el.nextElementSibling as HTMLElement const sibling = el.nextElementSibling as HTMLElement
if (sibling.tagName === "OL") if (sibling.tagName === "OL")
@ -101,7 +99,7 @@ function findAnnotationList(el: HTMLElement): HTMLElement | undefined {
/* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */ /* Skip empty paragraphs - see https://bit.ly/3r4ZJ2O */
else if (sibling.tagName === "P" && !sibling.children.length) else if (sibling.tagName === "P" && !sibling.children.length)
return findAnnotationList(sibling) return findCandidateList(sibling)
} }
/* Everything else */ /* Everything else */
@ -151,20 +149,17 @@ export function watchCodeBlock(
* @returns Code block and annotation component observable * @returns Code block and annotation component observable
*/ */
export function mountCodeBlock( export function mountCodeBlock(
el: HTMLElement, { hover$, ...options }: MountOptions el: HTMLElement, options: MountOptions
): Observable<Component<CodeBlock | Annotation>> { ): Observable<Component<CodeBlock | Annotation>> {
const { matches: hover } = matchMedia("(hover)")
return defer(() => { return defer(() => {
const push$ = new Subject<CodeBlock>() const push$ = new Subject<CodeBlock>()
push$ push$.subscribe(({ scrollable }) => {
.pipe( if (scrollable && hover)
withLatestFrom(hover$) el.setAttribute("tabindex", "0")
) else
.subscribe(([{ scrollable: scroll }, hover]) => { el.removeAttribute("tabindex")
if (scroll && hover) })
el.setAttribute("tabindex", "0")
else
el.removeAttribute("tabindex")
})
/* Render button for Clipboard.js integration */ /* Render button for Clipboard.js integration */
if (ClipboardJS.isSupported()) { if (ClipboardJS.isSupported()) {
@ -177,11 +172,12 @@ export function mountCodeBlock(
} }
/* Handle code annotations */ /* Handle code annotations */
const container = const container = el.closest([
el.closest(".highlighttable") || ":not(td.code) > .highlight", /* Code blocks */
el.closest(".highlight") ".highlighttable" /* Code blocks with line numbers */
].join(", "))
if (container instanceof HTMLElement) { if (container instanceof HTMLElement) {
const list = findAnnotationList(container) const list = findCandidateList(container)
/* Mount code annotations, if enabled */ /* Mount code annotations, if enabled */
if (typeof list !== "undefined" && ( if (typeof list !== "undefined" && (

View File

@ -31,12 +31,14 @@ import {
map, map,
switchMap, switchMap,
take, take,
tap tap,
throttleTime
} from "rxjs" } from "rxjs"
import { import {
ElementOffset, ElementOffset,
getElement, getElement,
getElementSize,
watchElementContentOffset, watchElementContentOffset,
watchElementFocus, watchElementFocus,
watchElementOffset watchElementOffset
@ -76,10 +78,13 @@ export function watchAnnotation(
watchElementContentOffset(container) watchElementContentOffset(container)
])) ]))
.pipe( .pipe(
map(([{ x, y }, scroll]) => ({ map(([{ x, y }, scroll]) => {
x: x - scroll.x, const { width } = getElementSize(el)
y: y - scroll.y return ({
})) x: x - scroll.x + width / 2,
y: y - scroll.y
})
})
) )
/* Actively watch code annotation on focus */ /* 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 index = getElement(":scope > :last-child", el)
const blur$ = fromEvent(index, "mousedown", { once: true }) const blur$ = fromEvent(index, "mousedown", { once: true })
push$ push$

View File

@ -21,7 +21,12 @@
*/ */
import ClipboardJS from "clipboard" import ClipboardJS from "clipboard"
import { Observable, Subject } from "rxjs" import {
Observable,
Subject,
mapTo,
tap
} from "rxjs"
import { translation } from "~/_" import { translation } from "~/_"
import { import {
@ -92,6 +97,13 @@ export function setupClipboardJS(
}) })
.on("success", ev => subscriber.next(ev)) .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); top: calc(var(--md-tooltip-y) + 1.2ch);
left: left:
clamp( clamp(
#{px2rem(0px)}, calc(
calc(var(--md-tooltip-x) + #{px2rem(12px)}), var(--md-tooltip-0, 0) +
calc(100vw - var(--md-tooltip-width) - 2 * #{px2rem(16px)}) #{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); 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 // Code annotation tooltip when not focused
:not(:focus-within) > & { :not(:focus-within) > & {
user-select: none; user-select: none;
@ -189,8 +186,8 @@
// alignment of text following a code annotation. // alignment of text following a code annotation.
&::after { &::after {
position: absolute; position: absolute;
top: 0.1ch; top: 0.025em;
left: -0.2ch; left: -0.126em;
z-index: -1; z-index: -1;
width: max(2.2ch, 100% + 1.2ch); width: max(2.2ch, 100% + 1.2ch);
height: 2.2ch; height: 2.2ch;

View File

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