diff --git a/ui/src/copyable_text.tsx b/ui/src/copyable_text.tsx index 6cf735176e..9f948b0692 100644 --- a/ui/src/copyable_text.tsx +++ b/ui/src/copyable_text.tsx @@ -11,9 +11,6 @@ const opacity: 0, transition: 'opacity .5s' }, - visible: { - opacity: 1, - }, btn: { position: 'absolute', minWidth: 'initial', @@ -61,27 +58,19 @@ export interface CopyableText { height?: S } -type ClipboardCopyButton = { - /** Text to be copied to clipboard. */ - value: S, - /** The element to which the copy button is attached. */ - anchorElement: HTMLElement | undefined, - /** Show copy button only on hover over anchor element. */ - showOnHoverOnly?: B -} - -export const ClipboardCopyButton = ({ value, anchorElement, showOnHoverOnly = false }: ClipboardCopyButton) => { +export const ClipboardCopyButton = ({ value, defaultVisible = true }: { value: S, defaultVisible?: B }) => { const timeoutRef = React.useRef(), [copied, setCopied] = React.useState(false), - [visible, setVisible] = React.useState(!showOnHoverOnly), onClick = React.useCallback(async () => { - if (!anchorElement) return try { if (document.queryCommandSupported('copy')) { - (anchorElement as any)?.select() + const el = document.createElement('textarea') + el.value = value + document.body.appendChild(el) + el.select() document.execCommand('copy') - window.getSelection()?.removeAllRanges() + document.body.removeChild(el) } } catch (error) { await window.navigator.clipboard.writeText(value) @@ -89,56 +78,39 @@ export const ClipboardCopyButton = ({ value, anchorElement, showOnHoverOnly = fa setCopied(true) timeoutRef.current = window.setTimeout(() => setCopied(false), 2000) - }, [anchorElement, value]) - - React.useEffect(() => { - if (anchorElement && showOnHoverOnly) { - anchorElement.addEventListener('mouseenter', () => setVisible(true)) - anchorElement.addEventListener('mouseleave', (ev: MouseEvent) => { - if ((ev.relatedTarget as HTMLElement)?.id === 'copybutton') return - setVisible(false) - }) - } - }, [anchorElement, showOnHoverOnly]) + }, [value]) React.useEffect(() => () => window.clearTimeout(timeoutRef.current), []) return } export const XCopyableText = ({ model }: { model: CopyableText }) => { const { name, multiline, label, value, height } = model, - heightStyle = multiline && height === '1' ? fullHeightStyle : undefined, - [inputEl, setInputEl] = React.useState(), - domRef = React.useCallback(node => { - const inputEl = node?.children[0]?.children[1] - if (inputEl) setInputEl(inputEl) - }, []) + heightStyle = multiline && height === '1' ? fullHeightStyle : undefined return (
{label} - +
} styles={{ root: { ...heightStyle, textFieldRoot: { position: 'relative', width: pc(100) }, + textFieldMultiline: multiline ? { '&:hover button': { opacity: 1 } } : undefined }, wrapper: heightStyle, fieldGroup: heightStyle || { minHeight: height }, diff --git a/ui/src/markdown.tsx b/ui/src/markdown.tsx index 95fbd57f3e..9dac854479 100644 --- a/ui/src/markdown.tsx +++ b/ui/src/markdown.tsx @@ -69,6 +69,14 @@ const }, }, }, + copyButton: { + position: 'relative', + $nest: { + '&:hover button': { + opacity: 1 + } + } + } }) const highlightSyntax = async (str: S, language: S, codeBlockId: S) => { const codeBlock = document.getElementById(codeBlockId) @@ -93,6 +101,16 @@ const highlightSyntax = async (str: S, language: S, codeBlockId: S) => { : hljs.highlightAuto(str).value codeBlock.innerHTML = highlightedCode + + // Add copy button. + const codeBlockContainer = codeBlock.parentElement + if (codeBlockContainer) { + const buttonContainer = document.createElement('span') + ReactDOM.render(, buttonContainer) + codeBlockContainer.classList.add(css.copyButton) + codeBlockContainer.insertAdjacentElement('afterbegin', buttonContainer) + } + return highlightedCode } @@ -107,18 +125,7 @@ export const Markdown = ({ source }: { source: S }) => { // HACK: MarkdownIt does not support async rules. // https://github.com/markdown-it/markdown-it/blob/master/docs/development.md#i-need-async-rule-how-to-do-it - setTimeout(async () => prevHighlights.current[+codeBlockId] = await highlightSyntax(str, lang, codeBlockId).finally(() => { - // Add copy button once code block is higlighted. - const codeBlockContainer = document.getElementById(codeBlockId)?.parentElement - if (codeBlockContainer) { - const buttonContainer = document.createElement('span') - ReactDOM.render(, buttonContainer, - () => { - codeBlockContainer.style.position = 'relative' - codeBlockContainer.insertAdjacentElement('afterbegin', buttonContainer) - }) - } - }), 0) + setTimeout(async () => prevHighlights.current[+codeBlockId] = await highlightSyntax(str, lang, codeBlockId), 0) // TODO: Sanitize the HTML. const ret = `${prevHighlights.current[codeBlockIdx.current] || str}`