Skip to content

Commit

Permalink
Features/copy code (#64)
Browse files Browse the repository at this point in the history
* Update TextEditor.tsx

* Update TextEditor.tsx

* Create CodeBlockWithCopy.tsx

* Update useTextEditor.ts

* Create Check.tsx

* Create Copy.tsx

* Update index.css

* Update types.d.ts

* Update README.md

* Update README.md
  • Loading branch information
tiavina-mika authored Oct 9, 2024
1 parent b127201 commit f255f66
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 31 deletions.
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,20 @@ function App() {
```

### Image upload
Here is the corrected English version:

```html
<ul>
<li>The image can be uploaded to the server via an API call or directly into the content as base64 string. </li>
<li>The image can be uploaded using upload button or pasted or dropped.</li>
<li>Add or modify the alt text and the legend (title) of the image</li>
<li>Delete the selected image using `Delete` keyboard key</li>
<li>The image can be uploaded to the server via an API call or inserted directly into the content as a base64 string.</li>
<li>The image can be uploaded using the upload button, or pasted or dropped.</li>
<li>Add or modify the alt text and the caption (title) of the image.</li>
<li>Delete the selected image using the `Delete` key on the keyboard.</li>
</ul>
```

```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);
Expand Down Expand Up @@ -160,15 +164,15 @@ import { TextEditorReadOnly } from 'mui-tiptap-editor';
<TextEditorReadOnly value="<h1>Hello word!</h1>" />
```

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
<TiptapParser content="<h1>Hello world</h1>" />
```

## Customization

### Toolbar
<p> Can display the menus as needed</p>
<p>Can display the menus as required.</p>

```tsx
<TextEditor toolbar={['bold', 'italic', 'underline']} />
Expand Down Expand Up @@ -300,6 +304,14 @@ See [`here`](https://github.com/tiavina-mika/mui-tiptap-editor/blob/main/src/dev
<tr>
<td>Versions</td>
<td>Features</td>
<tr>
<tr>
<th><a href="https://github.com/tiavina-mika/mui-tiptap-editor/pull/55">v0.9.19</a></th>
<td>
<ul>
<li>Copy the code block</li>
</ul>
</td>
<tr>
<tr>
<th><a href="https://github.com/tiavina-mika/mui-tiptap-editor/pull/55">v0.9.11</a></th>
Expand Down
56 changes: 56 additions & 0 deletions src/extensions/CodeBlockWithCopy.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<NodeViewWrapper className="code-block-root">
<button onClick={copyToClipboard}>
{copied ? <Check /> : <Copy />}
</button>
<pre>
<NodeViewContent as="code" />
</pre>
</NodeViewWrapper>
);
};

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) => <CodeBlockWithCopy {...props} />,
{ className }
);
},
})
.configure({
// Configure lowlight with common languages and set default language
lowlight: createLowlight(common),
defaultLanguage: language,
})
}
18 changes: 9 additions & 9 deletions src/hooks/useTextEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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] }),
Expand Down Expand Up @@ -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 = {
Expand All @@ -108,6 +102,10 @@ export type TextEditorProps = {
userPathname?: string;
uploadFileOptions?: Omit<ImageUploadOptions, 'type'>;
uploadFileLabels?: ILabels['upload'];
/**
* props for the block code extension
*/
codeBlock?: CodeBlockWithCopyProps;
} & Partial<EditorOptions>;

export const useTextEditor = ({
Expand All @@ -121,6 +119,7 @@ export const useTextEditor = ({
uploadFileLabels,
userPathname,
editable = true,
codeBlock,
...editorOptions
}: TextEditorProps) => {
const theme = useTheme();
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/icons/Check.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

import { SvgIcon } from "@mui/material";

const Check = () => {
return (
<SvgIcon>
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" strokeLinecap="round" strokeLinejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<polyline points="20 6 9 17 4 12" />
</svg>
</SvgIcon>
);
}

export default Check;
15 changes: 15 additions & 0 deletions src/icons/Copy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

import { SvgIcon } from "@mui/material";

const Copy = () => {
return (
<SvgIcon>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" stroke-linejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
</SvgIcon>
);
}

export default Copy;
57 changes: 43 additions & 14 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
5 changes: 5 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit f255f66

Please sign in to comment.