mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2024-11-28 01:10:58 +01:00
Refactored code annotations
This commit is contained in:
parent
f6503920b1
commit
abe475e151
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
29
material/assets/javascripts/bundle.7c769d4b.min.js
vendored
Normal file
29
material/assets/javascripts/bundle.7c769d4b.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
material/assets/javascripts/bundle.7c769d4b.min.js.map
Normal file
7
material/assets/javascripts/bundle.7c769d4b.min.js.map
Normal file
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
2
material/assets/stylesheets/main.ad626c1e.min.css
vendored
Normal file
2
material/assets/stylesheets/main.ad626c1e.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/stylesheets/main.ad626c1e.min.css.map
Normal file
1
material/assets/stylesheets/main.ad626c1e.min.css.map
Normal file
File diff suppressed because one or more lines are too long
@ -34,7 +34,7 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.6378942d.min.css' | url }}">
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/main.ad626c1e.min.css' | url }}">
|
||||
{% if config.theme.palette %}
|
||||
{% set palette = config.theme.palette %}
|
||||
<link rel="stylesheet" href="{{ 'assets/stylesheets/palette.9204c3b2.min.css' | url }}">
|
||||
@ -184,9 +184,11 @@
|
||||
"base": base_url,
|
||||
"features": features,
|
||||
"translations": {},
|
||||
"search": "assets/javascripts/workers/search.01824240.min.js" | url,
|
||||
"version": config.extra.version or None
|
||||
"search": "assets/javascripts/workers/search.01824240.min.js" | url
|
||||
} -%}
|
||||
{%- if config.extra.version -%}
|
||||
{%- set _ = app.update({ "version": config.extra.version }) -%}
|
||||
{%- endif -%}
|
||||
{%- set translations = app.translations -%}
|
||||
{%- for key in [
|
||||
"clipboard.copy",
|
||||
@ -211,7 +213,7 @@
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ 'assets/javascripts/bundle.0885dc41.min.js' | url }}"></script>
|
||||
<script src="{{ 'assets/javascripts/bundle.7c769d4b.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
@ -16,5 +16,5 @@
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ 'overrides/assets/javascripts/bundle.a8b5c64f.min.js' | url }}"></script>
|
||||
<script src="{{ 'overrides/assets/javascripts/bundle.afb943e6.min.js' | url }}"></script>
|
||||
{% endblock %}
|
||||
|
@ -33,8 +33,8 @@ export type Flag =
|
||||
| "content.code.annotate" /* Code annotations */
|
||||
| "header.autohide" /* Hide header */
|
||||
| "navigation.expand" /* Automatic expansion */
|
||||
| "navigation.instant" /* Instant loading */
|
||||
| "navigation.indexes" /* Section pages */
|
||||
| "navigation.instant" /* Instant loading */
|
||||
| "navigation.sections" /* Section navigation */
|
||||
| "navigation.tabs" /* Tabs navigation */
|
||||
| "navigation.tabs.sticky" /* Tabs navigation (sticky) */
|
||||
|
@ -53,7 +53,9 @@ export interface ElementOffset {
|
||||
*
|
||||
* @returns Element offset
|
||||
*/
|
||||
export function getElementOffset(el: HTMLElement): ElementOffset {
|
||||
export function getElementOffset(
|
||||
el: HTMLElement
|
||||
): ElementOffset {
|
||||
return {
|
||||
x: el.offsetLeft,
|
||||
y: el.offsetTop
|
||||
|
@ -43,7 +43,9 @@ import { ElementOffset } from "../_"
|
||||
*
|
||||
* @returns Element content offset
|
||||
*/
|
||||
export function getElementContentOffset(el: HTMLElement): ElementOffset {
|
||||
export function getElementContentOffset(
|
||||
el: HTMLElement
|
||||
): ElementOffset {
|
||||
return {
|
||||
x: el.scrollLeft,
|
||||
y: el.scrollTop
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
filter,
|
||||
finalize,
|
||||
map,
|
||||
merge,
|
||||
of,
|
||||
shareReplay,
|
||||
startWith,
|
||||
@ -73,7 +74,7 @@ const observer$ = defer(() => of(
|
||||
})
|
||||
))
|
||||
.pipe(
|
||||
switchMap(observer => NEVER.pipe(startWith(observer))
|
||||
switchMap(observer => merge(NEVER, of(observer))
|
||||
.pipe(
|
||||
finalize(() => observer.disconnect())
|
||||
)
|
||||
@ -92,7 +93,9 @@ const observer$ = defer(() => of(
|
||||
*
|
||||
* @returns Element size
|
||||
*/
|
||||
export function getElementSize(el: HTMLElement): ElementSize {
|
||||
export function getElementSize(
|
||||
el: HTMLElement
|
||||
): ElementSize {
|
||||
return {
|
||||
width: el.offsetWidth,
|
||||
height: el.offsetHeight
|
||||
|
@ -33,7 +33,9 @@ import { ElementSize } from "../_"
|
||||
*
|
||||
* @returns Element content size
|
||||
*/
|
||||
export function getElementContentSize(el: HTMLElement): ElementSize {
|
||||
export function getElementContentSize(
|
||||
el: HTMLElement
|
||||
): ElementSize {
|
||||
return {
|
||||
width: el.scrollWidth,
|
||||
height: el.scrollHeight
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
filter,
|
||||
finalize,
|
||||
map,
|
||||
merge,
|
||||
of,
|
||||
shareReplay,
|
||||
startWith,
|
||||
@ -68,7 +69,7 @@ const observer$ = defer(() => of(
|
||||
})
|
||||
))
|
||||
.pipe(
|
||||
switchMap(observer => NEVER.pipe(startWith(observer))
|
||||
switchMap(observer => merge(NEVER, of(observer))
|
||||
.pipe(
|
||||
finalize(() => observer.disconnect())
|
||||
)
|
||||
|
@ -22,10 +22,14 @@
|
||||
|
||||
import { Observable, merge } from "rxjs"
|
||||
|
||||
import { Viewport, getElements } from "~/browser"
|
||||
import { getElements } from "~/browser"
|
||||
|
||||
import { Component } from "../../_"
|
||||
import { CodeBlock, mountCodeBlock } from "../code"
|
||||
import {
|
||||
Annotation,
|
||||
CodeBlock,
|
||||
mountCodeBlock
|
||||
} from "../code"
|
||||
import { Details, mountDetails } from "../details"
|
||||
import { DataTable, mountDataTable } from "../table"
|
||||
import { ContentTabs, mountContentTabs } from "../tabs"
|
||||
@ -38,6 +42,7 @@ import { ContentTabs, mountContentTabs } from "../tabs"
|
||||
* Content
|
||||
*/
|
||||
export type Content =
|
||||
| Annotation
|
||||
| ContentTabs
|
||||
| CodeBlock
|
||||
| DataTable
|
||||
@ -52,7 +57,6 @@ export type Content =
|
||||
*/
|
||||
interface MountOptions {
|
||||
target$: Observable<HTMLElement> /* Location target observable */
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
hover$: Observable<boolean> /* Media hover observable */
|
||||
print$: Observable<boolean> /* Media print observable */
|
||||
}
|
||||
@ -73,13 +77,13 @@ interface MountOptions {
|
||||
* @returns Content component observable
|
||||
*/
|
||||
export function mountContent(
|
||||
el: HTMLElement, { target$, viewport$, hover$, print$ }: MountOptions
|
||||
el: HTMLElement, { target$, hover$, print$ }: MountOptions
|
||||
): Observable<Component<Content>> {
|
||||
return merge(
|
||||
|
||||
/* Code blocks */
|
||||
...getElements("pre > code", el)
|
||||
.map(child => mountCodeBlock(child, { viewport$, hover$, print$ })),
|
||||
.map(child => mountCodeBlock(child, { hover$, print$ })),
|
||||
|
||||
/* Data tables */
|
||||
...getElements("table:not([class])", el)
|
||||
|
219
src/assets/javascripts/components/content/code/_/index.ts
Normal file
219
src/assets/javascripts/components/content/code/_/index.ts
Normal file
@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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,
|
||||
distinctUntilKeyChanged,
|
||||
finalize,
|
||||
map,
|
||||
mergeWith,
|
||||
switchMap,
|
||||
takeLast,
|
||||
takeUntil,
|
||||
tap,
|
||||
withLatestFrom
|
||||
} 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 {
|
||||
hover$: Observable<boolean> /* Media hover observable */
|
||||
print$: Observable<boolean> /* Media print observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Data
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Global sequence number for Clipboard.js integration
|
||||
*/
|
||||
let sequence = 0
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Find the code annotations belonging to a code block
|
||||
*
|
||||
* @param el - Code block element
|
||||
*
|
||||
* @returns Code annotation list or nothing
|
||||
*/
|
||||
function findAnnotationList(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 findAnnotationList(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, { hover$, ...options }: MountOptions
|
||||
): Observable<Component<CodeBlock | Annotation>> {
|
||||
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")
|
||||
})
|
||||
|
||||
/* 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(".highlighttable") ||
|
||||
el.closest(".highlight")
|
||||
if (container instanceof HTMLElement) {
|
||||
const list = findAnnotationList(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))),
|
||||
switchMap(({ width, height }) => width && height
|
||||
? annotations$
|
||||
: EMPTY
|
||||
)
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* Create and return component */
|
||||
return watchCodeBlock(el)
|
||||
.pipe(
|
||||
tap(state => push$.next(state)),
|
||||
finalize(() => push$.complete()),
|
||||
map(state => ({ ref: el, ...state }))
|
||||
)
|
||||
})
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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 {
|
||||
EMPTY,
|
||||
Observable,
|
||||
Subject,
|
||||
combineLatest,
|
||||
defer,
|
||||
finalize,
|
||||
fromEvent,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
tap
|
||||
} from "rxjs"
|
||||
|
||||
import {
|
||||
ElementOffset,
|
||||
watchElementContentOffset,
|
||||
watchElementFocus,
|
||||
watchElementOffset
|
||||
} from "~/browser"
|
||||
|
||||
import { Component } from "../../../../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Code annotation
|
||||
*/
|
||||
export interface Annotation {
|
||||
active: boolean /* Code annotation is visible */
|
||||
offset: ElementOffset /* Code annotation offset */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch code annotation
|
||||
*
|
||||
* @param el - Code annotation element
|
||||
* @param container - Containing code block element
|
||||
*
|
||||
* @returns Code annotation observable
|
||||
*/
|
||||
export function watchAnnotation(
|
||||
el: HTMLElement, container: HTMLElement
|
||||
): Observable<Annotation> {
|
||||
const offset$ = defer(() => combineLatest([
|
||||
watchElementOffset(el),
|
||||
watchElementContentOffset(container)
|
||||
]))
|
||||
.pipe(
|
||||
map(([{ x, y }, scroll]) => ({
|
||||
x: x - scroll.x,
|
||||
y: y - scroll.y
|
||||
}))
|
||||
)
|
||||
|
||||
/* Actively watch code annotation on focus */
|
||||
return watchElementFocus(el)
|
||||
.pipe(
|
||||
switchMap(active => offset$
|
||||
.pipe(
|
||||
map(offset => ({ active, offset })),
|
||||
take(+!active || Infinity)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount code annotation
|
||||
*
|
||||
* @param el - Code annotation element
|
||||
* @param container - Containing code block element
|
||||
*
|
||||
* @returns Code annotation component observable
|
||||
*/
|
||||
export function mountAnnotation(
|
||||
el: HTMLElement, container: HTMLElement
|
||||
): Observable<Component<Annotation>> {
|
||||
return defer(() => {
|
||||
const push$ = new Subject<Annotation>()
|
||||
push$.subscribe({
|
||||
|
||||
/* Handle emission */
|
||||
next({ offset }) {
|
||||
el.style.setProperty("--md-tooltip-x", `${offset.x}px`)
|
||||
el.style.setProperty("--md-tooltip-y", `${offset.y}px`)
|
||||
},
|
||||
|
||||
/* Handle complete */
|
||||
complete() {
|
||||
el.style.removeProperty("--md-tooltip-x")
|
||||
el.style.removeProperty("--md-tooltip-y")
|
||||
}
|
||||
})
|
||||
|
||||
/* Blur open annotation on click (= close) */
|
||||
const index = el.lastElementChild!
|
||||
const blur$ = fromEvent(index, "mousedown", { once: true })
|
||||
push$
|
||||
.pipe(
|
||||
switchMap(({ active }) => active ? blur$ : EMPTY),
|
||||
tap(ev => ev.preventDefault())
|
||||
)
|
||||
.subscribe(() => el.blur())
|
||||
|
||||
/* Create and return component */
|
||||
return watchAnnotation(el, container)
|
||||
.pipe(
|
||||
tap(state => push$.next(state)),
|
||||
finalize(() => push$.complete()),
|
||||
map(state => ({ ref: el, ...state }))
|
||||
)
|
||||
})
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from "./_"
|
||||
export * from "./list"
|
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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 {
|
||||
Observable,
|
||||
Subject,
|
||||
defer,
|
||||
finalize,
|
||||
merge,
|
||||
share,
|
||||
startWith,
|
||||
takeLast,
|
||||
takeUntil
|
||||
} from "rxjs"
|
||||
|
||||
import {
|
||||
getElement,
|
||||
getElements
|
||||
} from "~/browser"
|
||||
import { renderAnnotation } from "~/templates"
|
||||
|
||||
import { Component } from "../../../../_"
|
||||
import {
|
||||
Annotation,
|
||||
mountAnnotation
|
||||
} from "../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
print$: Observable<boolean> /* Media print observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Find all code annotation markers in the given code block
|
||||
*
|
||||
* @param container - Containing code block element
|
||||
*
|
||||
* @returns Code annotation markers
|
||||
*/
|
||||
function findAnnotationMarkers(container: HTMLElement): Text[] {
|
||||
const markers: Text[] = []
|
||||
for (const comment of getElements(".c, .c1, .cm", container)) {
|
||||
let match: RegExpExecArray | null
|
||||
let text = comment.firstChild as Text
|
||||
|
||||
/* Split text at marker and add to list */
|
||||
while ((match = /\((\d+)\)/.exec(text.textContent!))) {
|
||||
const marker = text.splitText(match.index)
|
||||
text = marker.splitText(match[0].length)
|
||||
markers.push(marker)
|
||||
}
|
||||
}
|
||||
return markers
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the child nodes of two elements
|
||||
*
|
||||
* @param source - Source element
|
||||
* @param target - Target element
|
||||
*/
|
||||
function swap(source: HTMLElement, target: HTMLElement): void {
|
||||
target.append(...Array.from(source.childNodes))
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Mount code annotation list
|
||||
*
|
||||
* @param el - Code annotation list element
|
||||
* @param container - Containing code block element
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Code annotation list component observable
|
||||
*/
|
||||
export function mountAnnotationList(
|
||||
el: HTMLElement, container: HTMLElement, options: MountOptions
|
||||
): Observable<Component<Annotation>> {
|
||||
const { print$ } = options
|
||||
|
||||
/* Find and replace all markers with empty annotations */
|
||||
const annotations = new Map<number, HTMLElement>()
|
||||
for (const marker of findAnnotationMarkers(container)) {
|
||||
const [, id] = marker.textContent!.match(/\((\d+)\)/)!
|
||||
annotations.set(+id, renderAnnotation(+id))
|
||||
marker.replaceWith(annotations.get(+id)!)
|
||||
}
|
||||
|
||||
/* Create and return component */
|
||||
return defer(() => {
|
||||
const push$ = new Subject<Annotation>()
|
||||
|
||||
/* Handle print mode - see https://bit.ly/3rgPdpt */
|
||||
print$
|
||||
.pipe(
|
||||
startWith(false),
|
||||
takeUntil(push$.pipe(takeLast(1)))
|
||||
)
|
||||
.subscribe(active => {
|
||||
el.hidden = !active
|
||||
|
||||
/* Move annotation contents back into list */
|
||||
for (const [id, annotation] of annotations) {
|
||||
const tooltip = getElement(".md-typeset", annotation)
|
||||
const child = getElement(`li:nth-child(${id})`, el)
|
||||
if (!active)
|
||||
swap(child, tooltip)
|
||||
else
|
||||
swap(tooltip, child)
|
||||
}
|
||||
})
|
||||
|
||||
/* Create and return component */
|
||||
return merge(
|
||||
...[...annotations].map(([, annotation]) => (
|
||||
mountAnnotation(annotation, container)
|
||||
))
|
||||
)
|
||||
.pipe(
|
||||
finalize(() => push$.complete()),
|
||||
share()
|
||||
)
|
||||
})
|
||||
}
|
@ -20,295 +20,5 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import ClipboardJS from "clipboard"
|
||||
import {
|
||||
NEVER,
|
||||
Observable,
|
||||
Subject,
|
||||
combineLatest,
|
||||
defer,
|
||||
distinctUntilKeyChanged,
|
||||
finalize,
|
||||
fromEvent,
|
||||
map,
|
||||
mapTo,
|
||||
merge,
|
||||
mergeMap,
|
||||
mergeWith,
|
||||
of,
|
||||
share,
|
||||
tap,
|
||||
withLatestFrom
|
||||
} from "rxjs"
|
||||
|
||||
import { feature } from "~/_"
|
||||
import {
|
||||
resetFocusable,
|
||||
setFocusable
|
||||
} from "~/actions"
|
||||
import {
|
||||
Viewport,
|
||||
getElement,
|
||||
getElementContentSize,
|
||||
getElementSize,
|
||||
getElements,
|
||||
getOptionalElement,
|
||||
watchElementContentOffset,
|
||||
watchElementOffset
|
||||
} from "~/browser"
|
||||
import {
|
||||
renderAnnotation,
|
||||
renderClipboardButton
|
||||
} from "~/templates"
|
||||
|
||||
import { Component } from "../../_"
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Code block
|
||||
*/
|
||||
export interface CodeBlock {
|
||||
scrollable: boolean /* Code block overflows */
|
||||
annotations: HTMLElement[] /* Code block annotations */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper types
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Watch options
|
||||
*/
|
||||
interface WatchOptions {
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
print$: Observable<boolean> /* Media print observable */
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount options
|
||||
*/
|
||||
interface MountOptions {
|
||||
viewport$: Observable<Viewport> /* Viewport observable */
|
||||
hover$: Observable<boolean> /* Media hover observable */
|
||||
print$: Observable<boolean> /* Media print observable */
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Data
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Global sequence number for Clipboard.js integration
|
||||
*/
|
||||
let sequence = 0
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* Helper functions
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Find the code annotations belonging to a code block
|
||||
*
|
||||
* @param el - Code block element
|
||||
*
|
||||
* @returns Code annotations or nothing
|
||||
*/
|
||||
function findAnnotationsList(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 findAnnotationsList(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
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Code block observable
|
||||
*/
|
||||
export function watchCodeBlock(
|
||||
el: HTMLElement, { viewport$, print$ }: WatchOptions
|
||||
): Observable<CodeBlock> {
|
||||
|
||||
/* Trigger re-rendering when code blocks are revealed */
|
||||
const reveal$ = defer(() => {
|
||||
const container = el.closest("[data-tabs]")
|
||||
if (container instanceof HTMLElement) {
|
||||
return merge(
|
||||
...getElements(":scope > input", container)
|
||||
.map(input => fromEvent(input, "change"))
|
||||
)
|
||||
}
|
||||
return NEVER
|
||||
})
|
||||
.pipe(
|
||||
mapTo(undefined),
|
||||
share()
|
||||
)
|
||||
|
||||
/* Check for code annotations */
|
||||
const annotations: HTMLElement[] = []
|
||||
const container =
|
||||
el.closest(".highlighttable") ||
|
||||
el.closest(".highlight")
|
||||
if (container instanceof HTMLElement) {
|
||||
const list = findAnnotationsList(container)
|
||||
if (typeof list !== "undefined" && (
|
||||
container.classList.contains("annotate") ||
|
||||
feature("content.code.annotate")
|
||||
)) {
|
||||
list.remove()
|
||||
|
||||
/* Replace comments with annotations */
|
||||
const items = getElements(":scope > li", list)
|
||||
for (const comment of getElements(".c, .c1, .cm", el)) {
|
||||
|
||||
/* Split comment at annotations */
|
||||
let match: RegExpExecArray | null
|
||||
let text = comment.firstChild as Text
|
||||
do {
|
||||
match = /\((\d+)\)/.exec(text.textContent!)
|
||||
if (match && match.index) {
|
||||
const index = text.splitText(match.index)
|
||||
text = index.splitText(match[0].length)
|
||||
|
||||
/* Render and insert code annotation */
|
||||
const [, j = -1] = match
|
||||
const content = items[+j - 1]
|
||||
if (typeof content !== "undefined") {
|
||||
const annotation = renderAnnotation(+j, content.childNodes)
|
||||
index.replaceWith(annotation)
|
||||
annotations.push(annotation)
|
||||
}
|
||||
}
|
||||
} while (match)
|
||||
}
|
||||
|
||||
/* Move elements back on print */ // TODO: fix instant loading memleak
|
||||
print$.subscribe(active => {
|
||||
if (active) {
|
||||
container.insertAdjacentElement("afterend", list)
|
||||
for (const annotation of annotations) {
|
||||
const id = parseInt(annotation.getAttribute("data-index")!, 10)
|
||||
const typeset = getOptionalElement(":scope .md-typeset", annotation)!
|
||||
items[id - 1].append(...Array.from(typeset.childNodes))
|
||||
}
|
||||
} else {
|
||||
list.remove()
|
||||
for (const annotation of annotations) {
|
||||
const id = parseInt(annotation.getAttribute("data-index")!, 10)
|
||||
const nodes = items[id - 1].childNodes
|
||||
getElement(":scope .md-typeset", annotation)
|
||||
.append(...Array.from(nodes))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const change$ = viewport$
|
||||
.pipe(
|
||||
distinctUntilKeyChanged("size"),
|
||||
mergeWith(reveal$),
|
||||
mapTo(undefined)
|
||||
)
|
||||
|
||||
/* Compute code annotation position */ // TODO: fix instant loading memleak
|
||||
of(...annotations)
|
||||
.pipe(
|
||||
mergeMap(annotation => combineLatest([
|
||||
of(annotation),
|
||||
watchElementOffset(annotation),
|
||||
watchElementContentOffset(el),
|
||||
change$
|
||||
]))
|
||||
)
|
||||
// TODO: return to mountCodeBlock and render and complete there
|
||||
.subscribe(([annotation, { x, y }, scroll]) => {
|
||||
annotation.style.setProperty(
|
||||
"--md-tooltip-x", `${x - scroll.x}px`
|
||||
)
|
||||
annotation.style.setProperty(
|
||||
"--md-tooltip-y", `${y - scroll.y}px`
|
||||
)
|
||||
})
|
||||
|
||||
/* Compute overflow state on resize and content tab change */
|
||||
return change$
|
||||
.pipe(
|
||||
map(() => {
|
||||
const visible = getElementSize(el)
|
||||
const content = getElementContentSize(el)
|
||||
return {
|
||||
scrollable: content.width > visible.width,
|
||||
annotations
|
||||
}
|
||||
}),
|
||||
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.
|
||||
*
|
||||
* @param el - Code block element
|
||||
* @param options - Options
|
||||
*
|
||||
* @returns Code block component observable
|
||||
*/
|
||||
export function mountCodeBlock(
|
||||
el: HTMLElement, { hover$, ...options }: MountOptions
|
||||
): Observable<Component<CodeBlock>> {
|
||||
const internal$ = new Subject<CodeBlock>()
|
||||
internal$
|
||||
.pipe(
|
||||
withLatestFrom(hover$)
|
||||
)
|
||||
.subscribe(([{ scrollable: scroll }, hover]) => {
|
||||
if (scroll && hover)
|
||||
setFocusable(el)
|
||||
else
|
||||
resetFocusable(el)
|
||||
})
|
||||
|
||||
/* Render button for Clipboard.js integration */
|
||||
if (ClipboardJS.isSupported()) {
|
||||
const parent = el.closest("pre")!
|
||||
parent.id = `__code_${++sequence}`
|
||||
parent.insertBefore(
|
||||
renderClipboardButton(parent.id),
|
||||
el
|
||||
)
|
||||
}
|
||||
|
||||
/* Create and return component */
|
||||
return watchCodeBlock(el, options)
|
||||
.pipe(
|
||||
tap(state => internal$.next(state)),
|
||||
finalize(() => internal$.complete()),
|
||||
map(state => ({ ref: el, ...state }))
|
||||
)
|
||||
}
|
||||
export * from "./_"
|
||||
export * from "./annotation"
|
||||
|
@ -92,7 +92,7 @@ interface MountOptions {
|
||||
export function watchHeaderTitle(
|
||||
el: HTMLHeadingElement, { viewport$, header$ }: WatchOptions
|
||||
): Observable<HeaderTitle> {
|
||||
return watchViewportAt(el, { header$, viewport$ })
|
||||
return watchViewportAt(el, { viewport$, header$ })
|
||||
.pipe(
|
||||
map(({ offset: { y } }) => {
|
||||
const { height } = getElementSize(el)
|
||||
|
@ -27,22 +27,17 @@ import { h } from "~/utilities"
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Render a a code annotation
|
||||
* Render an empty code annotation
|
||||
*
|
||||
* @param id - Unique identifier
|
||||
* @param content - Annotation content
|
||||
* @param id - Annotation identifier
|
||||
*
|
||||
* @returns Element
|
||||
*/
|
||||
export function renderAnnotation(
|
||||
id: number, content: NodeListOf<ChildNode>
|
||||
): HTMLElement {
|
||||
export function renderAnnotation(id: number): HTMLElement {
|
||||
return (
|
||||
<aside class="md-annotation" data-index={id} tabIndex={0}>
|
||||
<aside class="md-annotation" tabIndex={0}>
|
||||
<div class="md-annotation__inner md-tooltip">
|
||||
<div class="md-tooltip__inner md-typeset">
|
||||
{...Array.from(content)}
|
||||
</div>
|
||||
<div class="md-tooltip__inner md-typeset"></div>
|
||||
</div>
|
||||
<span class="md-annotation__index">{id}</span>
|
||||
</aside>
|
||||
|
@ -183,10 +183,10 @@
|
||||
transition: z-index 250ms;
|
||||
user-select: none;
|
||||
|
||||
// Code annotation index bubble – the bubble 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.
|
||||
// Code 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.
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0.1ch;
|
||||
@ -210,12 +210,12 @@
|
||||
animation: none;
|
||||
}
|
||||
|
||||
// Code annotation index bubble on focus/hover
|
||||
// Code annotation marker on focus/hover
|
||||
:is(:focus-within, :hover) > & {
|
||||
background-color: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
// Code annotation index bubble on focus
|
||||
// Code annotation marker on focus
|
||||
:focus-within > & {
|
||||
transition:
|
||||
color 250ms,
|
||||
|
@ -329,10 +329,14 @@
|
||||
"base": base_url,
|
||||
"features": features,
|
||||
"translations": {},
|
||||
"search": "assets/javascripts/workers/search.js" | url,
|
||||
"version": config.extra.version or None
|
||||
"search": "assets/javascripts/workers/search.js" | url
|
||||
} -%}
|
||||
|
||||
<!-- Versioning -->
|
||||
{%- if config.extra.version -%}
|
||||
{%- set _ = app.update({ "version": config.extra.version }) -%}
|
||||
{%- endif -%}
|
||||
|
||||
<!-- Translations -->
|
||||
{%- set translations = app.translations -%}
|
||||
{%- for key in [
|
||||
|
Loading…
Reference in New Issue
Block a user