Skip to content
93 changes: 40 additions & 53 deletions src/Elastic.Documentation.Site/Assets/copybutton.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Localization support
import * as ClipboardJS from 'clipboard'
import { $$ } from 'select-dom'

const DOCUMENTATION_OPTIONS = {
VERSION: '',
Expand Down Expand Up @@ -97,16 +96,7 @@ if (!iconCopy) {
</svg>`
}

const codeCellId = (index) => `codecell${index}`

// Clears selected text since ClipboardJS will select the text when copying
const clearSelection = () => {
if (window.getSelection) {
window.getSelection().removeAllRanges()
} else if ('selection' in document) {
;(document.selection as Selection).empty()
}
}
const codeCellId = (index: number, prefix: string) => `${prefix}${index}`

// Changes tooltip text for a moment, then changes it back
// We want the timeout of our `success` class to be a bit shorter than the
Expand All @@ -131,30 +121,45 @@ const temporarilyChangeIcon = (el) => {
}, timeoutIcon)
}

const addCopyButtonToCodeCells = () => {
// If ClipboardJS hasn't loaded, wait a bit and try again. This
// happens because we load ClipboardJS asynchronously.

const addCopyButtonToCodeCells = (
selector: string,
baseElement: ParentNode,
prefix: string
) => {
// Add copybuttons to all of our code cells
const COPYBUTTON_SELECTOR = '.highlight pre'
const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR)
const codeCells = $$(selector, baseElement)
codeCells.forEach((codeCell, index) => {
if (codeCell.id) {
return
}

const id = codeCellId(index)
const id = codeCellId(index, prefix)
codeCell.setAttribute('id', id)
const clipboardButton = document.createElement('button')
clipboardButton.setAttribute('aria-label', 'Copy code to clipboard')
clipboardButton.className = 'copybtn o-tooltip--left'
clipboardButton.setAttribute('data-tooltip', messages[locale]['copy'])
clipboardButton.setAttribute('data-clipboard-target', `#${id}`)
clipboardButton.innerHTML = iconCopy
clipboardButton.onclick = async () => {
try {
const text = copyTargetText(clipboardButton, baseElement)
await navigator.clipboard.writeText(text)
temporarilyChangeTooltip(
clipboardButton,
messages[locale]['copy'],
messages[locale]['copy_success']
)
temporarilyChangeIcon(clipboardButton)
} catch (error) {
console.error(error)
}
}

const clipboardButton = (id) =>
`<button aria-label="Copy code to clipboard" class="copybtn o-tooltip--left" data-tooltip="${messages[locale]['copy']}" data-clipboard-target="#${id}">
${iconCopy}
</button>`
codeCell.insertAdjacentHTML('afterend', clipboardButton(id))
codeCell.insertAdjacentElement('afterend', clipboardButton)
})

function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
function escapeRegExp(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}

function filterText(target, excludes) {
Expand Down Expand Up @@ -228,8 +233,8 @@ const addCopyButtonToCodeCells = () => {
return textContent
}

const copyTargetText = (trigger) => {
const target = document.querySelector(
const copyTargetText = (trigger, searchElement: ParentNode = document) => {
const target = searchElement.querySelector(
trigger.attributes['data-clipboard-target'].value
)
// get filtered text
Expand All @@ -239,30 +244,12 @@ const addCopyButtonToCodeCells = () => {
.join('\n')
return formatCopyText(text, '', false, true, true, true, '', '')
}

// Initialize with a callback so we can modify the text before copy
const clipboard = new ClipboardJS('.copybtn', { text: copyTargetText })

// Update UI with error/success messages
clipboard.on('success', (event) => {
clearSelection()
temporarilyChangeTooltip(
event.trigger,
messages[locale]['copy'],
messages[locale]['copy_success']
)
temporarilyChangeIcon(event.trigger)
})

clipboard.on('error', (event) => {
temporarilyChangeTooltip(
event.trigger,
messages[locale]['copy'],
messages[locale]['copy_failure']
)
})
}

export function initCopyButton() {
addCopyButtonToCodeCells()
export function initCopyButton(
selector: string = '.highlight pre',
baseElement: ParentNode = document,
prefix: string = 'markdown-content-codecell-'
) {
addCopyButtonToCodeCells(selector, baseElement, prefix)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import { EuiButton, useEuiTheme } from '@elastic/eui'
import { css } from '@emotion/react'
import * as React from 'react'

const buttonStyles = css`
border: none;
& > span {
justify-content: flex-start;
}
`

export interface AskAiSuggestion {
question: string
}
Expand All @@ -16,15 +23,14 @@ export const AskAiSuggestions = (props: Props) => {
const { submitQuestion } = useChatActions()
const { setModalMode } = useModalActions()
const { euiTheme } = useEuiTheme()
const buttonCss = css`
border: none;
& > span {
justify-content: flex-start;
}

const dynamicButtonStyles = css`
${buttonStyles}
svg {
color: ${euiTheme.colors.textSubdued};
}
`

return (
<ul>
{Array.from(props.suggestions).map((suggestion) => (
Expand All @@ -34,7 +40,7 @@ export const AskAiSuggestions = (props: Props) => {
color="text"
fullWidth
size="s"
css={buttonCss}
css={dynamicButtonStyles}
onClick={() => {
submitQuestion(suggestion.question)
setModalMode('askAi')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@ import { css } from '@emotion/react'
import * as React from 'react'
import { useCallback, useEffect, useRef } from 'react'

const containerStyles = css`
height: 100%;
max-height: 70vh;
overflow: hidden;
`

const scrollContainerStyles = css`
position: relative;
overflow: hidden;
`

const scrollableStyles = css`
height: 100%;
overflow-y: auto;
scrollbar-gutter: stable;
padding: 1rem;
`

const messagesStyles = css`
max-width: 800px;
margin: 0 auto;
`

// Small helper for scroll behavior
const scrollToBottom = (container: HTMLDivElement | null) => {
if (!container) return
Expand Down Expand Up @@ -44,28 +67,9 @@ export const Chat = () => {
const scrollRef = useRef<HTMLDivElement>(null)
const lastMessageStatusRef = useRef<string | null>(null)

const containerStyles = css`
height: 100%;
max-height: 70vh;
overflow: hidden;
`

const scrollContainerStyles = css`
position: relative;
overflow: hidden;
`

const scrollableStyles = css`
height: 100%;
overflow-y: auto;
scrollbar-gutter: stable;
const dynamicScrollableStyles = css`
${scrollableStyles}
${useEuiOverflowScroll('y', true)}
padding: 1rem;
`

const messagesStyles = css`
max-width: 800px;
margin: 0 auto;
`

const handleSubmit = useCallback(
Expand Down Expand Up @@ -116,14 +120,14 @@ export const Chat = () => {
gutterSize="none"
css={containerStyles}
>
{/* Header - only show when there are messages */}
<EuiSpacer size="m" />

{messages.length > 0 && (
<NewConversationHeader onClick={clearChat} />
)}

{/* Messages */}
<EuiFlexItem grow={true} css={scrollContainerStyles}>
<div ref={scrollRef} css={scrollableStyles}>
<div ref={scrollRef} css={dynamicScrollableStyles}>
{messages.length === 0 ? (
<EuiEmptyPrompt
iconType="logoElastic"
Expand Down
Loading
Loading