Skip to content

Commit

Permalink
chore: re-implement without anchorElement, refactor #2025
Browse files Browse the repository at this point in the history
  • Loading branch information
marek-mihok authored and mturoci committed Sep 6, 2023
1 parent e3768cf commit 8f35825
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 51 deletions.
50 changes: 11 additions & 39 deletions ui/src/copyable_text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ const
opacity: 0,
transition: 'opacity .5s'
},
visible: {
opacity: 1,
},
btn: {
position: 'absolute',
minWidth: 'initial',
Expand Down Expand Up @@ -61,84 +58,59 @@ 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<U>(),
[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)
}

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 <Fluent.PrimaryButton
id='copybutton'
title='Copy to clipboard'
onClick={onClick}
iconProps={{ iconName: copied ? 'CheckMark' : 'Copy' }}
className={clas(css.btn, copied ? css.copiedBtn : '', showOnHoverOnly ? css.animate : '', visible ? css.visible : '')}
className={clas(css.btn, copied ? css.copiedBtn : '', defaultVisible ? '' : css.animate)}
/>
}

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 (
<Fluent.TextField
data-test={name}
// Temporary solution which will be replaced with 'ref' once Fluent.TextField is converted to a function component.
elementRef={domRef}
value={value}
multiline={multiline}
onRenderLabel={() =>
<div className={css.labelContainer}>
<Fluent.Label>{label}</Fluent.Label>
<ClipboardCopyButton value={value} anchorElement={inputEl} showOnHoverOnly={!!multiline} />
<ClipboardCopyButton value={value} defaultVisible={!multiline} />
</div>
}
styles={{
root: {
...heightStyle,
textFieldRoot: { position: 'relative', width: pc(100) },
textFieldMultiline: multiline ? { '&:hover button': { opacity: 1 } } : undefined
},
wrapper: heightStyle,
fieldGroup: heightStyle || { minHeight: height },
Expand Down
31 changes: 19 additions & 12 deletions ui/src/markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(<ClipboardCopyButton value={str} defaultVisible={false} />, buttonContainer)
codeBlockContainer.classList.add(css.copyButton)
codeBlockContainer.insertAdjacentElement('afterbegin', buttonContainer)
}

return highlightedCode
}

Expand All @@ -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(<ClipboardCopyButton value={str} anchorElement={codeBlockContainer} showOnHoverOnly />, 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 = `<code id='${codeBlockId}' class="hljs">${prevHighlights.current[codeBlockIdx.current] || str}</code>`
Expand Down

0 comments on commit 8f35825

Please sign in to comment.