diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 8391a1d6c..e327e4ed6 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -2307,3 +2307,7 @@ ui: users_deleted: These users have been deleted. posts_deleted: These questions have been deleted. answers_deleted: These answers have been deleted. + copy: Copy to clipboard + copied: Copied + + diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 17ba2e47d..847cf395c 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -2269,3 +2269,6 @@ ui: users_deleted: 这些用户已被删除。 posts_deleted: 这些帖子已被删除。 answers_deleted: 这些回答已被删除。 + copy: 复制 + copied: 已复制 + diff --git a/ui/src/components/Editor/Viewer.tsx b/ui/src/components/Editor/Viewer.tsx index aedf61251..3f41831dc 100644 --- a/ui/src/components/Editor/Viewer.tsx +++ b/ui/src/components/Editor/Viewer.tsx @@ -25,6 +25,7 @@ import { memo, useImperativeHandle, } from 'react'; +import { useTranslation } from 'react-i18next'; import { markdownToHtml } from '@/services'; import ImgViewer from '@/components/ImgViewer'; @@ -37,6 +38,7 @@ let renderTimer; const Index = ({ value }, ref) => { const [html, setHtml] = useState(''); const previewRef = useRef(null); + const { t } = useTranslation('translation', { keyPrefix: 'messages' }); const renderMarkdown = (markdown) => { clearTimeout(renderTimer); @@ -59,7 +61,10 @@ const Index = ({ value }, ref) => { previewRef.current?.scrollTo(0, scrollTop); - htmlRender(previewRef.current); + htmlRender(previewRef.current, { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); }, [html]); useImperativeHandle(ref, () => { diff --git a/ui/src/components/Editor/utils/index.ts b/ui/src/components/Editor/utils/index.ts index 759033cca..6c7a3ac4b 100644 --- a/ui/src/components/Editor/utils/index.ts +++ b/ui/src/components/Editor/utils/index.ts @@ -24,6 +24,8 @@ import { EditorState, Compartment } from '@codemirror/state'; import { EditorView, placeholder } from '@codemirror/view'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { languages } from '@codemirror/language-data'; +import copy from 'copy-to-clipboard'; +import Tooltip from 'bootstrap/js/dist/tooltip'; import { Editor } from '../types'; import { isDarkTheme } from '@/utils/common'; @@ -31,8 +33,16 @@ import { isDarkTheme } from '@/utils/common'; import createEditorUtils from './extension'; const editableCompartment = new Compartment(); -export function htmlRender(el: HTMLElement | null) { +interface htmlRenderConfig { + copyText: string; + copySuccessText: string; +} +export function htmlRender(el: HTMLElement | null, config?: htmlRenderConfig) { if (!el) return; + const { copyText = '', copySuccessText = '' } = config || { + copyText: 'Copy to clipboard', + copySuccessText: 'Copied!', + }; // Replace all br tags with newlines // Fixed an issue where the BR tag in the editor block formula HTML caused rendering errors. el.querySelectorAll('p').forEach((p) => { @@ -69,6 +79,52 @@ export function htmlRender(el: HTMLElement | null) { a.rel = 'nofollow'; } }); + + // Add copy button to all pre tags + el.querySelectorAll('pre').forEach((pre) => { + // Create copy button + const codeWrap = document.createElement('div'); + codeWrap.className = 'position-relative a-code-wrap'; + const codeTool = document.createElement('div'); + codeTool.className = 'a-code-tool'; + const uniqueId = `a-copy-code-${Date.now().toString().substring(5)}-${Math.floor(Math.random() * 10)}${Math.floor(Math.random() * 10)}${Math.floor(Math.random() * 10)}`; + const str = ` + + `; + codeTool.innerHTML = str; + + // Add copy button to pre tag + pre.style.position = 'relative'; + + // 将 codeTool 和 pre 插入到 codeWrap 中, 并且使用 codeWrap 替换 pre + codeWrap.appendChild(codeTool); + pre.parentNode?.replaceChild(codeWrap, pre); + codeWrap.appendChild(pre); + + const tooltipTriggerList = el.querySelectorAll('.a-copy-code'); + + console.log('tooltipTriggerList', Array.from(tooltipTriggerList).length); + Array.from(tooltipTriggerList)?.map( + (tooltipTriggerEl) => new Tooltip(tooltipTriggerEl), + ); + + // Copy pre content on button click + const copyBtn = codeTool.querySelector('.a-copy-code'); + copyBtn?.addEventListener('click', () => { + const textToCopy = pre.textContent || ''; + copy(textToCopy); + // Change tooltip text on copy success + const tooltipInstance = Tooltip.getOrCreateInstance(`#${uniqueId}`); + tooltipInstance?.setContent({ '.tooltip-inner': copySuccessText }); + const myTooltipEl = document.querySelector(`#${uniqueId}`); + myTooltipEl?.addEventListener('hidden.bs.tooltip', () => { + console.log('hidden.bs.tooltip'); + tooltipInstance.setContent({ '.tooltip-inner': copyText }); + }); + }); + }); } export const useEditor = ({ diff --git a/ui/src/index.scss b/ui/src/index.scss index bcbf3c7d7..dfbe6094c 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -374,3 +374,24 @@ img[src=""] { .mb-12 { margin-bottom: 12px; } + +.a-code-wrap:hover .a-code-tool { + display: block; +} +.a-code-tool { + position: absolute; + top: 0; + right: 0; + font-size: 16px; + z-index: 1; + display: none; + .a-copy-code { + line-height: 24px; + width: 1.5rem; + height: 1.5rem; + display: flex; + justify-content: center; + align-items: center; + padding: 0; + } +} diff --git a/ui/src/pages/Legal/Privacy/index.tsx b/ui/src/pages/Legal/Privacy/index.tsx index 326c37309..5454a415d 100644 --- a/ui/src/pages/Legal/Privacy/index.tsx +++ b/ui/src/pages/Legal/Privacy/index.tsx @@ -38,7 +38,10 @@ const Index: FC = () => { if (!fmt) { return; } - htmlRender(fmt); + htmlRender(fmt, { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); }, [privacy?.privacy_policy_parsed_text]); try { diff --git a/ui/src/pages/Legal/Tos/index.tsx b/ui/src/pages/Legal/Tos/index.tsx index fd9b5045d..6e9965466 100644 --- a/ui/src/pages/Legal/Tos/index.tsx +++ b/ui/src/pages/Legal/Tos/index.tsx @@ -38,7 +38,10 @@ const Index: FC = () => { if (!fmt) { return; } - htmlRender(fmt); + htmlRender(fmt, { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); }, [tos?.terms_of_service_parsed_text]); try { diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index 931b1a755..825f698da 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -76,8 +76,13 @@ const Index: FC = ({ return; } - htmlRender(answerRef.current.querySelector('.fmt')); + htmlRender(answerRef.current.querySelector('.fmt'), { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); + }, [answerRef.current]); + useEffect(() => { if (aid === data.id) { setTimeout(() => { const element = answerRef.current; @@ -87,7 +92,7 @@ const Index: FC = ({ } }, 100); } - }, [data.id, answerRef.current]); + }, [data.id]); if (!data?.id) { return null; diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 0a76c2f9f..081c8759b 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -79,7 +79,10 @@ const Index: FC = ({ data, initPage, hasAnswer, isLogged }) => { return; } - htmlRender(ref.current); + htmlRender(ref.current, { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); }, [ref.current]); if (!data?.id) { diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index 831aa5a24..3be7befcf 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -96,7 +96,10 @@ const Index = () => { if (!questionContentRef?.current) { return; } - htmlRender(questionContentRef.current); + htmlRender(questionContentRef.current, { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); }, [questionContentRef]); usePromptWithUnload({ diff --git a/ui/src/pages/Review/components/FlagContent/index.tsx b/ui/src/pages/Review/components/FlagContent/index.tsx index 44ac1aeb2..95bdc85ec 100644 --- a/ui/src/pages/Review/components/FlagContent/index.tsx +++ b/ui/src/pages/Review/components/FlagContent/index.tsx @@ -120,7 +120,10 @@ const Index: FC = ({ refreshCount }) => { } setTimeout(() => { - htmlRender(ref.current); + htmlRender(ref.current, { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); }, 70); }, [ref.current]); diff --git a/ui/src/pages/Review/components/QueuedContent/index.tsx b/ui/src/pages/Review/components/QueuedContent/index.tsx index f8fa36d50..66470e5f0 100644 --- a/ui/src/pages/Review/components/QueuedContent/index.tsx +++ b/ui/src/pages/Review/components/QueuedContent/index.tsx @@ -87,7 +87,10 @@ const Index: FC = ({ refreshCount }) => { } setTimeout(() => { - htmlRender(ref.current); + htmlRender(ref.current, { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); }, 70); }, [ref.current]); diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 7c620715e..1c32e2492 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -80,7 +80,10 @@ const TagIntroduction = () => { if (!fmt) { return; } - htmlRender(fmt); + htmlRender(fmt, { + copySuccessText: t('copied', { keyPrefix: 'messages' }), + copyText: t('copy', { keyPrefix: 'messages' }), + }); }, [tagInfo?.parsed_text]); if (!tagInfo) {