diff --git a/docs/.vitepress/locale.ts b/docs/.vitepress/locale.ts index a2f4613..d48169f 100644 --- a/docs/.vitepress/locale.ts +++ b/docs/.vitepress/locale.ts @@ -228,6 +228,10 @@ export function getLocaleConfig(lang: string) { text: 'Emoji', link: '/extensions/Emoji/index.md', }, + { + text: 'Katex', + link: '/extensions/Katex/index.md', + }, ], }, ] diff --git a/docs/extensions/Emoji/index.md b/docs/extensions/Emoji/index.md index 0176b4e..fb5a18c 100644 --- a/docs/extensions/Emoji/index.md +++ b/docs/extensions/Emoji/index.md @@ -1,5 +1,9 @@ --- description: Emoji + +next: + text: Katex + link: /extensions/Katex/index.md --- # Emoji diff --git a/docs/extensions/Katex/index.md b/docs/extensions/Katex/index.md new file mode 100644 index 0000000..d4f6819 --- /dev/null +++ b/docs/extensions/Katex/index.md @@ -0,0 +1,21 @@ +--- +description: Katex +--- + +# Katex + +- Katex Extension for Tiptap Editor. +- This extension allows you to add Katex math equations to your editor. +- This extension is based on [katex](https://katex.org/). + +## Usage + +```tsx +import { Katex } from 'reactjs-tiptap-editor'; // [!code ++] + +const extensions = [ + ..., + // Import Extensions Here + Katex, // [!code ++] +]; +``` diff --git a/docs/guide/bubble-menu.md b/docs/guide/bubble-menu.md index 13a31d8..9698af8 100644 --- a/docs/guide/bubble-menu.md +++ b/docs/guide/bubble-menu.md @@ -23,6 +23,7 @@ The system provides the following default bubble menus: | BubbleMenuImage | Provides image-related operations like resizing, alignment, etc. | imageConfig | | BubbleMenuVideo | Provides video-related operations like playback control, size adjustment, etc. | videoConfig | | TableBubbleMenu | Provides table-related operations like adding/deleting rows and columns, merging cells, etc. | tableConfig | +| BubbleMenuKatex | The BubbleMenuKatex component provides operations related to rendering mathematical equations using the KaTeX library. It allows users to insert, edit, and format mathematical expressions within the editor. | katexConfig | | ColumnsMenu | Provides multi-column layout operations like adjusting column numbers, widths, etc. | columnConfig | | ContentMenu | Provides general content-related operations like copy, paste, delete, etc. | floatingMenuConfig | diff --git a/package.json b/package.json index 0948927..4c1086b 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "clsx": "^2.1.1", "deep-equal": "^2.2.3", "echo-drag-handle-plugin": "^0.0.2", + "katex": "^0.16.11", "lodash-unified": "^1.0.3", "lucide-react": "^0.427.0", "react-colorful": "^5.6.1", @@ -123,6 +124,7 @@ "@eslint-react/eslint-plugin": "^1.10.1", "@total-typescript/ts-reset": "^0.5.1", "@types/deep-equal": "^1.0.4", + "@types/katex": "^0.16.7", "@types/node": "^22.3.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", diff --git a/playground/src/App.tsx b/playground/src/App.tsx index 3abf62a..1cf870d 100644 --- a/playground/src/App.tsx +++ b/playground/src/App.tsx @@ -23,6 +23,7 @@ import RichTextEditor, { ImageUpload, Indent, Italic, + Katex, LineHeight, Link, MoreMark, @@ -108,6 +109,7 @@ const extensions = [ Table, Iframe.configure({ spacer: true }), Emoji, + Katex, ] const DEFAULT = `

Rich Text Editor

A modern WYSIWYG rich text editor based on tiptap and shadcn ui for Reactjs


Demo

👉Demo

Features

Installation

pnpm add reactjs-tiptap-editor

` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d52f9a..64ca2e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,6 +179,9 @@ importers: echo-drag-handle-plugin: specifier: ^0.0.2 version: 0.0.2(@tiptap/core@2.6.3(@tiptap/pm@2.6.3))(@tiptap/pm@2.6.3)(y-prosemirror@1.2.9(prosemirror-model@1.22.2)(prosemirror-state@1.4.3)(prosemirror-view@1.33.9)(y-protocols@1.0.6(yjs@13.6.18))(yjs@13.6.18)) + katex: + specifier: ^0.16.11 + version: 0.16.11 lodash-unified: specifier: ^1.0.3 version: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) @@ -213,6 +216,9 @@ importers: '@types/deep-equal': specifier: ^1.0.4 version: 1.0.4 + '@types/katex': + specifier: ^0.16.7 + version: 0.16.7 '@types/node': specifier: ^22.3.0 version: 22.3.0 @@ -2737,6 +2743,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} @@ -4912,6 +4921,10 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + katex@0.16.11: + resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -9512,6 +9525,8 @@ snapshots: '@types/json5@0.0.29': {} + '@types/katex@0.16.7': {} + '@types/linkify-it@5.0.0': {} '@types/lodash-es@4.17.12': @@ -12324,6 +12339,10 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 + katex@0.16.11: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 diff --git a/src/components/BubbleMenu.tsx b/src/components/BubbleMenu.tsx index b72dca7..3790e66 100644 --- a/src/components/BubbleMenu.tsx +++ b/src/components/BubbleMenu.tsx @@ -3,6 +3,7 @@ import type { Editor } from '@tiptap/core' import { BubbleMenuImage, BubbleMenuLink, BubbleMenuText, BubbleMenuVideo, ContentMenu, TableBubbleMenu } from '@/components' import ColumnsMenu from '@/extensions/MultiColumn/menus/ColumnsMenu' import type { BubbleMenuProps as BubbleMenuPropsType } from '@/types' +import BubbleMenuKatex from '@/components/menus/components/BubbleMenuKatex' export interface BubbleMenuComponentProps { editor: Editor @@ -27,6 +28,7 @@ export function BubbleMenu({ editor, disabled, bubbleMenu }: BubbleMenuComponent extensionsNames.includes('link') && !bubbleMenu?.linkConfig?.hidden ? : null, extensionsNames.includes('image') && !bubbleMenu?.imageConfig?.hidden ? : null, extensionsNames.includes('video') && !bubbleMenu?.videoConfig?.hidden ? : null, + extensionsNames.includes('katex') && !bubbleMenu?.katexConfig?.hidden ? : null, !bubbleMenu?.floatingMenuConfig?.hidden ? : null, !bubbleMenu?.textConfig?.hidden ? : null, ] diff --git a/src/components/icons/icons.ts b/src/components/icons/icons.ts index 36ce6e1..af6a723 100644 --- a/src/components/icons/icons.ts +++ b/src/components/icons/icons.ts @@ -39,6 +39,7 @@ import { Quote, Redo2, Replace, + Sigma, SmilePlus, SmilePlusIcon, Sparkles, @@ -151,4 +152,5 @@ export const icons = { DeleteRow, SearchAndReplace: Replace, EmojiIcon: SmilePlusIcon, + KatexIcon: Sigma, } as any diff --git a/src/components/menus/components/BubbleMenuKatex.tsx b/src/components/menus/components/BubbleMenuKatex.tsx new file mode 100644 index 0000000..cb97ea6 --- /dev/null +++ b/src/components/menus/components/BubbleMenuKatex.tsx @@ -0,0 +1,103 @@ +import { BubbleMenu } from '@tiptap/react' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { HelpCircle, Pencil, Trash2 } from 'lucide-react' +import { Katex } from '@/extensions' +import { deleteNode } from '@/utils/delete-node' +import { useAttributes } from '@/hooks/useAttributes' +import type { IKatexAttrs } from '@/extensions/Katex' +import { Textarea } from '@/components/ui/textarea' +import { ActionButton } from '@/components/ActionButton' +import { Button } from '@/components/ui' + +function BubbleMenuKatex({ editor, ...props }: any) { + const attrs = useAttributes(editor, Katex.name, { + text: '', + defaultShowPicker: false, + }) + const { text, defaultShowPicker } = attrs + const ref: any = useRef() + const [visible, toggleVisible] = useState(false) + + const shouldShow = useCallback(() => editor.isActive(Katex.name), [editor]) + + const deleteMe = useCallback(() => deleteNode(Katex.name, editor), [editor]) + + const submit = useCallback(() => { + editor.chain().focus().setKatex({ text: ref.current.value }).run() + }, [editor]) + + useEffect(() => { + if (defaultShowPicker) { + toggleVisible(true) + editor.chain().updateAttributes(Katex.name, { defaultShowPicker: false }).focus().run() + } + }, [editor, defaultShowPicker, toggleVisible]) + + useEffect(() => { + if (visible) { + setTimeout(() => ref.current?.focus(), 200) + } + }, [visible]) + + return ( + { + toggleVisible(false) + }, + }} + > + {props?.disabled + ? ( + <> + ) + : ( +
+ {visible + ? ( + <> +