diff --git a/README.md b/README.md index 52b435a..1c66358 100644 --- a/README.md +++ b/README.md @@ -111,16 +111,20 @@ function App() { ``` ### Image upload +Here is the corrected English version: + +```html +``` ```tsx -// example of API upload using fetch -// the return data must be the image url (string) or image attributes (object) like src, alt, id, title, ... +// Example of an API upload using fetch +// The returned data must be the image URL (string) or image attributes (object) such as src, alt, id, title, etc. const uploadFile = async (file: File) => { const formData = new FormData(); formData.append("file", file); @@ -160,7 +164,7 @@ import { TextEditorReadOnly } from 'mui-tiptap-editor'; ``` -2. If it is just displaying the value without using the editor, you can use this library [`tiptap-parser`](https://www.npmjs.com/package/tiptap-parser). Example: The editor is used in the back office, but the content must be displayed on the website +2. If you only need to display the value without using the editor, you can use this library: [`tiptap-parser`](https://www.npmjs.com/package/tiptap-parser). Example: The editor is used in the back office, but the content must be displayed on the website. ```tsx ``` @@ -168,7 +172,7 @@ import { TextEditorReadOnly } from 'mui-tiptap-editor'; ## Customization ### Toolbar -

Can display the menus as needed

+

Can display the menus as required.

```tsx @@ -300,6 +304,14 @@ See [`here`](https://github.com/tiavina-mika/mui-tiptap-editor/blob/main/src/dev Versions Features + + + v0.9.19 + +
    +
  • Copy the code block
  • +
+ v0.9.11 diff --git a/src/extensions/CodeBlockWithCopy.tsx b/src/extensions/CodeBlockWithCopy.tsx new file mode 100644 index 0000000..254c81e --- /dev/null +++ b/src/extensions/CodeBlockWithCopy.tsx @@ -0,0 +1,56 @@ +/** + * This file defines a custom CodeBlockWithCopy component for use with the TipTap editor. + * It includes a button to copy the code block content to the clipboard. + * The component uses lowlight for syntax highlighting and integrates with TipTap's NodeViewRenderer. + */ + +import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react'; +import { useState } from 'react'; +import { createLowlight, common } from "lowlight"; +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; +import { CodeBlockWithCopyProps } from '../types'; +import Copy from '../icons/Copy'; +import Check from '../icons/Check'; + +const CodeBlockWithCopy = ({ node }: any) => { + const [copied, setCopied] = useState(false); + + + const copyToClipboard = () => { + navigator.clipboard.writeText(node.textContent).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); // "Copied!" message for 2 seconds + }); + }; + + return ( + + +
+        
+      
+
+ ); +}; + +export const getCodeBlockWithCopy = (props?: CodeBlockWithCopyProps) => { + const { language = 'javascript', className } = props || {}; + + return CodeBlockLowlight + .extend({ + addNodeView() { + // Use ReactNodeViewRenderer to render the CodeBlockWithCopy component + return ReactNodeViewRenderer( + (props: any) => , + { className } + ); + }, + }) + .configure({ + // Configure lowlight with common languages and set default language + lowlight: createLowlight(common), + defaultLanguage: language, + }) +} diff --git a/src/hooks/useTextEditor.ts b/src/hooks/useTextEditor.ts index 395fbbc..d6de6be 100644 --- a/src/hooks/useTextEditor.ts +++ b/src/hooks/useTextEditor.ts @@ -15,10 +15,8 @@ import Table from "@tiptap/extension-table"; import TableCell from "@tiptap/extension-table-cell"; import TableHeader from "@tiptap/extension-table-header"; import TableRow from "@tiptap/extension-table-row"; -import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import Youtube from "@tiptap/extension-youtube"; import BubbleMenu from '@tiptap/extension-bubble-menu'; -import { createLowlight, common } from "lowlight"; import { useEditor, EditorOptions, @@ -28,9 +26,10 @@ import { import StarterKit from '@tiptap/starter-kit'; import { useEffect } from 'react'; import Heading from '@tiptap/extension-heading'; -import { ILabels, ImageUploadOptions, ITextEditorOption } from '../types.d'; +import { CodeBlockWithCopyProps, ILabels, ImageUploadOptions, ITextEditorOption } from '../types.d'; import getCustomImage from '../extensions/CustomImage'; import { getCustomMention } from '../extensions/CustomMention'; +import { getCodeBlockWithCopy } from '../extensions/CodeBlockWithCopy'; const extensions = [ Color.configure({ types: [TextStyle.name, ListItem.name] }), @@ -86,15 +85,10 @@ const extensions = [ Youtube, TextAlign.configure({ types: ["heading", "paragraph", "table", "image"] - }), - CodeBlockLowlight.configure({ - lowlight: createLowlight(common), - defaultLanguage: "javascript" - }), + }),, BubbleMenu.configure({ element: document.querySelector('.bubble-menu'), } as any), - // History ]; export type TextEditorProps = { @@ -108,6 +102,10 @@ export type TextEditorProps = { userPathname?: string; uploadFileOptions?: Omit; uploadFileLabels?: ILabels['upload']; + /** + * props for the block code extension + */ + codeBlock?: CodeBlockWithCopyProps; } & Partial; export const useTextEditor = ({ @@ -121,6 +119,7 @@ export const useTextEditor = ({ uploadFileLabels, userPathname, editable = true, + codeBlock, ...editorOptions }: TextEditorProps) => { const theme = useTheme(); @@ -136,6 +135,7 @@ export const useTextEditor = ({ getCustomMention({ pathname: userPathname, mentions }), // upload image extension getCustomImage(uploadFileOptions, uploadFileLabels, uploadFileOptions?.maxMediaLegendLength), + getCodeBlockWithCopy(codeBlock), ...extensions, ] as AnyExtension[], /* The `onUpdate` function in the `useTextEditor` hook is a callback that is triggered whenever the diff --git a/src/icons/Check.tsx b/src/icons/Check.tsx new file mode 100644 index 0000000..f32e16f --- /dev/null +++ b/src/icons/Check.tsx @@ -0,0 +1,14 @@ + +import { SvgIcon } from "@mui/material"; + +const Check = () => { + return ( + + + + + + ); +} + +export default Check; diff --git a/src/icons/Copy.tsx b/src/icons/Copy.tsx new file mode 100644 index 0000000..92fe892 --- /dev/null +++ b/src/icons/Copy.tsx @@ -0,0 +1,15 @@ + +import { SvgIcon } from "@mui/material"; + +const Copy = () => { + return ( + + + + + + + ); +} + +export default Copy; diff --git a/src/index.css b/src/index.css index 496d6f1..9f5e1b5 100644 --- a/src/index.css +++ b/src/index.css @@ -127,20 +127,7 @@ code { border-left: 3px solid rgba(13, 13, 13, 0.1); padding-left: 1rem; } -.tiptap pre { - background: #0d0d0d; - color: #fff; - font-family: "JetBrainsMono", monospace; - padding: 0.75rem 1rem; - border-radius: 0.5rem; -} -/* code highlight */ -.tiptap pre code { - color: inherit; - padding: 0; - background: none; - font-size: 0.8rem; -} + .tiptap .hljs-comment, .tiptap .hljs-quote { @@ -310,3 +297,45 @@ code { .tiptap-image { display: flex; } + +/* --------- code block ----------- */ +.tiptap .code-block-root { + position: relative; +} +/* important: code container */ +.tiptap .code-block-root pre { + background: #0d0d0d; + color: #fff; + font-family: "JetBrainsMono", monospace; + padding: 1rem; + border-radius: 0.5rem; +} +/* code highlight */ +.tiptap .code-block-root pre code { + color: inherit; + padding: 0; + background: none; + font-size: 0.8rem; +} +.tiptap .code-block-root button { + position: absolute; + right: 1rem; + top: 1rem; + background: transparent; + border: none; + display: flex; + justify-content: center; + align-items: center; + border: 1px solid rgb(255 255 255 / 0.3); + width: 2rem; + height: 2rem; + cursor: pointer; + z-index: 1000; + color: #fff; + font-size: 12px; + border-radius: .25rem; + } + +.tiptap .code-block-root button svg { + width: 1rem; + } diff --git a/src/types.d.ts b/src/types.d.ts index fb2dc0e..0b3d34a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -34,6 +34,11 @@ export type UploadResponse = { id?: string; alt?: string; }; + +export type CodeBlockWithCopyProps = { + language?: string; + className?: string; +} /** * Image upload options from drop or paste event * the image can be uploaded to the server via an API or saved inside as a base64 string