This is an automated email from the ASF dual-hosted git repository.

shuai pushed a commit to branch feat/1.4.3/ui
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git

commit 7a15e24f7bebb78d486725eabb9b4c5b6ea62968
Author: shuai <[email protected]>
AuthorDate: Fri Dec 27 12:12:12 2024 +0800

    feat: add a copy button to the code block #1211
---
 i18n/en_US.yaml                                    |  2 +
 i18n/zh_CN.yaml                                    |  3 +-
 ui/src/components/Editor/Viewer.tsx                |  7 ++-
 ui/src/components/Editor/utils/index.ts            | 58 +++++++++++++++++++++-
 ui/src/index.scss                                  | 21 ++++++++
 ui/src/pages/Legal/Privacy/index.tsx               |  5 +-
 ui/src/pages/Legal/Tos/index.tsx                   |  5 +-
 .../Questions/Detail/components/Answer/index.tsx   |  9 +++-
 .../Questions/Detail/components/Question/index.tsx |  5 +-
 ui/src/pages/Questions/EditAnswer/index.tsx        |  5 +-
 .../pages/Review/components/FlagContent/index.tsx  |  5 +-
 .../Review/components/QueuedContent/index.tsx      |  5 +-
 ui/src/pages/Tags/Info/index.tsx                   |  5 +-
 13 files changed, 123 insertions(+), 12 deletions(-)

diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 0b123b33..b11ba3a6 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -2300,5 +2300,7 @@ ui:
     user_deleted: This user has been deleted.
     badge_activated: This badge has been activated.
     badge_inactivated: This badge has been inactivated.
+    copy: Copy to clipboard
+    copied: Copied
 
 
diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml
index 7b32c82b..9eb0254a 100644
--- a/i18n/zh_CN.yaml
+++ b/i18n/zh_CN.yaml
@@ -2262,5 +2262,6 @@ ui:
     user_deleted: 此用户已被删除
     badge_activated: 此徽章已被激活。
     badge_inactivated: 此徽章已被禁用。
-
+    copy: 复制
+    copied: 已复制
 
diff --git a/ui/src/components/Editor/Viewer.tsx 
b/ui/src/components/Editor/Viewer.tsx
index aedf6125..3f41831d 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<HTMLDivElement>(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 759033cc..6c7a3ac4 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 = `
+      <button type="button" class="btn btn-dark rounded-0 a-copy-code" 
data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="${copyText}" 
id="${uniqueId}">
+        <i class="br bi-copy"></i>
+      </button>
+    `;
+    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 bcbf3c7d..dfbe6094 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 326c3730..5454a415 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 fd9b5045..6e996546 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 931b1a75..825f698d 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<Props> = ({
       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<Props> = ({
         }
       }, 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 0a76c2f9..081c8759 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<Props> = ({ 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 831aa5a2..3be7befc 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 44ac1aeb..95bdc85e 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<IProps> = ({ 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 f8fa36d5..66470e5f 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<IProps> = ({ 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 7c620715..1c32e249 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) {

Reply via email to