mirror of
https://github.com/squidfunk/mkdocs-material.git
synced 2025-02-17 18:49:21 +01:00
Refactored instant loading setup
This commit is contained in:
parent
4d370fe903
commit
edc9d6fc61
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/javascripts/bundle.edc2ff56.min.js
vendored
Normal file
2
material/assets/javascripts/bundle.edc2ff56.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
material/assets/javascripts/bundle.edc2ff56.min.js.map
Normal file
1
material/assets/javascripts/bundle.edc2ff56.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"assets/javascripts/bundle.js": "assets/javascripts/bundle.630a9b34.min.js",
|
"assets/javascripts/bundle.js": "assets/javascripts/bundle.edc2ff56.min.js",
|
||||||
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.630a9b34.min.js.map",
|
"assets/javascripts/bundle.js.map": "assets/javascripts/bundle.edc2ff56.min.js.map",
|
||||||
"assets/javascripts/vendor.js": "assets/javascripts/vendor.c1fcc1cc.min.js",
|
"assets/javascripts/vendor.js": "assets/javascripts/vendor.c1fcc1cc.min.js",
|
||||||
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.c1fcc1cc.min.js.map",
|
"assets/javascripts/vendor.js.map": "assets/javascripts/vendor.c1fcc1cc.min.js.map",
|
||||||
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.3bc815f0.min.js",
|
"assets/javascripts/worker/search.js": "assets/javascripts/worker/search.3bc815f0.min.js",
|
||||||
|
@ -175,7 +175,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ 'assets/javascripts/vendor.c1fcc1cc.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/vendor.c1fcc1cc.min.js' | url }}"></script>
|
||||||
<script src="{{ 'assets/javascripts/bundle.630a9b34.min.js' | url }}"></script>
|
<script src="{{ 'assets/javascripts/bundle.edc2ff56.min.js' | url }}"></script>
|
||||||
{%- set translations = {} -%}
|
{%- set translations = {} -%}
|
||||||
{%- for key in [
|
{%- for key in [
|
||||||
"clipboard.copy",
|
"clipboard.copy",
|
||||||
|
@ -49,6 +49,7 @@ export function getLocationHash(): string {
|
|||||||
export function setLocationHash(hash: string): void {
|
export function setLocationHash(hash: string): void {
|
||||||
const el = document.createElement("a")
|
const el = document.createElement("a")
|
||||||
el.href = hash
|
el.href = hash
|
||||||
|
el.addEventListener("click", ev => ev.stopPropagation())
|
||||||
el.click()
|
el.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO: remove this after we finished refactoring
|
// DISCLAIMER: this file is still WIP. There're some refactoring opportunities
|
||||||
|
// which must be tackled after we gathered some feedback on v5.
|
||||||
// tslint:disable
|
// tslint:disable
|
||||||
|
|
||||||
import "../stylesheets/main.scss"
|
import "../stylesheets/main.scss"
|
||||||
@ -32,8 +33,6 @@ import {
|
|||||||
combineLatest,
|
combineLatest,
|
||||||
animationFrameScheduler,
|
animationFrameScheduler,
|
||||||
fromEvent,
|
fromEvent,
|
||||||
of,
|
|
||||||
NEVER,
|
|
||||||
from
|
from
|
||||||
} from "rxjs"
|
} from "rxjs"
|
||||||
import { ajax } from "rxjs/ajax"
|
import { ajax } from "rxjs/ajax"
|
||||||
@ -46,9 +45,7 @@ import {
|
|||||||
observeOn,
|
observeOn,
|
||||||
take,
|
take,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
share,
|
pluck
|
||||||
pluck,
|
|
||||||
skip
|
|
||||||
} from "rxjs/operators"
|
} from "rxjs/operators"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -61,7 +58,6 @@ import {
|
|||||||
watchLocationHash,
|
watchLocationHash,
|
||||||
watchViewport,
|
watchViewport,
|
||||||
isLocalLocation,
|
isLocalLocation,
|
||||||
isAnchorLocation,
|
|
||||||
setLocationHash,
|
setLocationHash,
|
||||||
watchLocationBase
|
watchLocationBase
|
||||||
} from "browser"
|
} from "browser"
|
||||||
@ -255,7 +251,6 @@ export function initialize(config: unknown) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
const worker = setupSearchWorker(config.search.worker, {
|
const worker = setupSearchWorker(config.search.worker, {
|
||||||
base$, index$
|
base$, index$
|
||||||
})
|
})
|
||||||
@ -299,10 +294,9 @@ export function initialize(config: unknown) {
|
|||||||
tap(() => setToggle("search", false)),
|
tap(() => setToggle("search", false)),
|
||||||
delay(125), // ensure that it runs after the body scroll reset...
|
delay(125), // ensure that it runs after the body scroll reset...
|
||||||
)
|
)
|
||||||
.subscribe(hash => setLocationHash(`#${hash}`)) // TODO: must be unified
|
.subscribe(hash => setLocationHash(`#${hash}`))
|
||||||
|
|
||||||
// Scroll lock // document -> document$ => { body } !?
|
// TODO: scroll restoration must be centralized
|
||||||
// put into search...
|
|
||||||
combineLatest([
|
combineLatest([
|
||||||
watchToggle("search"),
|
watchToggle("search"),
|
||||||
tablet$,
|
tablet$,
|
||||||
@ -313,7 +307,7 @@ export function initialize(config: unknown) {
|
|||||||
const active = toggle && !tablet
|
const active = toggle && !tablet
|
||||||
return document$
|
return document$
|
||||||
.pipe(
|
.pipe(
|
||||||
delay(active ? 400 : 100), // TOOD: directly combine this with the hash!
|
delay(active ? 400 : 100),
|
||||||
observeOn(animationFrameScheduler),
|
observeOn(animationFrameScheduler),
|
||||||
tap(({ body }) => active
|
tap(({ body }) => active
|
||||||
? setScrollLock(body, y)
|
? setScrollLock(body, y)
|
||||||
@ -326,68 +320,40 @@ export function initialize(config: unknown) {
|
|||||||
|
|
||||||
/* ----------------------------------------------------------------------- */
|
/* ----------------------------------------------------------------------- */
|
||||||
|
|
||||||
/* Intercept internal link clicks */
|
/* Always close drawer on click */
|
||||||
const link$ = fromEvent<MouseEvent>(document.body, "click")
|
fromEvent<MouseEvent>(document.body, "click")
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(ev => !(ev.metaKey || ev.ctrlKey)),
|
filter(ev => !(ev.metaKey || ev.ctrlKey)),
|
||||||
switchMap(ev => {
|
filter(ev => {
|
||||||
if (ev.target instanceof HTMLElement) {
|
if (ev.target instanceof HTMLElement) {
|
||||||
const el = ev.target.closest("a") // TODO: abstract as link click?
|
const el = ev.target.closest("a") // TODO: abstract as link click?
|
||||||
if (el && isLocalLocation(el)) {
|
if (el && isLocalLocation(el)) {
|
||||||
if (!isAnchorLocation(el) && config.features.includes("instant"))
|
return true
|
||||||
ev.preventDefault()
|
|
||||||
return of(el)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NEVER
|
return false
|
||||||
}),
|
})
|
||||||
share()
|
|
||||||
)
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
setToggle("drawer", false)
|
||||||
|
})
|
||||||
|
|
||||||
/* Always close drawer on click */
|
/* Enable instant loading, if not on file:// protocol */
|
||||||
link$.subscribe(() => {
|
if (config.features.includes("instant") && location.protocol !== "file:")
|
||||||
setToggle("drawer", false)
|
setupInstantLoading({ document$, location$, viewport$ })
|
||||||
})
|
|
||||||
|
|
||||||
/* Hack: ensure that page loads restore scroll offset */
|
|
||||||
fromEvent(window, "beforeunload")
|
|
||||||
.subscribe(() => {
|
|
||||||
history.scrollRestoration = "auto"
|
|
||||||
})
|
|
||||||
|
|
||||||
// instant loading
|
|
||||||
if (config.features.includes("instant")) {
|
|
||||||
|
|
||||||
/* Disable automatic scroll restoration, as it doesn't work nicely */
|
|
||||||
if ("scrollRestoration" in history)
|
|
||||||
history.scrollRestoration = "manual"
|
|
||||||
|
|
||||||
/* Resolve relative links for stability */
|
|
||||||
for (const selector of [
|
|
||||||
`link[rel="shortcut icon"]`,
|
|
||||||
// `link[rel="stylesheet"]` // reduce style computations
|
|
||||||
])
|
|
||||||
for (const el of getElements<HTMLLinkElement>(selector))
|
|
||||||
el.href = el.href
|
|
||||||
|
|
||||||
setupInstantLoading({
|
|
||||||
document$, link$, location$, viewport$
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------- */
|
/* ----------------------------------------------------------------------- */
|
||||||
|
|
||||||
// if we use a single tab outside of search, unhide all permalinks.
|
/* Unhide permalinks on first tab */
|
||||||
// TODO: experimental. necessary!?
|
|
||||||
keyboard$
|
keyboard$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(key => key.mode === "global" && ["Tab"].includes(key.type)),
|
filter(key => key.mode === "global" && key.type === "Tab"),
|
||||||
take(1)
|
take(1)
|
||||||
)
|
)
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
for (const link of getElements(".headerlink"))
|
for (const link of getElements(".headerlink"))
|
||||||
link.style.visibility = "visible"
|
link.style.visibility = "visible"
|
||||||
})
|
})
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------- */
|
/* ----------------------------------------------------------------------- */
|
||||||
|
|
||||||
@ -395,6 +361,7 @@ export function initialize(config: unknown) {
|
|||||||
|
|
||||||
/* Browser observables */
|
/* Browser observables */
|
||||||
document$,
|
document$,
|
||||||
|
location$,
|
||||||
viewport$,
|
viewport$,
|
||||||
|
|
||||||
/* Component observables */
|
/* Component observables */
|
||||||
@ -406,7 +373,7 @@ export function initialize(config: unknown) {
|
|||||||
tabs$,
|
tabs$,
|
||||||
toc$,
|
toc$,
|
||||||
|
|
||||||
/* Integation observables */
|
/* Integration observables */
|
||||||
clipboard$,
|
clipboard$,
|
||||||
keyboard$,
|
keyboard$,
|
||||||
dialog$
|
dialog$
|
||||||
|
@ -25,7 +25,7 @@ import { NEVER, Observable, Subject, fromEventPattern } from "rxjs"
|
|||||||
import { mapTo, share, tap } from "rxjs/operators"
|
import { mapTo, share, tap } from "rxjs/operators"
|
||||||
|
|
||||||
import { getElements } from "browser"
|
import { getElements } from "browser"
|
||||||
import { renderClipboard } from "templates"
|
import { renderClipboardButton } from "templates"
|
||||||
import { translate } from "utilities"
|
import { translate } from "utilities"
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
@ -66,7 +66,7 @@ export function setupClipboard(
|
|||||||
blocks.forEach((block, index) => {
|
blocks.forEach((block, index) => {
|
||||||
const parent = block.parentElement!
|
const parent = block.parentElement!
|
||||||
parent.id = `__code_${index}`
|
parent.id = `__code_${index}`
|
||||||
parent.insertBefore(renderClipboard(parent.id), block)
|
parent.insertBefore(renderClipboardButton(parent.id), block)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
* IN THE SOFTWARE.
|
* IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NEVER, Observable, Subject, fromEvent, merge } from "rxjs"
|
import { NEVER, Observable, Subject, fromEvent, merge, of } from "rxjs"
|
||||||
import { ajax } from "rxjs//ajax"
|
import { ajax } from "rxjs//ajax"
|
||||||
import {
|
import {
|
||||||
bufferCount,
|
bufferCount,
|
||||||
@ -43,9 +43,11 @@ import {
|
|||||||
ViewportOffset,
|
ViewportOffset,
|
||||||
getElement,
|
getElement,
|
||||||
isAnchorLocation,
|
isAnchorLocation,
|
||||||
|
isLocalLocation,
|
||||||
replaceElement,
|
replaceElement,
|
||||||
setLocation,
|
setLocation,
|
||||||
setLocationHash,
|
setLocationHash,
|
||||||
|
setToggle,
|
||||||
setViewportOffset
|
setViewportOffset
|
||||||
} from "browser"
|
} from "browser"
|
||||||
|
|
||||||
@ -68,9 +70,8 @@ interface State {
|
|||||||
*/
|
*/
|
||||||
interface SetupOptions {
|
interface SetupOptions {
|
||||||
document$: Subject<Document> /* Document subject */
|
document$: Subject<Document> /* Document subject */
|
||||||
viewport$: Observable<Viewport> /* Viewport observable */
|
|
||||||
link$: Observable<HTMLAnchorElement> /* Internal link observable */
|
|
||||||
location$: Subject<URL> /* Location subject */
|
location$: Subject<URL> /* Location subject */
|
||||||
|
viewport$: Observable<Viewport> /* Viewport observable */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------------------------------------------------------
|
/* ----------------------------------------------------------------------------
|
||||||
@ -97,19 +98,51 @@ interface SetupOptions {
|
|||||||
* location change is dispatched regularly.
|
* location change is dispatched regularly.
|
||||||
*
|
*
|
||||||
* @param options - Options
|
* @param options - Options
|
||||||
*
|
|
||||||
* @return TODO: return type?
|
|
||||||
*/
|
*/
|
||||||
export function setupInstantLoading(
|
export function setupInstantLoading(
|
||||||
{ document$, viewport$, link$, location$ }: SetupOptions
|
{ document$, viewport$, location$ }: SetupOptions
|
||||||
) { // TODO: add return type
|
): void {
|
||||||
const state$ = link$
|
|
||||||
|
/* Disable automatic scroll restoration */
|
||||||
|
if ("scrollRestoration" in history)
|
||||||
|
history.scrollRestoration = "manual"
|
||||||
|
|
||||||
|
/* Hack: ensure that reloads restore viewport offset */
|
||||||
|
fromEvent(window, "beforeunload")
|
||||||
|
.subscribe(() => {
|
||||||
|
history.scrollRestoration = "auto"
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Hack: ensure absolute favicon link to omit 404s on document switch */
|
||||||
|
const favicon = getElement<HTMLLinkElement>(`link[rel="shortcut icon"]`)
|
||||||
|
if (typeof favicon !== "undefined")
|
||||||
|
favicon.href = favicon.href // tslint:disable-line no-self-assignment
|
||||||
|
|
||||||
|
/* Intercept link clicks and convert to state change */
|
||||||
|
const state$ = fromEvent<MouseEvent>(document.body, "click")
|
||||||
.pipe(
|
.pipe(
|
||||||
|
filter(ev => !(ev.metaKey || ev.ctrlKey)),
|
||||||
|
switchMap(ev => {
|
||||||
|
if (ev.target instanceof HTMLElement) {
|
||||||
|
const el = ev.target.closest("a")
|
||||||
|
if (el && isLocalLocation(el)) {
|
||||||
|
if (!isAnchorLocation(el))
|
||||||
|
ev.preventDefault()
|
||||||
|
return of(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NEVER
|
||||||
|
}),
|
||||||
map(el => ({ url: new URL(el.href) })),
|
map(el => ({ url: new URL(el.href) })),
|
||||||
share<State>()
|
share<State>()
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Intercept internal links to dispatch */
|
/* Always close search on link click */
|
||||||
|
state$.subscribe(() => {
|
||||||
|
setToggle("search", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Filter state changes to dispatch */
|
||||||
const push$ = state$
|
const push$ = state$
|
||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilChanged((prev, next) => prev.url.href === next.url.href),
|
distinctUntilChanged((prev, next) => prev.url.href === next.url.href),
|
||||||
@ -121,11 +154,11 @@ export function setupInstantLoading(
|
|||||||
const pop$ = fromEvent<PopStateEvent>(window, "popstate")
|
const pop$ = fromEvent<PopStateEvent>(window, "popstate")
|
||||||
.pipe(
|
.pipe(
|
||||||
filter(ev => ev.state !== null),
|
filter(ev => ev.state !== null),
|
||||||
map<PopStateEvent, State>(ev => ({
|
map(ev => ({
|
||||||
url: new URL(location.href),
|
url: new URL(location.href),
|
||||||
offset: ev.state
|
offset: ev.state
|
||||||
})),
|
})),
|
||||||
share()
|
share<State>()
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Emit location change */
|
/* Emit location change */
|
||||||
@ -135,13 +168,11 @@ export function setupInstantLoading(
|
|||||||
)
|
)
|
||||||
.subscribe(location$)
|
.subscribe(location$)
|
||||||
|
|
||||||
const dom = new DOMParser()
|
/* Fetch document on location change */
|
||||||
const ajax$ = location$
|
const ajax$ = location$
|
||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilKeyChanged("pathname"),
|
distinctUntilKeyChanged("pathname"),
|
||||||
skip(1),
|
skip(1),
|
||||||
|
|
||||||
/* Fetch document */
|
|
||||||
switchMap(url => ajax({
|
switchMap(url => ajax({
|
||||||
url: url.href,
|
url: url.href,
|
||||||
responseType: "text",
|
responseType: "text",
|
||||||
@ -154,9 +185,9 @@ export function setupInstantLoading(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
// share()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* Set new location as soon as the document was fetched */
|
||||||
push$
|
push$
|
||||||
.pipe(
|
.pipe(
|
||||||
sample(ajax$)
|
sample(ajax$)
|
||||||
@ -165,55 +196,30 @@ export function setupInstantLoading(
|
|||||||
history.pushState({}, "", url.toString())
|
history.pushState({}, "", url.toString())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/* Parse and emit document */
|
||||||
|
const dom = new DOMParser()
|
||||||
ajax$
|
ajax$
|
||||||
.pipe(
|
.pipe(
|
||||||
map(({ response }) => {
|
map(({ response }) => dom.parseFromString(response, "text/html"))
|
||||||
return dom.parseFromString(response, "text/html")
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
.subscribe(document$)
|
.subscribe(document$)
|
||||||
|
|
||||||
/* History: debounce update of viewport offset */
|
/* Intercept instant loading */
|
||||||
viewport$
|
|
||||||
.pipe(
|
|
||||||
debounceTime(250),
|
|
||||||
distinctUntilKeyChanged("offset")
|
|
||||||
)
|
|
||||||
.subscribe(({ offset }) => {
|
|
||||||
history.replaceState(offset, "")
|
|
||||||
})
|
|
||||||
|
|
||||||
/* Apply viewport offset from history */
|
|
||||||
merge(state$, pop$)
|
|
||||||
.pipe(
|
|
||||||
bufferCount(2, 1),
|
|
||||||
filter(([prev, next]) => {
|
|
||||||
return prev.url.pathname === next.url.pathname
|
|
||||||
&& !isAnchorLocation(next.url)
|
|
||||||
}),
|
|
||||||
map(([, state]) => state)
|
|
||||||
)
|
|
||||||
.subscribe(({ offset }) => {
|
|
||||||
setViewportOffset(offset || { y: 0 })
|
|
||||||
})
|
|
||||||
|
|
||||||
/* Intercept actual instant loading */
|
|
||||||
const instant$ = merge(push$, pop$)
|
const instant$ = merge(push$, pop$)
|
||||||
.pipe(
|
.pipe(
|
||||||
sample(document$)
|
sample(document$)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: from here on, everything is beta.... ###############################
|
// TODO: this must be combined with search scroll restoration on mobile
|
||||||
|
|
||||||
instant$.subscribe(({ url, offset }) => {
|
instant$.subscribe(({ url, offset }) => {
|
||||||
if (url.hash && !offset) {
|
if (url.hash && !offset) {
|
||||||
// console.log("set hash!")
|
setLocationHash(url.hash)
|
||||||
setLocationHash(url.hash) // must delay, if search is open!
|
|
||||||
} else {
|
} else {
|
||||||
setViewportOffset(offset || { y: 0 })
|
setViewportOffset(offset || { y: 0 })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/* Replace document metadata */
|
||||||
instant$
|
instant$
|
||||||
.pipe(
|
.pipe(
|
||||||
withLatestFrom(document$)
|
withLatestFrom(document$)
|
||||||
@ -238,4 +244,28 @@ export function setupInstantLoading(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/* Debounce update of viewport offset */
|
||||||
|
viewport$
|
||||||
|
.pipe(
|
||||||
|
debounceTime(250),
|
||||||
|
distinctUntilKeyChanged("offset")
|
||||||
|
)
|
||||||
|
.subscribe(({ offset }) => {
|
||||||
|
history.replaceState(offset, "")
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Set viewport offset from history */
|
||||||
|
merge(state$, pop$)
|
||||||
|
.pipe(
|
||||||
|
bufferCount(2, 1),
|
||||||
|
filter(([prev, next]) => {
|
||||||
|
return prev.url.pathname === next.url.pathname
|
||||||
|
&& !isAnchorLocation(next.url)
|
||||||
|
}),
|
||||||
|
map(([, state]) => state)
|
||||||
|
)
|
||||||
|
.subscribe(({ offset }) => {
|
||||||
|
setViewportOffset(offset || { y: 0 })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -53,14 +53,14 @@ const path =
|
|||||||
*
|
*
|
||||||
* @return Element
|
* @return Element
|
||||||
*/
|
*/
|
||||||
export function renderClipboard(
|
export function renderClipboardButton(
|
||||||
id: string
|
id: string
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class={css.container}
|
class={css.container}
|
||||||
title={translate("clipboard.copy")}
|
title={translate("clipboard.copy")}
|
||||||
data-clipboard-target={`#${id} code`}
|
data-clipboard-target={`#${id} > code`}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<path d={path}></path>
|
<path d={path}></path>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user