├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── release.yml │ ├── preview-playground.yml │ └── preview-label.yml ├── playground ├── .env.exmaple ├── README.md ├── src │ ├── vite-env.d.ts │ ├── types │ │ ├── global.d.ts │ │ └── env.d.ts │ ├── components │ │ └── Hello │ │ │ └── Hello.tsx │ ├── main.tsx │ └── global.css ├── postcss.config.js ├── tailwind.config.js ├── .gitignore ├── index.html ├── tsconfig.json ├── vite.config.ts ├── package.json └── public │ └── vite.svg ├── src ├── locale-bundle.ts ├── bubble.ts ├── extensions │ ├── Bold │ │ ├── index.ts │ │ ├── Bold.ts │ │ └── components │ │ │ └── RichTextBold.tsx │ ├── Clear │ │ ├── index.ts │ │ ├── Clear.ts │ │ └── components │ │ │ └── RichTextClear.tsx │ ├── Code │ │ ├── index.ts │ │ ├── Code.ts │ │ └── components │ │ │ └── RichTextCode.tsx │ ├── Color │ │ ├── index.ts │ │ └── components │ │ │ └── RichTextColor.tsx │ ├── Emoji │ │ └── index.ts │ ├── Image │ │ ├── index.ts │ │ └── store.ts │ ├── Katex │ │ ├── index.ts │ │ └── components │ │ │ └── KatexWrapper.tsx │ ├── Link │ │ ├── index.ts │ │ └── components │ │ │ ├── LinkViewBlock.tsx │ │ │ └── RichTextLink.tsx │ ├── Table │ │ ├── index.ts │ │ └── components │ │ │ └── RichTextTable.tsx │ ├── Video │ │ ├── index.ts │ │ └── store.ts │ ├── CodeView │ │ ├── index.ts │ │ └── components │ │ │ └── RichTextCodeView.tsx │ ├── Column │ │ ├── index.ts │ │ └── components │ │ │ └── RichTextColumn.tsx │ ├── Drawer │ │ ├── index.ts │ │ └── components │ │ │ └── ControlDrawer │ │ │ └── ControlDrawer.module.scss │ ├── Heading │ │ └── index.ts │ ├── History │ │ ├── index.ts │ │ ├── History.ts │ │ └── components │ │ │ └── RichTextHistory.tsx │ ├── Iframe │ │ ├── index.ts │ │ └── components │ │ │ ├── index.module.scss │ │ │ └── RichTextIframe.tsx │ ├── Indent │ │ ├── index.ts │ │ └── components │ │ │ └── RichTextIndent.tsx │ ├── Italic │ │ ├── index.ts │ │ ├── Italic.ts │ │ └── components │ │ │ └── RichTextItalic.tsx │ ├── Mention │ │ └── index.ts │ ├── Mermaid │ │ └── index.ts │ ├── Strike │ │ ├── index.ts │ │ ├── Strike.ts │ │ └── components │ │ │ └── RichTextStrike.tsx │ ├── Twitter │ │ ├── index.ts │ │ └── components │ │ │ ├── NodeViewTweet.tsx │ │ │ └── RichTextTwitter.tsx │ ├── Attachment │ │ ├── index.ts │ │ └── components │ │ │ ├── NodeViewAttachment │ │ │ └── index.module.scss │ │ │ └── RichTextAttachment.tsx │ ├── Blockquote │ │ ├── index.ts │ │ ├── components │ │ │ └── RichTextBlockquote.tsx │ │ └── Blockquote.ts │ ├── BulletList │ │ ├── index.ts │ │ ├── components │ │ │ └── RichTextBulletList.tsx │ │ └── BulletList.ts │ ├── CodeBlock │ │ ├── index.ts │ │ └── components │ │ │ ├── NodeViewCodeBlock │ │ │ └── index.module.scss │ │ │ └── RichTextCodeBlock.tsx │ ├── Excalidraw │ │ ├── index.ts │ │ └── components │ │ │ └── NodeViewExcalidraw │ │ │ └── index.module.scss │ ├── ExportPdf │ │ ├── index.ts │ │ ├── components │ │ │ └── RichTextExportPdf.tsx │ │ └── ExportPdf.ts │ ├── ExportWord │ │ ├── index.ts │ │ └── components │ │ │ └── RichTextExportWord.tsx │ ├── FontFamily │ │ └── index.ts │ ├── FontSize │ │ └── index.ts │ ├── Highlight │ │ ├── index.ts │ │ └── components │ │ │ └── RichTextHighlight.tsx │ ├── ImageGif │ │ └── index.ts │ ├── ImportWord │ │ ├── index.ts │ │ └── ImportWord.ts │ ├── LineHeight │ │ ├── index.ts │ │ └── components │ │ │ └── RichTextLightHeight.tsx │ ├── MoreMark │ │ └── index.ts │ ├── TaskList │ │ ├── index.ts │ │ ├── components │ │ │ └── RichTextTaskList.tsx │ │ └── TaskList.ts │ ├── TextAlign │ │ └── index.ts │ ├── OrderedList │ │ ├── index.ts │ │ ├── components │ │ │ └── RichTextOrderedList.tsx │ │ └── OrderedList.ts │ ├── SlashCommand │ │ ├── index.ts │ │ ├── types.ts │ │ └── components │ │ │ └── SlashCommandList.tsx │ ├── HorizontalRule │ │ ├── index.ts │ │ ├── components │ │ │ └── RichTextHorizontalRule.tsx │ │ └── HorizontalRule.ts │ ├── TextDirection │ │ ├── index.ts │ │ └── TextDirection.ts │ ├── TextUnderline │ │ ├── index.ts │ │ ├── TextUnderline.ts │ │ └── components │ │ │ └── RichTextUnderline.tsx │ └── SearchAndReplace │ │ └── index.ts ├── index.ts ├── vite-env.d.ts ├── styles │ ├── index.scss │ ├── mention.scss │ ├── global.scss │ └── columns.scss ├── utils │ ├── shortId.ts │ ├── json.ts │ ├── customEvents │ │ ├── events.constant.ts │ │ └── customEvents.ts │ ├── storage.ts │ ├── download.ts │ ├── base64.ts │ ├── updatePosition.ts │ ├── editor-container-size.ts │ ├── getRenderContainer.ts │ ├── delete-node.ts │ ├── color.ts │ ├── plateform.ts │ ├── _event.ts │ └── is-mobile.ts ├── lib │ └── utils.ts ├── components │ ├── index.ts │ ├── icons │ │ ├── SizeL.tsx │ │ ├── CodeView.tsx │ │ ├── Direction.tsx │ │ ├── ColumnAddRight.tsx │ │ ├── ColumnAddLeft.tsx │ │ ├── index.ts │ │ ├── Twitter.tsx │ │ ├── SizeM.tsx │ │ ├── NoFill.tsx │ │ ├── SizeS.tsx │ │ ├── LineHeight.tsx │ │ ├── AspectRatio.tsx │ │ ├── Icon.tsx │ │ ├── Flag.tsx │ │ ├── FileWordOutline.tsx │ │ ├── Mermaid.tsx │ │ ├── Blockquote.tsx │ │ ├── ExportPdf.tsx │ │ ├── LeftToRight.tsx │ │ ├── RightToLeft.tsx │ │ ├── MenuDown.tsx │ │ ├── ImportWord.tsx │ │ ├── Html.tsx │ │ ├── GIfIcon.tsx │ │ ├── Activity.tsx │ │ ├── DeleteRow.tsx │ │ ├── DeleteColumn.tsx │ │ ├── Food.tsx │ │ ├── Object.tsx │ │ ├── Travel.tsx │ │ ├── ExportWord.tsx │ │ ├── Symbol.tsx │ │ └── Animas.tsx │ ├── ui │ │ ├── index.ts │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── toaster.tsx │ │ ├── input.tsx │ │ ├── tooltip.tsx │ │ ├── checkbox.tsx │ │ ├── switch.tsx │ │ ├── popover.tsx │ │ └── toggle.tsx │ ├── Bubble │ │ └── index.ts │ └── RichTextProvider.tsx ├── store │ ├── commandList.ts │ ├── store.ts │ ├── fast-context.tsx │ └── editor.ts └── hooks │ ├── useExtension.tsx │ ├── useCopy.tsx │ ├── useButtonProps.ts │ └── useAttributes.tsx ├── docs ├── public │ ├── og.png │ ├── logo.png │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ └── android-chrome-512x512.png ├── vercel.json ├── .vitepress │ ├── i18n │ │ ├── composable.ts │ │ ├── locales.ts │ │ └── utils.ts │ ├── plugins │ │ ├── changelog.ts │ │ └── contributors.ts │ ├── theme │ │ └── index.ts │ ├── components │ │ ├── HomeContributors.vue │ │ ├── Contributors.vue │ │ └── HomePage.vue │ └── transformHead.ts ├── tsconfig.json ├── guide │ ├── custom-theme.md │ ├── toolbar.md │ └── getting-started.md ├── package.json ├── index.md ├── vite.config.ts └── extensions │ ├── Clear │ └── index.md │ ├── Emoji │ └── index.md │ ├── Iframe │ └── index.md │ ├── ExportWord │ └── index.md │ ├── CodeView │ └── index.md │ ├── Drawer │ └── index.md │ ├── SlashCommand │ └── index.md │ ├── Excalidraw │ └── index.md │ ├── Link │ └── index.md │ └── Table │ └── index.md ├── pnpm-workspace.yaml ├── screenshot ├── screenshot.png └── contributor-circles.png ├── postcss.config.js ├── components.json ├── index.html ├── contributorkit.config.ts ├── .gitignore ├── eslint.config.js ├── tsconfig.json ├── scripts ├── genExtensions.ts ├── gen-docs-nav-extension.ts ├── contributors.ts └── modifyCss.ts ├── LICENSE └── CONTRIBUTING.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [hunghg255] 2 | -------------------------------------------------------------------------------- /playground/.env.exmaple: -------------------------------------------------------------------------------- 1 | VITE_GIPHY_API_KEY= 2 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | # Vite + React + TS 2 | 3 | -------------------------------------------------------------------------------- /src/locale-bundle.ts: -------------------------------------------------------------------------------- 1 | export * from './locales'; 2 | -------------------------------------------------------------------------------- /src/bubble.ts: -------------------------------------------------------------------------------- 1 | export * from './components/Bubble'; 2 | -------------------------------------------------------------------------------- /src/extensions/Bold/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Bold'; 2 | -------------------------------------------------------------------------------- /src/extensions/Clear/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Clear'; 2 | -------------------------------------------------------------------------------- /src/extensions/Code/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Code'; 2 | -------------------------------------------------------------------------------- /src/extensions/Color/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Color'; 2 | -------------------------------------------------------------------------------- /src/extensions/Emoji/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Emoji'; 2 | -------------------------------------------------------------------------------- /src/extensions/Image/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Image'; 2 | -------------------------------------------------------------------------------- /src/extensions/Katex/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Katex'; 2 | -------------------------------------------------------------------------------- /src/extensions/Link/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Link'; 2 | -------------------------------------------------------------------------------- /src/extensions/Table/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Table'; 2 | -------------------------------------------------------------------------------- /src/extensions/Video/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Video'; 2 | -------------------------------------------------------------------------------- /src/extensions/CodeView/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CodeView'; -------------------------------------------------------------------------------- /src/extensions/Column/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Column'; 2 | -------------------------------------------------------------------------------- /src/extensions/Drawer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Drawer'; 2 | -------------------------------------------------------------------------------- /src/extensions/Heading/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Heading'; 2 | -------------------------------------------------------------------------------- /src/extensions/History/index.ts: -------------------------------------------------------------------------------- 1 | export * from './History'; 2 | -------------------------------------------------------------------------------- /src/extensions/Iframe/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Iframe'; 2 | -------------------------------------------------------------------------------- /src/extensions/Indent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Indent'; 2 | -------------------------------------------------------------------------------- /src/extensions/Italic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Italic'; 2 | -------------------------------------------------------------------------------- /src/extensions/Mention/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Mention'; 2 | -------------------------------------------------------------------------------- /src/extensions/Mermaid/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Mermaid'; 2 | -------------------------------------------------------------------------------- /src/extensions/Strike/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Strike'; 2 | -------------------------------------------------------------------------------- /src/extensions/Twitter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Twitter'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@/components/RichTextProvider'; 2 | -------------------------------------------------------------------------------- /src/extensions/Attachment/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Attachment'; 2 | -------------------------------------------------------------------------------- /src/extensions/Blockquote/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Blockquote'; 2 | -------------------------------------------------------------------------------- /src/extensions/BulletList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BulletList'; 2 | -------------------------------------------------------------------------------- /src/extensions/CodeBlock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CodeBlock'; 2 | -------------------------------------------------------------------------------- /src/extensions/Excalidraw/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Excalidraw'; 2 | -------------------------------------------------------------------------------- /src/extensions/ExportPdf/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ExportPdf'; 2 | -------------------------------------------------------------------------------- /src/extensions/ExportWord/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ExportWord'; 2 | -------------------------------------------------------------------------------- /src/extensions/FontFamily/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FontFamily'; 2 | -------------------------------------------------------------------------------- /src/extensions/FontSize/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FontSize'; 2 | -------------------------------------------------------------------------------- /src/extensions/Highlight/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Highlight'; 2 | -------------------------------------------------------------------------------- /src/extensions/ImageGif/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ImageGif'; 2 | -------------------------------------------------------------------------------- /src/extensions/ImportWord/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ImportWord'; 2 | -------------------------------------------------------------------------------- /src/extensions/LineHeight/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LineHeight'; 2 | -------------------------------------------------------------------------------- /src/extensions/MoreMark/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MoreMark'; 2 | -------------------------------------------------------------------------------- /src/extensions/TaskList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TaskList'; 2 | -------------------------------------------------------------------------------- /src/extensions/TextAlign/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextAlign'; 2 | -------------------------------------------------------------------------------- /src/extensions/OrderedList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './OrderedList'; 2 | -------------------------------------------------------------------------------- /src/extensions/SlashCommand/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SlashCommand'; 2 | -------------------------------------------------------------------------------- /src/extensions/HorizontalRule/index.ts: -------------------------------------------------------------------------------- 1 | export * from './HorizontalRule'; 2 | -------------------------------------------------------------------------------- /src/extensions/TextDirection/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextDirection'; 2 | -------------------------------------------------------------------------------- /src/extensions/TextUnderline/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextUnderline'; 2 | -------------------------------------------------------------------------------- /src/extensions/SearchAndReplace/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SearchAndReplace'; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare const process; 4 | -------------------------------------------------------------------------------- /docs/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/docs/public/og.png -------------------------------------------------------------------------------- /playground/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare const process; 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check: true 2 | packages: 3 | - playground 4 | - docs 5 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /screenshot/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/screenshot/screenshot.png -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './global'; 2 | @use './editor'; 3 | @use './ProseMirror'; 4 | @use './columns'; 5 | @use './mention'; 6 | -------------------------------------------------------------------------------- /screenshot/contributor-circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/screenshot/contributor-circles.png -------------------------------------------------------------------------------- /docs/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/docs/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor/HEAD/docs/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /playground/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | ENV: typeof process.env; 4 | } 5 | } 6 | export {}; 7 | -------------------------------------------------------------------------------- /src/styles/mention.scss: -------------------------------------------------------------------------------- 1 | .mention { 2 | padding: 2px 6px; 3 | color: #fff; 4 | background-color: #666e76; 5 | border-radius: 6px; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/shortId.ts: -------------------------------------------------------------------------------- 1 | export function shortId(length = 8) { 2 | return Math.random() 3 | .toString(36) 4 | .substring(2, length + 2); 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | 'tailwindcss': {}, 5 | 'autoprefixer': {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /playground/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "tailwindcss/nesting": {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /playground/src/types/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | MAIN_SERVICE_BASE_URL: string; 4 | API_APP: string; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "rewrites": [ 4 | { 5 | "source": "/:path*", 6 | "destination": "/:path*.html" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | // asd 8 | -------------------------------------------------------------------------------- /playground/src/components/Hello/Hello.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Hello = () => { 4 | return ( 5 | <> 6 |

Hello Hello

7 | 8 | ); 9 | }; 10 | 11 | export default Hello; 12 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActionButton'; 2 | export * from './ActionMenuButton'; 3 | export * from './ColorPicker'; 4 | export * from './RichTextProvider'; 5 | export * from './icons'; 6 | export * from './ui'; 7 | -------------------------------------------------------------------------------- /docs/.vitepress/i18n/composable.ts: -------------------------------------------------------------------------------- 1 | import { useData } from 'vitepress' 2 | import { t } from './utils' 3 | 4 | export function useTranslate(lang?: string) { 5 | return (key: string) => t(key, lang || useData().lang.value) 6 | } 7 | -------------------------------------------------------------------------------- /playground/src/main.tsx: -------------------------------------------------------------------------------- 1 | import './global.css' 2 | 3 | import React from 'react' 4 | 5 | import ReactDOM from 'react-dom/client' 6 | 7 | import App from './App' 8 | 9 | ReactDOM.createRoot(document.querySelector('#root') as HTMLElement).render() 10 | -------------------------------------------------------------------------------- /playground/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './pages/**/*.{ts,tsx}', 5 | './components/**/*.{ts,tsx}', 6 | './app/**/*.{ts,tsx}', 7 | './src/**/*.{ts,tsx}', 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /docs/.vitepress/i18n/locales.ts: -------------------------------------------------------------------------------- 1 | export const zhCN = { 2 | 'Guide': '指南', 3 | 4 | 'English': '简体中文', 5 | 'en': 'zh-CN', 6 | 7 | 'Loading...': '加载中…', 8 | } 9 | 10 | export const langMap: Record> = { 11 | 'zh-CN': zhCN, 12 | } 13 | -------------------------------------------------------------------------------- /docs/.vitepress/i18n/utils.ts: -------------------------------------------------------------------------------- 1 | import { langMap } from './locales' 2 | 3 | export function t(key: string, lang: string) { 4 | return langMap[lang]?.[key] || key 5 | } 6 | 7 | export function createTranslate(lang: string) { 8 | return (key: string) => t(key, lang) 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | // https://github.com/radix-ui/primitives/issues/1496 6 | html body[data-scroll-locked] { 7 | --removed-body-scroll-bar-size: 0 !important; 8 | // margin-right: 0 !important; 9 | position: initial !important; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/icons/SizeL.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react'; 2 | 3 | export function SizeL(props: SVGProps) { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/icons/CodeView.tsx: -------------------------------------------------------------------------------- 1 | export function CodeView() { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | } -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | .env 26 | -------------------------------------------------------------------------------- /src/components/icons/Direction.tsx: -------------------------------------------------------------------------------- 1 | export function Direction() { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/icons/ColumnAddRight.tsx: -------------------------------------------------------------------------------- 1 | export function ColumnAddRight() { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/store/commandList.ts: -------------------------------------------------------------------------------- 1 | import { CommandList } from '@/extensions/SlashCommand/types'; 2 | import { createSignal, useSignal } from 'reactjs-signal'; 3 | 4 | const signalCommandList = createSignal([]); 5 | 6 | export function useSignalCommandList () { 7 | const [commandList, setCommandList] = useSignal(signalCommandList); 8 | 9 | return [commandList, setCommandList] as const; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/icons/ColumnAddLeft.tsx: -------------------------------------------------------------------------------- 1 | export function ColumnAddLeft() { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AspectRatio'; 2 | export * from './Blockquote'; 3 | export * from './DeleteColumn'; 4 | export * from './DeleteRow'; 5 | export * from './FileWordOutline'; 6 | export * from './Icon'; 7 | export * from './LineHeight'; 8 | export * from './MenuDown'; 9 | export * from './SizeL'; 10 | export * from './SizeM'; 11 | export * from './SizeS'; 12 | export * from './icons'; 13 | -------------------------------------------------------------------------------- /src/components/icons/Twitter.tsx: -------------------------------------------------------------------------------- 1 | export function Twitter() { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/json.ts: -------------------------------------------------------------------------------- 1 | export function safeJSONParse(str: any, defaultValue = {}) { 2 | if (typeof str === 'object') 3 | return str; 4 | 5 | try { 6 | return JSON.parse(str); 7 | } catch { 8 | return defaultValue; 9 | } 10 | } 11 | 12 | export function safeJSONStringify(obj: any, defaultValue = '{}') { 13 | try { 14 | return JSON.stringify(obj); 15 | } catch { 16 | return defaultValue; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Tiptap Editor 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Tiptap Editor 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/icons/SizeM.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react'; 2 | 3 | export function SizeM(props: SVGProps) { 4 | return ( 5 | 6 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/icons/NoFill.tsx: -------------------------------------------------------------------------------- 1 | export function NoFill() { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './button'; 2 | export * from './dropdown-menu'; 3 | export * from './input'; 4 | export * from './label'; 5 | export * from './popover'; 6 | export * from './separator'; 7 | export * from './switch'; 8 | export * from './tabs'; 9 | export * from './toast'; 10 | export * from './toggle'; 11 | export * from './tooltip'; 12 | export * from './select'; 13 | export * from './use-toast'; 14 | export * from './checkbox'; 15 | -------------------------------------------------------------------------------- /docs/.vitepress/plugins/changelog.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | 3 | const ID = '/virtual-changelog' 4 | 5 | export function ChangeLog(data: any[]): Plugin { 6 | return { 7 | name: 'reactjs-tiptap-editor-changelog', 8 | resolveId(id) { 9 | return id === ID ? ID : null 10 | }, 11 | load(id) { 12 | if (id !== ID) 13 | return null 14 | return `export default ${JSON.stringify(data)}` 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /contributorkit.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'contributorkit' 2 | 3 | export default defineConfig({ 4 | outputDir: './screenshot', 5 | owner: 'hunghg255', 6 | repo: 'reactjs-tiptap-editor', 7 | renders: [ 8 | { 9 | name: 'contributor-wide', 10 | width: 1000, 11 | formats: ['svg'], 12 | }, 13 | { 14 | renderer: 'circles', 15 | name: 'contributor-circles', 16 | width: 1000, 17 | }, 18 | ], 19 | }) 20 | -------------------------------------------------------------------------------- /src/components/icons/SizeS.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react'; 2 | 3 | export function SizeS(props: SVGProps) { 4 | return ( 5 | 6 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/columns.scss: -------------------------------------------------------------------------------- 1 | .columns { 2 | display: flex; 3 | width: 100%; 4 | gap: 8px; 5 | margin-top: 0.75em; 6 | 7 | .column { 8 | min-width: 0; 9 | padding: 12px; 10 | border-width: 1px; 11 | border-style: solid; 12 | border-color: hsl(var(--border)); 13 | border-radius: 2px; 14 | flex: 1 1 0%; 15 | box-sizing: border-box; 16 | 17 | p { 18 | &:first-of-type { 19 | margin-top: 0; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/icons/LineHeight.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react'; 2 | 3 | export function FormatLineHeight(props: SVGProps) { 4 | return ( 5 | 6 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/customEvents/events.constant.ts: -------------------------------------------------------------------------------- 1 | export const EVENTS = { 2 | UPLOAD_IMAGE: (id: any) => `UPLOAD_IMAGE-${id}`, 3 | UPLOAD_VIDEO: (id: string) => `UPLOAD_VIDEO-${id}`, 4 | 5 | CHANGE_THEME: 'CHANGE_THEME', 6 | CHANGE_COLOR: 'CHANGE_COLOR', 7 | CHANGE_BORDER_RADIUS: 'CHANGE_BORDER_RADIUS', 8 | MODIFY_LANGUAGE: 'MODIFY_LANGUAGE', 9 | CHANGE_LANGUAGE: 'CHANGE_LANGUAGE', 10 | } as const; 11 | 12 | // type EventsType = typeof EVENTS; 13 | export type EventValues = any; 14 | -------------------------------------------------------------------------------- /docs/.vitepress/plugins/contributors.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | 3 | const ID = '/virtual-contributors' 4 | 5 | export function Contributors(data: Record): Plugin { 6 | return { 7 | name: 'reactjs-tiptap-editor-contributors', 8 | resolveId(id) { 9 | return id === ID ? ID : null 10 | }, 11 | load(id) { 12 | if (id !== ID) 13 | return null 14 | return `export default ${JSON.stringify(data)}` 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/useExtension.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | import { useEditorInstance } from '@/store/editor'; 4 | 5 | export function useExtension(extensionName: string) { 6 | const editor = useEditorInstance(); 7 | 8 | return useMemo(() => { 9 | if (!editor) { 10 | return null; 11 | } 12 | const extension = editor.extensionManager.extensions.find(extension => extension.name === extensionName); 13 | 14 | return extension; 15 | }, [editor, extensionName]); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/icons/AspectRatio.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react'; 2 | 3 | export function AspectRatio(props: SVGProps) { 4 | return ( 5 | 6 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/extensions/SlashCommand/types.ts: -------------------------------------------------------------------------------- 1 | import type { Editor, Range } from '@tiptap/core'; 2 | 3 | export interface CommandList { 4 | name: string 5 | title: string 6 | commands: Command[] 7 | } 8 | 9 | export interface Command { 10 | name: string 11 | label: string 12 | description?: string 13 | aliases?: string[] 14 | iconName?: any 15 | iconUrl?: string 16 | action: ({ editor, range }: { editor: Editor, range: Range }) => void 17 | shouldBeHidden?: (editor: Editor) => boolean 18 | } 19 | -------------------------------------------------------------------------------- /src/extensions/Image/store.ts: -------------------------------------------------------------------------------- 1 | import { useStoreUploadImage } from '@/store/store'; 2 | import { dispatchEvent } from '@/utils/customEvents/customEvents'; 3 | import { EVENTS } from '@/utils/customEvents/events.constant'; 4 | 5 | export function useDialogImage() { 6 | const [v] = useStoreUploadImage(store => store.value); 7 | 8 | return v; 9 | } 10 | 11 | export const actionDialogImage = { 12 | setOpen: (id: any, value: boolean) => { 13 | dispatchEvent(EVENTS.UPLOAD_IMAGE(id), value); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/extensions/Video/store.ts: -------------------------------------------------------------------------------- 1 | import { useStoreUploadVideo } from '@/store/store'; 2 | import { dispatchEvent } from '@/utils/customEvents/customEvents'; 3 | import { EVENTS } from '@/utils/customEvents/events.constant'; 4 | 5 | export function useDialogVideo() { 6 | const [v] = useStoreUploadVideo(store => store.value); 7 | 8 | return v; 9 | } 10 | 11 | export const actionDialogVideo = { 12 | setOpen: (id: any, value: boolean) => { 13 | dispatchEvent(EVENTS.UPLOAD_VIDEO(id), value); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/icons/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { icons } from '@/components'; 3 | 4 | export interface IconComponentProps { 5 | name: string 6 | className?: string 7 | onClick?: React.MouseEventHandler 8 | } 9 | 10 | function IconComponent(props: IconComponentProps) { 11 | const Icon = icons[props.name]; 12 | 13 | return Icon ? : null; 14 | } 15 | 16 | export { IconComponent }; 17 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | export function getStorage(key: any, defaultValue = null) { 2 | if (typeof window === 'undefined') 3 | // eslint-disable-next-line unicorn/error-message 4 | throw new Error(); 5 | 6 | const value = localStorage.getItem(key); 7 | if (!value) 8 | return defaultValue; 9 | try { 10 | return JSON.parse(value); 11 | } catch { 12 | return value; 13 | } 14 | } 15 | 16 | export function setStorage(key: any, value: any) { 17 | window.localStorage.setItem(key, `${value}`); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /lib 5 | examples/dist 6 | 7 | auto-imports.d.ts 8 | components.d.ts 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | .env 28 | .vercel 29 | 30 | docs/.vitepress/dist 31 | docs/.vitepress/node_modules 32 | docs/.vitepress/.temp 33 | docs/.vitepress/cache 34 | -------------------------------------------------------------------------------- /src/utils/download.ts: -------------------------------------------------------------------------------- 1 | const isBrowser = typeof window !== 'undefined'; 2 | 3 | export function downloadFromBlob(blob: Blob, filename: string) { 4 | if (isBrowser) { 5 | const url = window.URL.createObjectURL(blob); 6 | const a = document.createElement('a'); 7 | a.href = url; 8 | a.download = filename; 9 | a.click(); 10 | window.URL.revokeObjectURL(url); 11 | return Promise.resolve(); 12 | } 13 | 14 | console.error('Download is not supported in Node.js'); 15 | 16 | return Promise.resolve(); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/icons/Flag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Flag() { 4 | return ( 5 | 11 | 15 | 16 | ); 17 | } 18 | 19 | export default Flag; 20 | -------------------------------------------------------------------------------- /src/components/Bubble/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RichTextBubbleColumns'; 2 | export * from './RichTextBubbleDrawer'; 3 | export * from './RichTextBubbleExcalidraw'; 4 | export * from './RichTextBubbleIframe'; 5 | export * from './RichTextBubbleKatex'; 6 | export * from './RichTextBubbleLink'; 7 | export * from './RichTextBubbleMedia'; 8 | export * from './RichTextBubbleMermaid'; 9 | export * from './RichTextBubbleTable'; 10 | export * from './RichTextBubbleText'; 11 | export * from './RichTextBubbleTwitter'; 12 | export * from './RichTextBubbleMenuDragHandle'; 13 | -------------------------------------------------------------------------------- /src/components/icons/FileWordOutline.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react'; 2 | 3 | export function FileWordOutline(props: SVGProps) { 4 | return ( 5 | 6 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/base64.ts: -------------------------------------------------------------------------------- 1 | export function base64ToBlob(base64: any, mimeType: any) { 2 | const byteCharacters = atob(base64.split(',')[1]); 3 | const byteNumbers = Array.from({ length: byteCharacters.length }); 4 | for (let i = 0; i < byteCharacters.length; i++) { 5 | byteNumbers[i] = byteCharacters.charCodeAt(i); 6 | } 7 | const byteArray = new Uint8Array(byteNumbers as any); 8 | return new Blob([byteArray], { type: mimeType }); 9 | } 10 | 11 | export function blobToFile(blob: any, fileName: any) { 12 | return new File([blob], fileName, { type: blob.type }); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/icons/Mermaid.tsx: -------------------------------------------------------------------------------- 1 | export function Mermaid() { 2 | return ( 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/customEvents/customEvents.ts: -------------------------------------------------------------------------------- 1 | import { type EventValues } from '@/utils/customEvents/events.constant'; 2 | 3 | export function listenEvent (eventName: EventValues, callback: any) { 4 | window.addEventListener(eventName, callback); 5 | 6 | return () => { 7 | window.removeEventListener(eventName, callback); 8 | }; 9 | } 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | export function dispatchEvent (eventName: EventValues, detail?: any) { 13 | window.dispatchEvent( 14 | new CustomEvent(eventName, { 15 | detail, 16 | }), 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useCopy.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | function useCopy() { 4 | const [isCopied, setIsCopied] = useState(false); 5 | 6 | const copyToClipboard = async (text: string) => { 7 | try { 8 | await navigator.clipboard.writeText(text); 9 | setIsCopied(true); 10 | setTimeout(() => setIsCopied(false), 2000); // Reset the copy status after 2 seconds 11 | } catch (error) { 12 | console.error('Failed to copy text: ', error); 13 | setIsCopied(false); 14 | } 15 | }; 16 | 17 | return { isCopied, copyToClipboard }; 18 | } 19 | 20 | export default useCopy; 21 | -------------------------------------------------------------------------------- /src/components/icons/Blockquote.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react'; 2 | 3 | export function BlockquoteLeft(props: SVGProps) { 4 | return ( 5 | 6 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/icons/ExportPdf.tsx: -------------------------------------------------------------------------------- 1 | export function ExportPdf() { 2 | return ( 3 | 4 | 5 | 6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/icons/LeftToRight.tsx: -------------------------------------------------------------------------------- 1 | export function LeftToRight() { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/components/icons/RightToLeft.tsx: -------------------------------------------------------------------------------- 1 | export function RightToLeft() { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/extensions/Attachment/components/NodeViewAttachment/index.module.scss: -------------------------------------------------------------------------------- 1 | .attachment, /* for browser ready HTML */ 2 | .wrap { /* for NodeView */ 3 | border-width: 1px !important; 4 | border-radius: var(--radius) !important; 5 | border-color: hsl(var(--border)) !important; 6 | padding: 8px; 7 | display: flex; 8 | align-items: center; 9 | justify-content: space-between; 10 | margin: 8px 0; 11 | 12 | :global { 13 | .attachment__icon { 14 | width: 32px; 15 | text-align: center; 16 | } 17 | 18 | .attachment__icon svg { 19 | width: 32px; 20 | display: inline-block; 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/components/icons/MenuDown.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from 'react'; 2 | 3 | export function MenuDown(props: SVGProps) { 4 | return ( 5 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { basic, react } from '@hunghg255/eslint-config'; 2 | import tailwind from 'eslint-plugin-tailwindcss'; 3 | 4 | export default [ 5 | ...basic(), 6 | ...react(), 7 | ...tailwind.configs['flat/recommended'], 8 | { 9 | rules: { 10 | indent: 'off', 11 | "@typescript-eslint/unbound-method": "off", 12 | quotes: ["error", "single"] 13 | }, 14 | }, 15 | { 16 | ignores: [ 17 | 'dist/**/*.ts', 18 | 'dist/**', 19 | 'contributorkit.config.ts', 20 | 'tailwind.config.js', 21 | 'postcss.config.js', 22 | 'animate.js', 23 | 'playground/**' 24 | ], 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /src/components/icons/ImportWord.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function ImportWord() { 4 | return ( 5 | 19 | 20 | ); 21 | } 22 | 23 | export default ImportWord; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["src", "lib"] 24 | } 25 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "types": ["node"], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /scripts/genExtensions.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import { globby } from 'globby'; 3 | 4 | (async () => { 5 | const files = await globby('src/extensions/**/*.ts', { 6 | ignore: ['src/**/*/index.ts', 'src/**/*.spec.ts'], // Exclude .spec.ts files 7 | }) 8 | 9 | const newFile = files.map((v: any) => { 10 | const vv = v.replace('src/', '') 11 | const [, _name, i] = vv.split('/') 12 | 13 | return { 14 | name: _name === 'BaseKit.ts' ? 'BaseKit.ts' : `${_name}.ts`, 15 | package: _name, 16 | alias: i ? [i.replace('.ts', '')] : _name === 'BaseKit.ts' ? ['BaseKit'] : [], 17 | } 18 | }) 19 | 20 | fs.writeFileSync('./scripts/extensions.json', JSON.stringify(newFile, null, 2)) 21 | })() 22 | -------------------------------------------------------------------------------- /scripts/gen-docs-nav-extension.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { cwd } from 'node:process' 4 | import { globbySync } from 'globby' 5 | 6 | async function genDocsNavExtension() { 7 | try { 8 | const files = await globbySync('docs/extensions/**/*.md'); 9 | 10 | const navItems = files.map((file) => { 11 | const parts = file.split('/'); 12 | const name = parts[2]; // Get the extension name from the path 13 | return { 14 | text: name, 15 | link: `/extensions/${name}/index.md`, 16 | }; 17 | }); 18 | console.log(navItems); 19 | 20 | } 21 | catch { 22 | console.error('Failed to modify CSS!') 23 | } 24 | } 25 | 26 | genDocsNavExtension() 27 | -------------------------------------------------------------------------------- /playground/src/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .collaboration-carets__caret { 6 | border-left: 1px solid #0d0d0d; 7 | border-right: 1px solid #0d0d0d; 8 | margin-left: -1px; 9 | margin-right: -1px; 10 | pointer-events: none; 11 | position: relative; 12 | word-break: normal; 13 | } 14 | 15 | .collaboration-carets__label { 16 | border-radius: 3px 3px 3px 0; 17 | color: #0d0d0d; 18 | font-size: 12px; 19 | font-style: normal; 20 | font-weight: 600; 21 | left: -1px; 22 | line-height: normal; 23 | padding: 0.1rem 0.3rem; 24 | position: absolute; 25 | top: -1.4em; 26 | user-select: none; 27 | white-space: nowrap; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/updatePosition.ts: -------------------------------------------------------------------------------- 1 | import { computePosition, flip, shift } from '@floating-ui/dom'; 2 | import { posToDOMRect } from '@tiptap/react'; 3 | 4 | export function updatePosition (editor: any, element: any) { 5 | const virtualElement = { 6 | getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to), 7 | }; 8 | 9 | computePosition(virtualElement, element, { 10 | placement: 'bottom-start', 11 | strategy: 'absolute', 12 | middleware: [shift(), flip()], 13 | }).then(({ x, y, strategy }) => { 14 | element.style.width = 'max-content'; 15 | element.style.position = strategy; 16 | element.style.left = `${x}px`; 17 | element.style.top = `${y}px`; 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "customConditions": ["dev"], 8 | "module": "esnext", 9 | "moduleResolution": "bundler", 10 | "paths": { 11 | "~/*": ["src/*"] 12 | }, 13 | "resolveJsonModule": true, 14 | "types": ["vite/client", "vitepress"], 15 | "strict": true, 16 | "strictNullChecks": true, 17 | "noUnusedLocals": true, 18 | "esModuleInterop": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "skipLibCheck": true 21 | }, 22 | "vueCompilerOptions": {}, 23 | "include": ["**/*", "./.*/**/*", "../packages/shim.d.ts"], 24 | "exclude": ["dist", "node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from 'vitepress/theme' 2 | // import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' 3 | import type { EnhanceAppContext } from 'vitepress' 4 | import './style.css' 5 | // import '@nolebase/vitepress-plugin-git-changelog/client/style.css' 6 | // import '@shikijs/vitepress-twoslash/style.css' 7 | import Contributors from '../components/Contributors.vue' 8 | import Changelog from '../components/Changelog.vue' 9 | import Layout from './Layout.vue' 10 | import 'uno.css' 11 | 12 | export default { 13 | ...Theme, 14 | Layout, 15 | enhanceApp({ app }: EnhanceAppContext) { 16 | app.component('Contributors', Contributors) 17 | app.component('Changelog', Changelog) 18 | 19 | // app.use(TwoslashFloatingVue) 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /docs/.vitepress/components/HomeContributors.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/components/icons/Html.tsx: -------------------------------------------------------------------------------- 1 | export function Html() { 2 | return ( 3 | 4 | {/* Icon from Huge Icons by Hugeicons - undefined */} 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /docs/guide/custom-theme.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Custom Theme 3 | 4 | next: 5 | text: Attachment 6 | link: /extensions/Attachment.md 7 | --- 8 | 9 | # Custom Theme 10 | 11 | - The editor allows you to create and apply custom themes to change its appearance according to your preferences. 12 | 13 | - Reference shacne's custom theme implementation: [https://ui.shadcn.com/themes](https://ui.shadcn.com/themes) 14 | 15 | ## Usage 16 | 17 | ```javascript 18 | import { themeActions } from 'reactjs-tiptap-editor/theme' 19 | 20 | // Set theme 21 | themeActions.setTheme('light') // or 'dark'; 22 | 23 | // Set color 24 | themeActions.setColor('default') // "red" | "blue" | "green" | "orange" | "rose" | "violet" | "yellow" 25 | 26 | // Set radius 27 | themeActions.setRadius('0.5rem') // any valid CSS border-radius value 28 | ``` 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | ci: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install pnpm 23 | uses: pnpm/action-setup@v4 24 | 25 | - name: Set node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: lts/* 29 | 30 | - name: Install 31 | run: pnpm install 32 | 33 | - name: Lint 34 | run: pnpm lint 35 | 36 | - name: Typecheck 37 | run: pnpm type-check 38 | 39 | - name: Build lib 40 | run: pnpm build:lib 41 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reactjs-tiptap-editor/docs", 3 | "type": "module", 4 | "version": "1.0.7", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vitepress dev", 8 | "build": "vitepress build", 9 | "preview": "vitepress preview" 10 | }, 11 | "devDependencies": { 12 | "@nolebase/vitepress-plugin-enhanced-mark": "^2.4.0", 13 | "@nolebase/vitepress-plugin-enhanced-readabilities": "^2.4.0", 14 | "@nolebase/vitepress-plugin-highlight-targeted-heading": "^2.4.0", 15 | "@shikijs/transformers": "^1.13.0", 16 | "@shikijs/vitepress-twoslash": "^1.13.0", 17 | "@vitejs/plugin-vue-jsx": "^4.0.1", 18 | "prettier": "^2.8.8", 19 | "sharp": "^0.33.4", 20 | "shiki": "^1.13.0", 21 | "unocss": "^0.62.1", 22 | "vite": "^5.4.0", 23 | "vitepress": "^1.3.2", 24 | "vue": "^3.4.37" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/store/store.ts: -------------------------------------------------------------------------------- 1 | import createFastContext from '@/store/fast-context'; 2 | 3 | const { Provider: ProviderUploadImage, useStore: useStoreUploadImage } = createFastContext({ 4 | value: false 5 | }); 6 | 7 | const { Provider: ProviderUploadVideo, useStore: useStoreUploadVideo } = createFastContext({ 8 | value: false 9 | }); 10 | 11 | const { Provider: ProviderEditableEditor, useStore: useStoreEditableEditor } = createFastContext({ 12 | value: false 13 | }); 14 | 15 | function useEditableEditor () { 16 | const [isEditableEditor] = useStoreEditableEditor(store => store.value); 17 | 18 | return isEditableEditor; 19 | } 20 | 21 | export { 22 | ProviderUploadImage, 23 | useStoreUploadImage, 24 | 25 | ProviderUploadVideo, 26 | useStoreUploadVideo, 27 | 28 | ProviderEditableEditor, 29 | useStoreEditableEditor, 30 | useEditableEditor, 31 | }; 32 | -------------------------------------------------------------------------------- /src/extensions/Iframe/components/index.module.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | display: flex; 3 | height: 100%; 4 | max-width: 100%; 5 | overflow: hidden; 6 | line-height: 0; 7 | flex-direction: column; 8 | border: 1px dashed hsl(var(--border)) !important; 9 | border-radius: 6px; 10 | 11 | .handlerWrap { 12 | display: flex; 13 | padding: 10px; 14 | } 15 | 16 | .innerWrap { 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | overflow: hidden; 21 | border-radius: var(--border-radius); 22 | flex: 1; 23 | } 24 | 25 | .emptyWrap { 26 | display: flex; 27 | height: 100%; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | 32 | :global { 33 | iframe { 34 | width: 100%; 35 | height: 100%; 36 | border: 0; 37 | border: none !important; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/editor-container-size.ts: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | 3 | const cache = new Map(); 4 | 5 | export function getEditorContainerDOMSize(editor: Editor): { width: number } { 6 | const targetNode = editor.options.element as HTMLElement; 7 | 8 | if (!cache.has('width')) { 9 | cache.set('width', targetNode.clientWidth); 10 | } 11 | 12 | if (cache.has('width') && cache.get('width') <= 0) { 13 | cache.set('width', targetNode.clientWidth); 14 | } 15 | 16 | const config = { attributes: true, childList: true, subtree: true }; 17 | const callback = function () { 18 | cache.set('width', targetNode.clientWidth); 19 | }; 20 | const observer = new MutationObserver(callback); 21 | observer.observe(targetNode, config); 22 | 23 | editor.on('destroy', () => { 24 | observer.disconnect(); 25 | }); 26 | 27 | return { width: cache.get('width') }; 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/useButtonProps.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | 3 | import { useExtension } from '@/hooks/useExtension'; 4 | import { useLocale } from '@/locales'; 5 | import { useEditorInstance } from '@/store/editor'; 6 | import { isFunction } from '@/utils/utils'; 7 | 8 | export function useButtonProps(extensionName: string) { 9 | const editor = useEditorInstance(); 10 | const extension = useExtension(extensionName); 11 | const { t } = useLocale(); 12 | 13 | return useMemo(() => { 14 | if (!editor || !extension) { 15 | return null; 16 | } 17 | 18 | const { 19 | button, 20 | } = extension.options; 21 | 22 | if (!button || !isFunction(button)) { 23 | return null; 24 | } 25 | 26 | const buttonProps = button({ 27 | editor, 28 | extension, 29 | t, 30 | }); 31 | 32 | return buttonProps; 33 | }, [editor, extension, t]); 34 | } 35 | -------------------------------------------------------------------------------- /docs/.vitepress/transformHead.ts: -------------------------------------------------------------------------------- 1 | import type { HeadConfig, TransformContext } from 'vitepress' 2 | 3 | export async function transformHead({ pageData }: TransformContext) { 4 | const head: HeadConfig[] = [] 5 | 6 | if (pageData.relativePath === 'index.md') { 7 | head.push( 8 | [ 9 | 'meta', 10 | { property: 'og:image', content: 'https://reactjs-tiptap-editor.vercel.app/og.png' }, 11 | ], 12 | [ 13 | 'meta', 14 | { property: 'twitter:image', content: 'https://reactjs-tiptap-editor.vercel.app/og.png' }, 15 | ], 16 | ) 17 | return head 18 | } 19 | 20 | head.push( 21 | ['meta', { property: 'og:image', content: 'https://reactjs-tiptap-editor.vercel.app/og.png' }], 22 | [ 23 | 'meta', 24 | { property: 'twitter:image', content: 'https://reactjs-tiptap-editor.vercel.app/og.png' }, 25 | ], 26 | ) 27 | 28 | return head 29 | } 30 | -------------------------------------------------------------------------------- /scripts/contributors.ts: -------------------------------------------------------------------------------- 1 | import cache from '../screenshot/.cache.json'; 2 | 3 | const users = cache.map((item) => item.login); 4 | 5 | export interface Contributor { 6 | name: string 7 | avatar: string 8 | } 9 | 10 | export interface CoreTeam { 11 | avatar: string 12 | name: string 13 | github: string 14 | twitter?: string 15 | sponsors?: boolean 16 | description: string 17 | packages?: string[] 18 | functions?: string[] 19 | } 20 | 21 | const contributorsAvatars: Record = {} 22 | 23 | function getAvatarUrl(name: string) { 24 | return `https://avatars.githubusercontent.com/${name}?v=4` 25 | } 26 | 27 | const contributorList = (users as string[]).reduce((acc, name) => { 28 | contributorsAvatars[name] = getAvatarUrl(name) 29 | acc.push({ name, avatar: contributorsAvatars[name] }) 30 | return acc 31 | }, [] as Contributor[]) 32 | 33 | export { contributorList as contributors } 34 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as LabelPrimitive from '@radix-ui/react-label'; 6 | import { type VariantProps, cva } from 'class-variance-authority'; 7 | 8 | import { cn } from '@/lib/utils'; 9 | 10 | const labelVariants = cva( 11 | 'richtext-text-sm richtext-font-medium richtext-leading-none richtext-text-foreground peer-disabled:richtext-cursor-not-allowed peer-disabled:richtext-opacity-70', 12 | ); 13 | 14 | const Label = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef & 17 | VariantProps 18 | >(({ className, ...props }, ref) => ( 19 | 24 | )); 25 | Label.displayName = LabelPrimitive.Root.displayName; 26 | 27 | export { Label }; 28 | -------------------------------------------------------------------------------- /src/extensions/Clear/Clear.ts: -------------------------------------------------------------------------------- 1 | import { Node } from '@tiptap/core'; 2 | 3 | import type { GeneralOptions } from '@/types'; 4 | 5 | export interface ClearOptions extends GeneralOptions {} 6 | 7 | export * from './components/RichTextClear'; 8 | 9 | export const Clear = /* @__PURE__ */ Node.create({ 10 | name: 'clear', 11 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 12 | //@ts-expect-error 13 | addOptions() { 14 | return { 15 | ...this.parent?.(), 16 | button: ({ editor, t }) => ({ 17 | // component: ActionButton, 18 | componentProps: { 19 | action: () => editor.chain().focus().clearNodes().unsetAllMarks().run(), 20 | isActive: () => editor.can().chain().focus().clearNodes().unsetAllMarks().run(), 21 | icon: 'Eraser', 22 | tooltip: t('editor.clear.tooltip'), 23 | }, 24 | }), 25 | }; 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/extensions/CodeBlock/components/NodeViewCodeBlock/index.module.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | --editor__bg: #292c33; 3 | --widget__border: #3a3f4b; 4 | --widget__color: #ccc; 5 | 6 | :global { 7 | .richtext-node-code-block { 8 | width: 100%; 9 | outline: solid 1px black; 10 | overflow: hidden; 11 | border-radius: 4px; 12 | position: relative; 13 | } 14 | 15 | .richtext-code-block-toolbar { 16 | display: flex; 17 | align-items: center; 18 | gap: 8px; 19 | z-index: 10; 20 | padding: 4px; 21 | background-color: var(--editor__bg); 22 | color: var(--widget__color); 23 | border-bottom: 1px solid var(--widget__border); 24 | 25 | .toolbar-divider { 26 | width: 1px; 27 | height: 16px; 28 | background-color: var(--widget__border); 29 | margin: 0 4px; 30 | } 31 | } 32 | } 33 | } 34 | 35 | .blockInfoEditable { 36 | pointer-events: none; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = 'horizontal', decorative = true, ...props }, 14 | ref, 15 | ) => ( 16 | 27 | ), 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /scripts/modifyCss.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { cwd } from 'node:process' 4 | 5 | function modifyPrefixVariableCss() { 6 | try { 7 | const css = fs.readFileSync(path.resolve(cwd(), 'lib/style.css'), 'utf-8') 8 | 9 | const cssMatch = ['background', 'foreground', 'muted', 'muted-foreground', 'popover', 'popover-foreground', 'card', 'card-foreground', 'border', 'input', 'primary', 'primary-foreground', 'secondary', 'secondary-foreground', 'accent', 'accent-foreground', 'destructive', 'destructive-foreground', 'ring', 'radius'] 10 | 11 | let newCss = css 12 | for (const match of cssMatch) { 13 | const reg = new RegExp(`--${match}`, 'g') 14 | 15 | newCss = newCss.replace(reg, `--richtext-${match}`) 16 | } 17 | 18 | fs.writeFileSync(path.resolve(cwd(), 'lib/style.css'), newCss) 19 | console.log('CSS modified successfully!') 20 | } 21 | catch { 22 | console.error('Failed to modify CSS!') 23 | } 24 | } 25 | 26 | modifyPrefixVariableCss() 27 | -------------------------------------------------------------------------------- /src/extensions/Twitter/components/NodeViewTweet.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { NodeViewWrapper, type ReactNodeViewRendererOptions } from '@tiptap/react'; 3 | import { Tweet } from 'react-tweet'; 4 | 5 | export const TWITTER_REGEX_GLOBAL = /(https?:\/\/)?(www\.)?x\.com\/(\w{1,15})(\/status\/(\d+))?(\/\S*)?/g; 6 | export const TWITTER_REGEX = /^https?:\/\/(www\.)?x\.com\/(\w{1,15})(\/status\/(\d+))?(\/\S*)?$/; 7 | 8 | export function isValidTwitterUrl(url: string) { 9 | return url.match(TWITTER_REGEX); 10 | } 11 | 12 | function NodeViewTweet({ node }: { node: Partial }) { 13 | // @ts-expect-error 14 | const url = node?.attrs?.src || ''; 15 | const tweetId = url?.split('/').pop(); 16 | 17 | if (!tweetId) { 18 | return null; 19 | } 20 | 21 | return ( 22 | 23 |
24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | export default NodeViewTweet; 31 | -------------------------------------------------------------------------------- /src/extensions/SlashCommand/components/SlashCommandList.tsx: -------------------------------------------------------------------------------- 1 | import { renderCommandListDefault } from '@/extensions/SlashCommand/renderCommandListDefault'; 2 | import { CommandList } from '@/extensions/SlashCommand/types'; 3 | import { useLocale } from '@/locales'; 4 | import { useSignalCommandList } from '@/store/commandList'; 5 | import { useEffect } from 'react'; 6 | 7 | interface SlashCommandListProps { 8 | // Define any props needed for the SlashCommandList component 9 | commandList?: CommandList[] 10 | } 11 | 12 | export function SlashCommandList ({ commandList }: SlashCommandListProps) { 13 | const [, setSignalCommandListValue] = useSignalCommandList(); 14 | const { t } = useLocale(); 15 | 16 | useEffect(() => { 17 | if (!commandList?.length) { 18 | const defaultCommands = renderCommandListDefault({ t }); 19 | setSignalCommandListValue(defaultCommands as any); 20 | return; 21 | } 22 | 23 | setSignalCommandListValue(commandList as any); 24 | }, [t, commandList]); 25 | 26 | return <>; 27 | } 28 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: React Tiptap Editor 6 | tagline: A modern WYSIWYG rich text editor based on tiptap and shadcn ui for Reactjs 7 | image: 8 | src: /logo.png 9 | alt: React Tiptap Editor 10 | actions: 11 | - theme: brand 12 | text: Get Started 13 | link: /guide/getting-started 14 | - theme: alt 15 | text: View on GitHub 16 | link: https://github.com/hunghg255/reactjs-tiptap-editor 17 | features: 18 | - title: Modern WYSIWYG editor 19 | details: reactjs-tiptap-editor is a modern WYSIWYG editor based on tiptap and shadcn ui for Reactjs. 20 | - title: Includes basic extensions 21 | details: reactjs-tiptap-editor includes basic extensions such as text, heading, paragraph, bold, italic, underline, strikethrough, code, code block, bullet list, ordered list, blockquote, link, image, table, and more. 22 | - title: Modern implementation 23 | details: reactjs-tiptap-editor is built with modern technologies such as Reactjs, TypeScript, and Tailwind CSS. 24 | --- 25 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |