├── .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 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/icons/CodeView.tsx:
--------------------------------------------------------------------------------
1 | export function CodeView() {
2 | return (
3 |
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 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/icons/ColumnAddRight.tsx:
--------------------------------------------------------------------------------
1 | export function ColumnAddRight() {
2 | return (
3 |
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 |
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 |
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 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/icons/NoFill.tsx:
--------------------------------------------------------------------------------
1 | export function NoFill() {
2 | return (
3 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/icons/ExportPdf.tsx:
--------------------------------------------------------------------------------
1 | export function ExportPdf() {
2 | return (
3 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/icons/LeftToRight.tsx:
--------------------------------------------------------------------------------
1 | export function LeftToRight() {
2 | return (
3 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/icons/RightToLeft.tsx:
--------------------------------------------------------------------------------
1 | export function RightToLeft() {
2 | return (
3 |
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 |
6 |
7 |
8 | Contributors
9 |
10 |
11 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/icons/Html.tsx:
--------------------------------------------------------------------------------
1 | export function Html() {
2 | return (
3 |
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 |
19 | );
20 | },
21 | );
22 | Textarea.displayName = 'Textarea';
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/playground/vite.config.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'node:path'
2 |
3 | import react from '@vitejs/plugin-react'
4 | import { defineConfig } from 'vite'
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig(({ mode }) => {
8 | const isDev = mode !== 'production'
9 | const isAnalyze = mode === 'analyze'
10 |
11 | return {
12 | define: {
13 | 'process.env': {}
14 | },
15 | plugins: [
16 | react(),
17 | ],
18 | optimizeDeps: {
19 | include: ['react'],
20 | },
21 | css: {
22 | devSourcemap: isDev,
23 | },
24 | build: {
25 | commonjsOptions: {
26 | include: [/node_modules/],
27 | },
28 | sourcemap: isAnalyze,
29 | },
30 | resolve: {
31 | alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
32 | },
33 | esbuild: {
34 | sourcemap: isDev,
35 | },
36 | server: {
37 | host: '0.0.0.0',
38 | port: 8000,
39 | },
40 | preview: {
41 | host: '0.0.0.0',
42 | port: 8000,
43 | },
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/src/extensions/Excalidraw/components/NodeViewExcalidraw/index.module.scss:
--------------------------------------------------------------------------------
1 | .wrap {
2 | position: relative;
3 | max-width: 100%;
4 | overflow: visible;
5 | line-height: 0;
6 |
7 | .renderWrap {
8 | border: 1px dashed hsl(var(--border)) !important;
9 | border-radius: 6px;
10 |
11 | &::after {
12 | background-color: transparent !important;
13 | }
14 | }
15 |
16 | .title {
17 | position: absolute;
18 | top: 10px;
19 | left: 10px;
20 | z-index: 2;
21 |
22 | .icon {
23 | display: flex;
24 | width: 18px;
25 | height: 18px;
26 | color: #fff;
27 | background-color: #f80;
28 | border-radius: 2px;
29 | justify-content: center;
30 | align-items: center;
31 | }
32 | }
33 |
34 | .handlerWrap {
35 | position: absolute;
36 | right: 10px;
37 | bottom: 10px;
38 | z-index: 2;
39 | padding: 2px 4px;
40 | border: 1px solid hsl(var(--border));
41 | border-radius: 6px;
42 | }
43 | }
44 |
45 | .disabled {
46 | pointer-events: none !important;
47 | }
48 |
--------------------------------------------------------------------------------
/src/extensions/Katex/components/KatexWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | import { NodeViewWrapper } from '@tiptap/react';
4 | import katex from 'katex';
5 |
6 | export function KatexNodeView({ node }: any) {
7 | const { text } = node.attrs;
8 |
9 | const formatText = useMemo(() => {
10 | try {
11 | return katex.renderToString(`${text}`);
12 | } catch {
13 | return text;
14 | }
15 | }, [text]);
16 |
17 | const content = useMemo(
18 | () =>
19 | text.trim()
20 | ? (
21 |
24 |
25 | )
26 | : (
27 |
28 | Not enter a formula
29 |
30 | ),
31 | [text, formatText],
32 | );
33 |
34 | return (
35 |
41 | {content}
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/Code/Code.ts:
--------------------------------------------------------------------------------
1 | import type { CodeOptions as TiptapCodeOptions } from '@tiptap/extension-code';
2 | import { Code as TiptapCode } from '@tiptap/extension-code';
3 |
4 | import type { GeneralOptions } from '@/types';
5 |
6 | export * from './components/RichTextCode';
7 |
8 | export interface CodeOptions extends TiptapCodeOptions, GeneralOptions {}
9 |
10 | export const Code = /* @__PURE__ */ TiptapCode.extend({
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, extension }) => ({
17 | componentProps: {
18 | action: () => editor.commands.toggleCode(),
19 | isActive: () => editor.isActive('code'),
20 | disabled: !editor.can().toggleCode(),
21 | icon: 'Code',
22 | shortcutKeys: extension.options.shortcutKeys ?? ['mod', 'E'],
23 | tooltip: t('editor.code.tooltip'),
24 | },
25 | }),
26 | };
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/icons/GIfIcon.tsx:
--------------------------------------------------------------------------------
1 | export function GifIcon() {
2 | return (
3 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/extensions/Clear/components/RichTextClear.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components/ActionButton';
2 | import { Clear } from '@/extensions/Clear/Clear';
3 | import { useActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextClear() {
7 | const buttonProps = useButtonProps(Clear.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { disabled } = useActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) action();
24 | };
25 |
26 | if (!buttonProps) {
27 | return <>>;
28 | }
29 |
30 | return (
31 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/icons/Activity.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Activity() {
4 | return (
5 |
16 | );
17 | }
18 |
19 | export default Activity;
20 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "private": true,
4 | "type": "module",
5 | "version": "1.0.7",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "start": "npm run build && vite preview",
11 | "lint": "eslint --ext .ts,.tsx src --color",
12 | "format": "prettier --write \"./src/**/*.{ts,tsx,json}\"",
13 | "analyze": "npm run lint && tsc && vite build --mode=analyze && source-map-explorer 'dist/assets/*.js'",
14 | "release": "bumpp -r"
15 | },
16 | "dependencies": {
17 | "react": "^19.1.0",
18 | "react-dom": "^19.1.0",
19 | "reactjs-tiptap-editor": "workspace:*"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^22.14.0",
23 | "@types/react": "^19.1.0",
24 | "@types/react-dom": "^19.1.1",
25 | "@vitejs/plugin-react": "^4.3.4",
26 | "bumpp": "^9.11.1",
27 | "git-scm-hooks": "^0.0.11",
28 | "prettier": "^2.8.8",
29 | "sass": "^1.86.2",
30 | "source-map-explorer": "^2.5.3",
31 | "typescript": "^5.8.2",
32 | "vite": "^6.2.4"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/extensions/Bold/Bold.ts:
--------------------------------------------------------------------------------
1 | import type { BoldOptions as TiptapImageOptions } from '@tiptap/extension-bold';
2 | import { Bold as TiptapBold } from '@tiptap/extension-bold';
3 |
4 | import { ActionButton } from '@/components';
5 | import type { GeneralOptions } from '@/types';
6 |
7 | export * from './components/RichTextBold';
8 |
9 | export interface BoldOptions extends TiptapImageOptions, GeneralOptions {}
10 |
11 | export const Bold = /* @__PURE__ */ TiptapBold.extend({
12 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
13 | //@ts-expect-error
14 | addOptions() {
15 | return {
16 | ...this.parent?.(),
17 | button: ({ editor, t, extension }: any) => ({
18 | component: ActionButton,
19 | componentProps: {
20 | action: () => editor.commands.toggleBold(),
21 | isActive: () => editor.isActive('bold'),
22 | icon: 'Bold',
23 | shortcutKeys: extension.options.shortcutKeys ?? ['mod', 'B'],
24 | tooltip: t('editor.bold.tooltip'),
25 | },
26 | }),
27 | };
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/docs/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import Unocss from 'unocss/vite'
3 | import VueJsx from '@vitejs/plugin-vue-jsx'
4 | import changeLog from '../scripts/changeLog.json'
5 | import contributions from '../scripts/contributions.json'
6 | import { ChangeLog } from './.vitepress/plugins/changelog'
7 | import { Contributors } from './.vitepress/plugins/contributors'
8 | import { MarkdownTransform } from './.vitepress/plugins/markdownTransform'
9 |
10 | export default defineConfig(async () => {
11 | return {
12 | plugins: [MarkdownTransform(), ChangeLog(changeLog), Contributors(contributions), VueJsx(), Unocss()],
13 | optimizeDeps: {
14 | include: [
15 | '@nolebase/vitepress-plugin-enhanced-readabilities > @nolebase/ui > @rive-app/canvas',
16 | ],
17 | exclude: ['@nolebase/vitepress-plugin-enhanced-readabilities/client', 'vitepress'],
18 | },
19 | ssr: {
20 | noExternal: [
21 | '@nolebase/vitepress-plugin-enhanced-readabilities',
22 | '@nolebase/vitepress-plugin-highlight-targeted-heading',
23 | ],
24 | },
25 | }
26 | })
27 |
--------------------------------------------------------------------------------
/src/extensions/Strike/Strike.ts:
--------------------------------------------------------------------------------
1 | import type { StrikeOptions as TiptapStrikeOptions } from '@tiptap/extension-strike';
2 | import { Strike as TiptapStrike } from '@tiptap/extension-strike';
3 |
4 | import type { GeneralOptions } from '@/types';
5 |
6 | export * from './components/RichTextStrike';
7 |
8 | export interface StrikeOptions extends TiptapStrikeOptions, GeneralOptions {}
9 |
10 | export const Strike = /* @__PURE__ */ TiptapStrike.extend({
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, extension }: any) => ({
17 | componentProps: {
18 | action: () => editor.commands.toggleStrike(),
19 | isActive: () => editor.isActive('strike') || false,
20 | disabled: false,
21 | icon: 'Strikethrough',
22 | shortcutKeys: extension.options.shortcutKeys ?? ['shift', 'mod', 'S'],
23 | tooltip: t('editor.strike.tooltip'),
24 | },
25 | }),
26 | };
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from '@/components/ui/toast';
11 | import { useToast } from '@/components/ui/use-toast';
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(({ id, title, description, action, ...props }) => {
19 | return (
20 |
23 |
24 | {title &&
25 | {title}
26 | }
27 |
28 | {description && (
29 |
30 | {description}
31 |
32 | )}
33 |
34 |
35 | {action}
36 |
37 |
38 | );
39 | })}
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/Italic/Italic.ts:
--------------------------------------------------------------------------------
1 | import type { ItalicOptions as TiptapItalicOptions } from '@tiptap/extension-italic';
2 | import TiptapItalic from '@tiptap/extension-italic';
3 |
4 | import type { GeneralOptions } from '@/types';
5 |
6 | export * from './components/RichTextItalic';
7 |
8 | export interface ItalicOptions extends TiptapItalicOptions, GeneralOptions {}
9 |
10 | export const Italic = /* @__PURE__ */ TiptapItalic.extend({
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, extension }) {
17 | return {
18 | componentProps: {
19 | action: () => editor.commands.toggleItalic(),
20 | isActive: () => editor.isActive('italic') || false,
21 | disabled: false,
22 | shortcutKeys: extension.options.shortcutKeys ?? ['mod', 'I'],
23 | icon: 'Italic',
24 | tooltip: t('editor.italic.tooltip'),
25 | },
26 | };
27 | },
28 | };
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 hunghg255
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guide
2 |
3 | Thanks for lending a hand 👋
4 |
5 | ## Development
6 |
7 | ### Setup
8 |
9 | - We use [pnpm](https://pnpm.js.org/) to manage dependencies. Install it with `npm i -g pnpm`.
10 |
11 | Install dependencies
12 |
13 | ```bash
14 | pnpm install
15 | ```
16 |
17 | Start the Demo server
18 |
19 | ```bash
20 | npm run build:lib:dev
21 | npm run playground
22 | ```
23 |
24 | ### Packages Structure
25 |
26 | - `src/components`: Contains all the components.
27 | - `src/hooks`: Contains all the hooks.
28 | - `src/utils`: Contains all the utility functions.
29 | - `src/styles`: Contains all the global styles.
30 | - `src/index.ts`: Exports all the components, hooks, and utility functions.
31 | - `src/extensions`: Contains all the extensions.
32 |
33 | ### Coding conventions
34 |
35 | - We use ESLint to lint and format the codebase. Before you commit, all files will be formatted automatically.
36 | - We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). Please use a prefix. If your PR has multiple commits and some of them don't follow the Conventional Commits rule, we'll do a squash merge.
37 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | },
22 | );
23 | Input.displayName = 'Input';
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/src/components/icons/DeleteRow.tsx:
--------------------------------------------------------------------------------
1 | function DeleteRow() {
2 | return (
3 |
23 | );
24 | }
25 |
26 | export { DeleteRow };
27 |
--------------------------------------------------------------------------------
/src/extensions/Iframe/components/RichTextIframe.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Iframe } from '@/extensions/Iframe/Iframe';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextIframe() {
7 | const buttonProps = useButtonProps(Iframe.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { editorDisabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (editorDisabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/icons/DeleteColumn.tsx:
--------------------------------------------------------------------------------
1 | function DeleteColumn() {
2 | return (
3 |
23 | );
24 | }
25 |
26 | export { DeleteColumn };
27 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | permissions:
4 | id-token: write # Required for OIDC
5 | contents: read
6 |
7 | on:
8 | push:
9 | tags:
10 | - 'v*'
11 |
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | release:
18 | permissions:
19 | id-token: write
20 | contents: write
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | with:
25 | fetch-depth: 0
26 |
27 | - name: Install pnpm
28 | uses: pnpm/action-setup@v4
29 |
30 | - name: Set node
31 | uses: actions/setup-node@v4
32 | with:
33 | node-version: lts/*
34 | registry-url: https://registry.npmjs.org/
35 |
36 | - name: Install
37 | run: pnpm install
38 |
39 | - run: npx changeloggithub@latest
40 | env:
41 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
42 |
43 | - name: Publish
44 | run: npm publish --access=public --no-git-checks
45 | env:
46 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
47 | NPM_CONFIG_PROVENANCE: true
48 |
--------------------------------------------------------------------------------
/src/extensions/Bold/components/RichTextBold.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Bold } from '@/extensions/Bold/Bold';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextBold() {
7 | const buttonProps = useButtonProps(Bold.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/Code/components/RichTextCode.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Code } from '@/extensions/Code/Code';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextCode() {
7 | const buttonProps = useButtonProps(Code.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/utils/getRenderContainer.ts:
--------------------------------------------------------------------------------
1 | import type { Editor } from '@tiptap/react';
2 |
3 | export function getRenderContainer(editor: Editor, nodeType: string) {
4 | const {
5 | view,
6 | state: {
7 | selection: { from },
8 | },
9 | } = editor;
10 |
11 | const elements = document.querySelectorAll('.has-focus');
12 | const elementCount = elements.length;
13 | const innermostNode = elements[elementCount - 1];
14 | const element = innermostNode as any;
15 |
16 | if (
17 | (element && element.dataset.type && element.dataset.type === nodeType)
18 | || (element && element.classList && element.classList.contains(nodeType))
19 | ) {
20 | return element;
21 | }
22 |
23 | const node = view.domAtPos(from).node as HTMLElement;
24 | let container: any = node;
25 |
26 | if (!container.tagName) {
27 | container = node.parentElement;
28 | }
29 |
30 | while (
31 | container
32 | && !(container.dataset.type && container.dataset.type === nodeType)
33 | && !container.classList.contains(nodeType)
34 | ) {
35 | container = container.parentElement;
36 | }
37 |
38 | return container;
39 | }
40 |
41 | export default getRenderContainer;
42 |
--------------------------------------------------------------------------------
/src/extensions/CodeBlock/components/RichTextCodeBlock.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | ActionButton,
5 | } from '@/components';
6 | import { CodeBlock } from '@/extensions/CodeBlock/CodeBlock';
7 | import { useToggleActive } from '@/hooks/useActive';
8 | import { useButtonProps } from '@/hooks/useButtonProps';
9 |
10 | export function RichTextCodeBlock() {
11 | const buttonProps = useButtonProps(CodeBlock.name);
12 |
13 | const {
14 | icon = undefined,
15 | tooltip = undefined,
16 | tooltipOptions = {},
17 | action = undefined,
18 | isActive = undefined,
19 | } = buttonProps?.componentProps ?? {};
20 |
21 | const { dataState, disabled, update } = useToggleActive(isActive);
22 |
23 | const onAction = () => {
24 | if (disabled) return;
25 |
26 | if (action) {
27 | action();
28 | update();
29 | }
30 | };
31 |
32 | if (!buttonProps) {
33 | return <>>;
34 | }
35 |
36 | return (
37 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/extensions/Column/components/RichTextColumn.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Column } from '@/extensions/Column/Column';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextColumn() {
7 | const buttonProps = useButtonProps(Column.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/Italic/components/RichTextItalic.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Italic } from '@/extensions/Italic/Italic';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextItalic() {
7 | const buttonProps = useButtonProps(Italic.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/Strike/components/RichTextStrike.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Strike } from '@/extensions/Strike/Strike';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextStrike() {
7 | const buttonProps = useButtonProps(Strike.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/Attachment/components/RichTextAttachment.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Attachment } from '@/extensions/Attachment/Attachment';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextAttachment() {
7 | const buttonProps = useButtonProps(Attachment.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { editorDisabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (editorDisabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/src/extensions/CodeView/components/RichTextCodeView.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { CodeView } from '@/extensions/CodeView/CodeView';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextCodeView() {
7 | const buttonProps = useButtonProps(CodeView.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/ExportPdf/components/RichTextExportPdf.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { ExportPdf } from '@/extensions/ExportPdf';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextExportPdf() {
7 | const buttonProps = useButtonProps(ExportPdf.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/TaskList/components/RichTextTaskList.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { TaskList } from '@/extensions/TaskList/TaskList';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextTaskList() {
7 | const buttonProps = useButtonProps(TaskList.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/TextUnderline/TextUnderline.ts:
--------------------------------------------------------------------------------
1 | import type { UnderlineOptions as TiptapUnderlineOptions } from '@tiptap/extension-underline';
2 | import TiptapUnderline from '@tiptap/extension-underline';
3 |
4 | import type { GeneralOptions } from '@/types';
5 |
6 | export * from './components/RichTextUnderline';
7 |
8 | export interface UnderlineOptions
9 | extends TiptapUnderlineOptions,
10 | GeneralOptions {}
11 |
12 | export const TextUnderline = /* @__PURE__ */ TiptapUnderline.extend({
13 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
14 | //@ts-expect-error
15 | addOptions() {
16 | return {
17 | ...this.parent?.(),
18 | button({ editor, t, extension }: any) {
19 | return {
20 | componentProps: {
21 | action: () => editor.commands.toggleUnderline(),
22 | isActive: () => editor.isActive('underline') || false,
23 | disabled: false,
24 | icon: 'Underline',
25 | shortcutKeys: extension.options.shortcutKeys ?? ['mod', 'U'],
26 | tooltip: t('editor.underline.tooltip'),
27 | },
28 | };
29 | },
30 | };
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/src/components/icons/Food.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Food() {
4 | return (
5 |
16 |
17 | );
18 | }
19 |
20 | export default Food;
21 |
--------------------------------------------------------------------------------
/src/extensions/Blockquote/components/RichTextBlockquote.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Blockquote } from '@/extensions/Blockquote/Blockquote';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextBlockquote() {
7 | const buttonProps = useButtonProps(Blockquote.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/BulletList/components/RichTextBulletList.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { BulletList } from '@/extensions/BulletList/BulletList';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextBulletList() {
7 | const buttonProps = useButtonProps(BulletList.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/OrderedList/components/RichTextOrderedList.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { OrderedList } from '@/extensions/OrderedList/OrderedList';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextOrderedList() {
7 | const buttonProps = useButtonProps(OrderedList.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/TextUnderline/components/RichTextUnderline.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { TextUnderline } from '@/extensions/TextUnderline/TextUnderline';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextUnderline() {
7 | const buttonProps = useButtonProps(TextUnderline.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/HorizontalRule/components/RichTextHorizontalRule.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { HorizontalRule } from '@/extensions/HorizontalRule/HorizontalRule';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextHorizontalRule() {
7 | const buttonProps = useButtonProps(HorizontalRule.name);
8 |
9 | const {
10 | icon = undefined,
11 | tooltip = undefined,
12 | shortcutKeys = undefined,
13 | tooltipOptions = {},
14 | action = undefined,
15 | isActive = undefined,
16 | } = buttonProps?.componentProps ?? {};
17 |
18 | const { dataState, disabled, update } = useToggleActive(isActive);
19 |
20 | const onAction = () => {
21 | if (disabled) return;
22 |
23 | if (action) {
24 | action();
25 | update();
26 | }
27 | };
28 |
29 | if (!buttonProps) {
30 | return <>>;
31 | }
32 |
33 | return (
34 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/extensions/Blockquote/Blockquote.ts:
--------------------------------------------------------------------------------
1 | import type { BlockquoteOptions as TiptapBlockquoteOptions } from '@tiptap/extension-blockquote';
2 | import { Blockquote as TiptapBlockquote } from '@tiptap/extension-blockquote';
3 |
4 | import type { GeneralOptions } from '@/types';
5 |
6 | export * from './components/RichTextBlockquote';
7 |
8 | export interface BlockquoteOptions
9 | extends TiptapBlockquoteOptions,
10 | GeneralOptions {}
11 |
12 | export const Blockquote = /* @__PURE__ */ TiptapBlockquote.extend({
13 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
14 | //@ts-expect-error
15 | addOptions() {
16 | return {
17 | ...this.parent?.(),
18 | HTMLAttributes: {
19 | class: 'blockquote',
20 | },
21 | button: ({ editor, t, extension }: any) => ({
22 | componentProps: {
23 | action: () => editor.commands.toggleBlockquote(),
24 | isActive: () => editor.isActive('blockquote'),
25 | disabled: !editor.can().toggleBlockquote(),
26 | icon: 'TextQuote',
27 | shortcutKeys: extension.options.shortcutKeys ?? ['shift', 'mod', 'B'],
28 | tooltip: t('editor.blockquote.tooltip'),
29 | },
30 | }),
31 | };
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/Contributors.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
No recent changes
31 |
32 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/extensions/OrderedList/OrderedList.ts:
--------------------------------------------------------------------------------
1 | // import type { OrderedListOptions as TiptapOrderedListOptions } from '@tiptap/extension-ordered-list';
2 | // import { OrderedList as TiptapOrderedList } from '@tiptap/extension-ordered-list';
3 | import { OrderedList as TiptapOrderedList, type OrderedListOptions as TiptapOrderedListOptions } from '@tiptap/extension-list';
4 |
5 | import type { GeneralOptions } from '@/types';
6 |
7 | export * from './components/RichTextOrderedList';
8 |
9 | export interface OrderedListOptions
10 | extends TiptapOrderedListOptions,
11 | GeneralOptions {}
12 |
13 | export const OrderedList = /* @__PURE__ */ TiptapOrderedList.extend({
14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
15 | //@ts-expect-error
16 | addOptions() {
17 | return {
18 | ...this.parent?.(),
19 | button: ({ editor, t, extension }) => ({
20 | componentProps: {
21 | action: () => editor.commands.toggleOrderedList(),
22 | isActive: () => editor.isActive('orderedList'),
23 | disabled: false,
24 | icon: 'ListOrdered',
25 | shortcutKeys: extension.options.shortcutKeys ?? ['mod', 'shift', '7'],
26 | tooltip: t('editor.orderedlist.tooltip'),
27 | },
28 | }),
29 | };
30 | },
31 | });
32 |
--------------------------------------------------------------------------------
/src/extensions/BulletList/BulletList.ts:
--------------------------------------------------------------------------------
1 | // import type { BulletListOptions as TiptapBulletListOptions } from '@tiptap/extension-bullet-list';
2 | // import { BulletList as TiptapBulletList } from '@tiptap/extension-bullet-list';
3 | import { BulletList as TiptapBulletList, type BulletListOptions as TiptapBulletListOptions } from '@tiptap/extension-list';
4 |
5 | import type { GeneralOptions } from '@/types';
6 |
7 | export * from './components/RichTextBulletList';
8 |
9 | export interface BulletListOptions
10 | extends TiptapBulletListOptions,
11 | GeneralOptions {}
12 |
13 | export const BulletList = /* @__PURE__ */ TiptapBulletList.extend({
14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
15 | //@ts-expect-error
16 | addOptions() {
17 | return {
18 | ...this.parent?.(),
19 | button: ({ editor, t, extension }) => ({
20 | // component: ActionButton,
21 | componentProps: {
22 | action: () => editor.commands.toggleBulletList(),
23 | isActive: () => editor.isActive('bulletList'),
24 | disabled: false,
25 | shortcutKeys: extension.options.shortcutKeys ?? ['shift', 'mod', '8'],
26 | icon: 'List',
27 | tooltip: t('editor.bulletlist.tooltip'),
28 | },
29 | }),
30 | };
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/.github/workflows/preview-playground.yml:
--------------------------------------------------------------------------------
1 | name: 🔂 Preview Pull Request
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - opened
7 | - synchronize
8 | - closed
9 | branches:
10 | - main
11 |
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | preview-playground:
18 | runs-on: ubuntu-latest
19 | permissions:
20 | pull-requests: write
21 | contents: read
22 | steps:
23 | - uses: actions/checkout@v4
24 | with:
25 | fetch-depth: 0
26 |
27 | - name: Install pnpm
28 | uses: pnpm/action-setup@v4
29 |
30 | - name: Set node
31 | uses: actions/setup-node@v4
32 | with:
33 | node-version: lts/*
34 | registry-url: https://registry.npmjs.org/
35 |
36 | - uses: hunghg255/surge-preview@master
37 | id: preview_step
38 | with:
39 | surge_token: ${{ secrets.SURGE_TOKEN }}
40 | github_token: ${{ secrets.GITHUB_TOKEN }}
41 | dist: "playground/dist"
42 | failOnError: true
43 | teardown: 'true'
44 | build: |
45 | pnpm install
46 | pnpm run build:playground
47 | - name: Get the preview_url
48 | run: echo "url => ${{ steps.preview_step.outputs.preview_url }}"
49 |
--------------------------------------------------------------------------------
/src/components/icons/Object.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Object() {
4 | return (
5 |
20 |
21 | );
22 | }
23 |
24 | export default Object;
25 |
--------------------------------------------------------------------------------
/src/extensions/Link/components/LinkViewBlock.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ActionButton } from '@/components';
4 | import { useLocale } from '@/locales';
5 |
6 | interface IPropsLinkViewBlock {
7 | editor: any
8 | link: string
9 | onClear?: any
10 | onEdit?: any
11 | }
12 |
13 | function LinkViewBlock(props: IPropsLinkViewBlock) {
14 | const { t } = useLocale();
15 |
16 | return (
17 |
18 |
{
24 | window.open(props?.link, '_blank');
25 | }}
26 | />
27 |
28 | {
33 | props?.onEdit();
34 | }}
35 | />
36 |
37 | {
42 | props?.onClear();
43 | }}
44 | />
45 |
46 | );
47 | }
48 |
49 | export default LinkViewBlock;
50 |
--------------------------------------------------------------------------------
/src/utils/delete-node.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 | import type { Editor } from '@tiptap/core';
3 |
4 | export function deleteNode(nodeType: string, editor: Editor) {
5 | const { state } = editor;
6 | const $pos = state.selection.$anchor;
7 | let done = false;
8 |
9 | if ($pos.depth) {
10 | for (let d = $pos.depth; d > 0; d--) {
11 | const node = $pos.node(d);
12 | if (node.type.name === nodeType) {
13 | // @ts-ignore
14 | if (editor.dispatchTransaction)
15 | // @ts-ignore
16 | editor.dispatchTransaction(state.tr.delete($pos.before(d), $pos.after(d)).scrollIntoView());
17 | done = true;
18 | }
19 | }
20 | } else {
21 | // @ts-ignore
22 | const node = state.selection.node;
23 | if (node && node.type.name === nodeType) {
24 | editor.chain().deleteSelection().run();
25 | done = true;
26 | }
27 | }
28 |
29 | if (!done) {
30 | const pos = $pos.pos;
31 |
32 | if (pos) {
33 | const node = state.tr.doc.nodeAt(pos);
34 |
35 | if (node && node.type.name === nodeType) {
36 | // @ts-ignore
37 | if (editor.dispatchTransaction)
38 | // @ts-ignore
39 | editor.dispatchTransaction(state.tr.delete(pos, pos + node.nodeSize));
40 | done = true;
41 | }
42 | }
43 | }
44 |
45 | return done;
46 | }
47 |
--------------------------------------------------------------------------------
/.github/workflows/preview-label.yml:
--------------------------------------------------------------------------------
1 | name: CR
2 |
3 | on:
4 | pull_request_target:
5 | types: [labeled, unlabeled]
6 |
7 | jobs:
8 | preview-pr:
9 | if: ${{ !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'preview-pr') && !contains(github.event.pull_request.labels.*.name, 'spam') && !contains(github.event.pull_request.labels.*.name, 'invalid') }}
10 |
11 | runs-on: ubuntu-latest
12 | permissions:
13 | pull-requests: write
14 | contents: read
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Install pnpm
21 | uses: pnpm/action-setup@v4
22 |
23 | - name: Set node
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: lts/*
27 | registry-url: https://registry.npmjs.org/
28 |
29 | - uses: hunghg255/surge-preview@master
30 | id: preview_step
31 | with:
32 | surge_token: ${{ secrets.SURGE_TOKEN }}
33 | github_token: ${{ secrets.GITHUB_TOKEN }}
34 | dist: "playground/dist"
35 | failOnError: true
36 | teardown: 'true'
37 | build: |
38 | pnpm install
39 | pnpm run build:playground
40 | - name: Get the preview_url
41 | run: echo "url => ${{ steps.preview_step.outputs.preview_url }}"
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 |
5 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const TooltipProvider = TooltipPrimitive.Provider;
10 |
11 | const Tooltip = TooltipPrimitive.Root;
12 |
13 | const TooltipTrigger = TooltipPrimitive.Trigger;
14 |
15 | const TooltipContent = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, sideOffset = 4, ...props }, ref) => (
19 |
29 | ));
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
33 |
--------------------------------------------------------------------------------
/src/extensions/ExportWord/components/RichTextExportWord.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { ExportWord } from '@/extensions/ExportWord/ExportWord';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 | import { useState } from 'react';
6 |
7 | export function RichTextExportWord() {
8 | const buttonProps = useButtonProps(ExportWord.name);
9 | const [loading, setLoading] = useState(false);
10 |
11 | const {
12 | icon = undefined,
13 | tooltip = undefined,
14 | shortcutKeys = undefined,
15 | tooltipOptions = {},
16 | action = undefined,
17 | isActive = undefined,
18 | } = buttonProps?.componentProps ?? {};
19 |
20 | const { dataState, disabled, update } = useToggleActive(isActive);
21 |
22 | const onAction = async () => {
23 | if (disabled || loading) return;
24 |
25 | if (action) {
26 | setLoading(true);
27 | await action();
28 | update();
29 | setLoading(false);
30 | }
31 | };
32 |
33 | if (!buttonProps) {
34 | return <>>;
35 | }
36 |
37 | return (
38 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/icons/Travel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Travel() {
4 | return (
5 |
20 |
21 | );
22 | }
23 |
24 | export default Travel;
25 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 |
5 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
6 | import { Check } from 'lucide-react';
7 |
8 | import { cn } from '@/lib/utils';
9 |
10 | const Checkbox = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 |
25 |
26 |
27 |
28 | ));
29 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
30 |
31 | export { Checkbox };
32 |
--------------------------------------------------------------------------------
/playground/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/extensions/Indent/components/RichTextIndent.tsx:
--------------------------------------------------------------------------------
1 | import { ActionButton } from '@/components';
2 | import { Indent } from '@/extensions/Indent/Indent';
3 | import { useToggleActive } from '@/hooks/useActive';
4 | import { useButtonProps } from '@/hooks/useButtonProps';
5 |
6 | export function RichTextIndent() {
7 | const buttonProps = useButtonProps(Indent.name);
8 |
9 | const {
10 | indent,
11 | outdent
12 | } = buttonProps?.componentProps ?? {};
13 |
14 | const { editorDisabled } = useToggleActive();
15 |
16 | const onActionIndent = () => {
17 | if (editorDisabled) return;
18 |
19 | if (indent?.action) {
20 | indent?.action();
21 | }
22 | };
23 |
24 | const onActionOutdent = () => {
25 | if (editorDisabled) return;
26 |
27 | if (outdent?.action) {
28 | outdent?.action();
29 | }
30 | };
31 |
32 | if (!buttonProps) {
33 | return <>>;
34 | }
35 |
36 | return (
37 | <>
38 |
46 |
47 |
55 | >
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/extensions/Table/components/RichTextTable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ActionButton } from '@/components';
4 | import CreateTablePopover from '@/extensions/Table/components/CreateTablePopover';
5 | import { Table } from '@/extensions/Table/Table';
6 | import { useToggleActive } from '@/hooks/useActive';
7 | import { useButtonProps } from '@/hooks/useButtonProps';
8 | import { useEditorInstance } from '@/store/editor';
9 |
10 | export function RichTextTable() {
11 | const editor = useEditorInstance();
12 | const buttonProps = useButtonProps(Table.name);
13 |
14 | const {
15 | icon = undefined,
16 | tooltip = undefined,
17 | action = undefined,
18 | isActive = undefined,
19 | color
20 | } = buttonProps?.componentProps ?? {};
21 |
22 | const { dataState, disabled } = useToggleActive(isActive);
23 |
24 | if (!buttonProps) {
25 | return <>>;
26 | }
27 |
28 | function createTable(options: any) {
29 | editor
30 | .chain()
31 | .focus()
32 | .insertTable({ ...options, withHeaderRow: false })
33 | .run();
34 | }
35 |
36 | return (
37 |
40 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/hooks/useAttributes.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 | import { useEffect, useRef, useState } from 'react';
3 |
4 | import type { Editor } from '@tiptap/core';
5 | import deepEqual from 'deep-equal';
6 |
7 | type MapFn = (arg: T) => R;
8 |
9 | function mapSelf(d: T): T {
10 | return d;
11 | }
12 |
13 | export function useAttributes(editor: Editor, attrbute: string, defaultValue?: T, map?: (arg: T) => R) {
14 | const mapFn = (map || mapSelf) as MapFn;
15 | const [value, setValue] = useState(mapFn(defaultValue as any));
16 | const prevValueCache = useRef(value);
17 |
18 | useEffect(() => {
19 | const listener = () => {
20 | const attrs = { ...defaultValue, ...editor.getAttributes(attrbute) } as any;
21 | Object.keys(attrs).forEach((key) => {
22 | if (attrs[key] === null || attrs[key] === undefined) {
23 | // @ts-ignore
24 | attrs[key] = defaultValue ? defaultValue[key] : null;
25 | }
26 | });
27 | const nextAttrs = mapFn(attrs);
28 | if (deepEqual(prevValueCache.current, nextAttrs)) {
29 | return;
30 | }
31 | setValue(nextAttrs);
32 | prevValueCache.current = nextAttrs;
33 | };
34 |
35 | editor.on('selectionUpdate', listener);
36 | editor.on('transaction', listener);
37 |
38 | return () => {
39 | editor.off('selectionUpdate', listener);
40 | editor.off('transaction', listener);
41 | };
42 | }, [editor, defaultValue, attrbute, mapFn]);
43 |
44 | return value;
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 |
5 | import * as SwitchPrimitives from '@radix-ui/react-switch';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Switch = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
26 |
27 | ));
28 | Switch.displayName = SwitchPrimitives.Root.displayName;
29 |
30 | export { Switch };
31 |
--------------------------------------------------------------------------------
/docs/.vitepress/components/HomePage.vue:
--------------------------------------------------------------------------------
1 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Thanks to the following friends for their contributions to project
44 |
45 |

46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/utils/color.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-ts-comment */
2 | const colors = [
3 | '#47A1FF',
4 | '#59CB74',
5 | '#FFB952',
6 | '#FC6980',
7 | '#6367EC',
8 | '#DA65CC',
9 | '#FBD54A',
10 | '#ADDF84',
11 | '#6CD3FF',
12 | '#659AEC',
13 | '#9F8CF1',
14 | '#ED8CCE',
15 | '#A2E5FF',
16 | '#4DCCCB',
17 | '#F79452',
18 | '#84E0BE',
19 | '#5982F6',
20 | '#E37474',
21 | '#3FDDC7',
22 | '#9861E5',
23 | ];
24 |
25 | const total = colors.length;
26 | export const getRandomColor = () => colors[Math.trunc(Math.random() * total)];
27 |
28 | /**
29 | * @param hexCode
30 | * @param opacity
31 | * @returns
32 | */
33 | export function convertColorToRGBA(hexCode: string, opacity = 1) {
34 | let r = 0;
35 | let g = 0;
36 | let b = 0;
37 |
38 | if (hexCode.startsWith('rgb')) {
39 | // @ts-expect-error
40 | const rgb = hexCode
41 | .replace(/\s/g, '')
42 | .match(/rgb\((.*)\)$/)[1]
43 | .split(',');
44 |
45 | r = +rgb[0];
46 | g = +rgb[1];
47 | b = +rgb[2];
48 | } else if (hexCode.startsWith('#')) {
49 | let hex = hexCode.replace('#', '');
50 |
51 | if (hex.length === 3) {
52 | hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`;
53 | }
54 |
55 | r = Number.parseInt(hex.substring(0, 2), 16);
56 | g = Number.parseInt(hex.substring(2, 4), 16);
57 | b = Number.parseInt(hex.substring(4, 6), 16);
58 | } else {
59 | return hexCode;
60 | }
61 |
62 | if (opacity > 1 && opacity <= 100) {
63 | opacity = opacity / 100;
64 | }
65 |
66 | return `rgba(${r},${g},${b},${opacity})`;
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 |
5 | import * as PopoverPrimitive from '@radix-ui/react-popover';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Popover = PopoverPrimitive.Root;
10 |
11 | const PopoverTrigger = PopoverPrimitive.Trigger;
12 |
13 | const PopoverContent = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
17 |
18 |
29 |
30 | ));
31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
32 |
33 | export { Popover, PopoverTrigger, PopoverContent };
34 |
--------------------------------------------------------------------------------
/src/components/icons/ExportWord.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function ExportWord() {
4 | return (
5 |
21 | );
22 | }
23 |
24 | export default ExportWord;
25 |
--------------------------------------------------------------------------------
/src/extensions/History/History.ts:
--------------------------------------------------------------------------------
1 | import { UndoRedo, type UndoRedoOptions } from '@tiptap/extensions';
2 |
3 | // import HistoryActionButton from '@/extensions/History/components/HistoryActionButton';
4 | import type { GeneralOptions } from '@/types';
5 |
6 | export interface HistoryOptions extends UndoRedoOptions, GeneralOptions { }
7 |
8 | export const History = /* @__PURE__ */ UndoRedo.extend({
9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
10 | //@ts-expect-error
11 | addOptions() {
12 | return {
13 | ...this.parent?.(),
14 | depth: 100,
15 | newGroupDelay: 500,
16 | button: ({ editor, t, extension }: any) => {
17 | return {
18 | componentProps: {
19 | undo: {
20 | action: () => {
21 | editor.chain().focus().undo().run();
22 | },
23 | shortcutKeys: extension.options.shortcutKeys?.[0] ?? ['mod', 'Z'],
24 | isActive: () => editor.can().undo(),
25 | icon: 'Undo2',
26 | tooltip: t('editor.undo.tooltip'),
27 | },
28 | redo: {
29 | action: () => {
30 | editor.chain().focus().redo().run();
31 | },
32 | shortcutKeys: extension.options.shortcutKeys?.[1] ?? ['shift', 'mod', 'Z'],
33 | isActive: () => editor.can().redo(),
34 | icon: 'Redo2',
35 | tooltip: t('editor.redo.tooltip'),
36 | }
37 | }
38 | };
39 | },
40 | };
41 | },
42 | });
43 |
44 | export * from './components/RichTextHistory';
45 |
--------------------------------------------------------------------------------
/src/components/RichTextProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useId } from 'react';
2 |
3 | import { type Editor } from '@tiptap/core';
4 |
5 | import { TooltipProvider } from '@/components';
6 | import { RESET_CSS } from '@/constants/resetCSS';
7 | import { ProviderUniqueId } from '@/store/ProviderUniqueId';
8 | import { removeCSS, updateCSS } from '@/utils/dynamicCSS';
9 |
10 | import '../styles/index.scss';
11 | import { EditorContext } from '@tiptap/react';
12 |
13 | interface IProviderRichTextProps {
14 | editor: Editor
15 | children: React.ReactNode
16 | dark?: boolean
17 | }
18 |
19 | export function RichTextProvider({ editor, children }: IProviderRichTextProps) {
20 | const id = useId();
21 |
22 | useEffect(() => {
23 | // if (props?.resetCSS !== false) {
24 | updateCSS(RESET_CSS, 'react-tiptap-reset');
25 | // }
26 |
27 | return () => {
28 | removeCSS('react-tiptap-reset');
29 | };
30 | }, []);
31 |
32 | useEffect(() => {
33 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
34 | //@ts-expect-error
35 | if (editor) editor.id = id;
36 | }, [id, editor]);
37 |
38 | if (!editor) {
39 | return <>>;
40 | }
41 |
42 | return (
43 |
44 |
45 |
49 |
52 | {children}
53 |
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/extensions/HorizontalRule/HorizontalRule.ts:
--------------------------------------------------------------------------------
1 | import { mergeAttributes } from '@tiptap/core';
2 | import type { HorizontalRuleOptions as TiptapHorizontalRuleOptions } from '@tiptap/extension-horizontal-rule';
3 | import { HorizontalRule as TiptapHorizontalRule } from '@tiptap/extension-horizontal-rule';
4 |
5 | import { ActionButton } from '@/components';
6 | import type { GeneralOptions } from '@/types';
7 | export * from './components/RichTextHorizontalRule';
8 |
9 | export interface HorizontalRuleOptions
10 | extends TiptapHorizontalRuleOptions,
11 | GeneralOptions {}
12 |
13 | export const HorizontalRule = /* @__PURE__ */ TiptapHorizontalRule.extend({
14 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
15 | //@ts-expect-error
16 | addOptions() {
17 | return {
18 | ...this.parent?.(),
19 | button: ({ editor, t, extension }) => ({
20 | component: ActionButton,
21 | componentProps: {
22 | action: () => editor.commands.setHorizontalRule(),
23 | disabled: !editor.can().setHorizontalRule(),
24 | icon: 'Minus',
25 | shortcutKeys: extension.options.shortcutKeys ?? ['mod', 'alt', 'S'],
26 | tooltip: t('editor.horizontalrule.tooltip'),
27 | },
28 | }),
29 | };
30 | },
31 | addKeyboardShortcuts() {
32 | return {
33 | 'Mod-Alt-s': () => this.editor.commands.setHorizontalRule(),
34 | };
35 | },
36 | renderHTML() {
37 | return [
38 | 'div',
39 | mergeAttributes(this.options.HTMLAttributes, {
40 | 'data-type': this.name,
41 | }),
42 | ['hr'],
43 | ];
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/src/extensions/Drawer/components/ControlDrawer/ControlDrawer.module.scss:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | background-color: white;
3 | border-radius: 10px;
4 | cursor: pointer;
5 | position: absolute;
6 | top: 0;
7 | left: 0;
8 | z-index: 1000;
9 | padding: 10px;
10 | gap: 10px;
11 | z-index: 100;
12 | border: 1px solid #eeeeee;
13 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
14 | border-radius: 10px;
15 | }
16 |
17 | .tool {
18 | outline: none;
19 | border: none;
20 | background-color: white;
21 | border-radius: 5px;
22 | padding: 5px;
23 | cursor: pointer;
24 | transition: all 0.3s;
25 | color: rgb(89, 83, 83) !important;
26 | }
27 |
28 |
29 | .tool svg {
30 | width: 20px;
31 | height: 20px;
32 | }
33 |
34 | .tool:hover {
35 | background-color: #f5f5f5;
36 | }
37 |
38 | .active {
39 | background-color: #eeeeee !important;
40 | }
41 |
42 | .pen {
43 | display: flex;
44 | gap: 4px;
45 | align-items: center;
46 | flex-wrap: wrap;
47 | }
48 |
49 | .line {
50 | height: 20px;
51 | width: 1px;
52 | background-color: rgb(188, 185, 185);
53 | margin: 0 6px;
54 | transform: rotate(18deg);
55 | }
56 |
57 | .options {
58 | margin-top: 10px;
59 | padding: 10px 0 0;
60 | border-top: 1px dashed #eeeeee;
61 | display: flex;
62 | gap: 4px;
63 | align-items: center;
64 | flex-wrap: wrap;
65 | }
66 |
67 | .colorWrap {
68 | display: flex;
69 | gap: 8px;
70 | align-items: center;
71 | margin: 4px 0;
72 | }
73 | .color {
74 | outline: none !important;
75 | border: none !important;
76 | border-radius: 4px;
77 | cursor: pointer;
78 | width: 20px;
79 | height: 20px;
80 | border: 1px solid #eeeeee;
81 | }
82 |
83 | .colorActive {
84 | outline: 2px solid #2576B9 !important;
85 | }
86 |
--------------------------------------------------------------------------------
/src/extensions/ImportWord/ImportWord.ts:
--------------------------------------------------------------------------------
1 | import { Extension } from '@tiptap/core';
2 | import type { default as Mammoth } from 'mammoth';
3 |
4 | export * from '@/extensions/ImportWord/components/RichTextImportWord';
5 | import type { GeneralOptions } from '@/types';
6 |
7 | interface ImportWordOptions extends GeneralOptions {
8 | /** Function for converting Word files to HTML */
9 | convert?: (file: File) => Promise
10 |
11 | /** Function for uploading images */
12 | upload?: (files: File[]) => Promise
13 |
14 | /**
15 | * File Size limit(10 MB)
16 | *
17 | * @default 1024 * 1024 * 10
18 | */
19 | limit?: number
20 | mammothOptions?: Parameters[1]
21 | }
22 |
23 | export const ImportWord = /* @__PURE__ */ Extension.create({
24 | name: 'importWord',
25 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
26 | //@ts-expect-error
27 | addOptions() {
28 | return {
29 | ...this.parent?.(),
30 | upload: undefined,
31 | convert: undefined,
32 | limit: 1024 * 1024 * 10, // 10 MB
33 | button: ({ extension, t }) => {
34 | const { convert, limit, mammothOptions } = extension.options;
35 | return {
36 | componentProps: {
37 | convert,
38 | limit,
39 | mammothOptions,
40 | // action: () => editor.commands.setHorizontalRule(),
41 | // disabled: !editor.can().setHorizontalRule(),
42 | icon: 'Word',
43 | shortcutKeys: extension.options.shortcutKeys ?? ['alt', 'mod', 'S'],
44 | tooltip: t('editor.importWord.tooltip'),
45 | },
46 | };
47 | },
48 | };
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/src/components/icons/Symbol.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Symbol() {
4 | return (
5 |
16 | );
17 | }
18 |
19 | export default Symbol;
20 |
--------------------------------------------------------------------------------
/src/utils/plateform.ts:
--------------------------------------------------------------------------------
1 | // We'll cache the result of isMac() and isTouchDevice(), since they shouldn't
2 | // change during a session. That way repeated calls don't require any logic and
3 | // are rapid.
4 | let isMacResult: boolean | undefined;
5 | let isTouchDeviceResult: boolean | undefined;
6 |
7 | /**
8 | * Return true if the user is using a Mac (as opposed to Windows, etc.) device.
9 | */
10 | export function isMac(): boolean {
11 | if (isMacResult === undefined) {
12 | isMacResult = navigator.platform.includes('Mac');
13 | }
14 | return isMacResult;
15 | }
16 |
17 | /**
18 | * 根据 Mac 和非 Mac 平台,返回应该用于键盘快捷键的修饰键的可读版本。用于直观地指示应该按哪个键。
19 | */
20 | export function getShortcutKey(key: string): string {
21 | if (`${key}`.toLowerCase() === 'mod') {
22 | return isMac() ? '⌘' : 'Ctrl';
23 | } else if (`${key}`.toLowerCase() === 'alt') {
24 | return isMac() ? '⌥' : 'Alt';
25 | } else if (`${key}`.toLowerCase() === 'shift') {
26 | return isMac() ? '⇧' : 'Shift';
27 | } else {
28 | return key;
29 | }
30 | }
31 | export function getShortcutKeys(keys: string[]): string {
32 | return keys.map(getShortcutKey).join(' ');
33 | }
34 |
35 | /** Return true if the user is using a touch-based device. */
36 | export function isTouchDevice(): boolean {
37 | if (isTouchDeviceResult === undefined) {
38 | // This technique is taken from
39 | // https://hacks.mozilla.org/2013/04/detecting-touch-its-the-why-not-the-how/
40 | // (and https://stackoverflow.com/a/4819886/4543977)
41 | isTouchDeviceResult
42 | = (window && 'ontouchstart' in window)
43 | || navigator.maxTouchPoints > 0
44 | // @ts-expect-error: msMaxTouchPoints is IE-specific, so needs to be ignored
45 | || navigator.msMaxTouchPoints > 0;
46 | }
47 |
48 | return isTouchDeviceResult;
49 | }
50 |
--------------------------------------------------------------------------------
/src/extensions/TextDirection/TextDirection.ts:
--------------------------------------------------------------------------------
1 | import { Extension } from '@tiptap/core';
2 |
3 | export * from '@/extensions/TextDirection/components/RichTextTextDirection';
4 |
5 | export * from '@/extensions/TextDirection/components/RichTextTextDirection';
6 |
7 | const TextDirection = /* @__PURE__ */ Extension.create({
8 | name: 'richTextTextDirection',
9 | addOptions() {
10 | return {
11 | ...this.parent?.(),
12 | directions: ['auto', 'ltr', 'rtl', 'unset'],
13 | defaultDirection: 'auto',
14 | button({
15 | editor,
16 | extension,
17 | t,
18 | }: {
19 | editor: any
20 | extension: Extension
21 | t: (...args: any[]) => string
22 | }) {
23 | const directions = (extension.options?.directions as any[]) || [];
24 |
25 | const iconMap = {
26 | auto: 'TextDirection',
27 | ltr: 'LeftToRight',
28 | rtl: 'RightToLeft',
29 | unset: 'X',
30 | } as any;
31 |
32 | const items = directions.map(k => ({
33 | title: t(`editor.textDirection.${k}.tooltip`),
34 | value: k,
35 | icon: iconMap[k],
36 | action: () => {
37 | if (k === 'unset') {
38 | editor.commands?.unsetTextDirection?.();
39 | return;
40 | }
41 |
42 | editor.commands?.setTextDirection?.(k);
43 | },
44 | disabled: false,
45 | }));
46 |
47 | return {
48 | componentProps: {
49 | icon: 'TextDirection',
50 | tooltip: t('editor.textDirection.tooltip'),
51 | items,
52 | isActive: () => editor.getAttributes('paragraph'),
53 | },
54 | };
55 | },
56 | };
57 | },
58 | });
59 |
60 | export { TextDirection };
61 |
--------------------------------------------------------------------------------
/src/extensions/ExportPdf/ExportPdf.ts:
--------------------------------------------------------------------------------
1 | import { Extension } from '@tiptap/core';
2 |
3 | import { printEditorContent } from '@/utils/pdf';
4 |
5 | import type { GeneralOptions, PaperSize, PageMargin } from '@/types';
6 |
7 | declare module '@tiptap/core' {
8 | interface Commands {
9 | exportPdf: {
10 | exportToPdf: () => ReturnType
11 | }
12 | }
13 | }
14 |
15 | export interface ExportPdfOptions extends GeneralOptions {
16 | paperSize: PaperSize;
17 | title?: string;
18 | margins: {
19 | top?: PageMargin;
20 | right?: PageMargin;
21 | bottom?: PageMargin;
22 | left?: PageMargin;
23 | };
24 | }
25 |
26 | export * from './components/RichTextExportPdf';
27 |
28 | export const ExportPdf = /* @__PURE__ */ Extension.create({
29 | name: 'exportPdf',
30 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
31 | //@ts-expect-error
32 | addOptions() {
33 | return {
34 | ...this.parent?.(),
35 | paperSize: 'Letter',
36 | title: 'Echo Editor',
37 | margins: {
38 | top: '0.4in',
39 | right: '0.4in',
40 | bottom: '0.4in',
41 | left: '0.4in',
42 | },
43 | button: ({ editor, extension, t }) => ({
44 | componentProps: {
45 | action: () => {
46 | printEditorContent(editor, extension.options);
47 | },
48 | icon: 'ExportPdf',
49 | tooltip: t('editor.exportPdf.tooltip'),
50 | isActive: () => false,
51 | disabled: false,
52 | },
53 | }),
54 | };
55 | },
56 | addCommands() {
57 | return {
58 | exportToPdf:
59 | () =>
60 | ({ editor }) => {
61 | return printEditorContent(editor, this.options);
62 | },
63 | };
64 | },
65 | });
66 |
--------------------------------------------------------------------------------
/src/utils/_event.ts:
--------------------------------------------------------------------------------
1 | import mitt from '@/utils/mitt';
2 |
3 | let event: any;
4 | function getEventEmitter() {
5 | try {
6 | if (!event) {
7 | event = mitt();
8 | }
9 | return event;
10 | } catch {
11 | throw new Error('Error EventEmitter');
12 | }
13 | }
14 |
15 | export const OPEN_COUNT_SETTING_MODAL = 'OPEN_COUNT_SETTING_MODAL';
16 | export const OPEN_LINK_SETTING_MODAL = 'OPEN_LINK_SETTING_MODAL';
17 | export const OPEN_FLOW_SETTING_MODAL = 'OPEN_FLOW_SETTING_MODAL';
18 | export const OPEN_MIND_SETTING_MODAL = 'OPEN_MIND_SETTING_MODAL';
19 | export const OPEN_EXCALIDRAW_SETTING_MODAL = 'OPEN_EXCALIDRAW_SETTING_MODAL';
20 | export const OPEN_DRAWER_SETTING_MODAL = 'OPEN_DRAWER_SETTING_MODAL';
21 |
22 | export function subject(eventName: any, handler: any) {
23 | const event = getEventEmitter();
24 | event.on(eventName, handler);
25 | }
26 |
27 | export function cancelSubject(eventName: any, handler: any) {
28 | const event = getEventEmitter();
29 | event.off(eventName, handler);
30 | }
31 |
32 | export function triggerOpenCountSettingModal(data: any) {
33 | const event = getEventEmitter();
34 | event.emit(OPEN_COUNT_SETTING_MODAL, data);
35 | }
36 |
37 | export function triggerOpenLinkSettingModal(data: any) {
38 | const event = getEventEmitter();
39 | event.emit(OPEN_LINK_SETTING_MODAL, data);
40 | }
41 |
42 | export function triggerOpenFlowSettingModal(data: any) {
43 | const event = getEventEmitter();
44 | event.emit(OPEN_FLOW_SETTING_MODAL, data);
45 | }
46 |
47 | export function triggerOpenMindSettingModal(data: any) {
48 | const event = getEventEmitter();
49 | event.emit(OPEN_MIND_SETTING_MODAL, data);
50 | }
51 |
52 | export function triggerOpenExcalidrawSettingModal(data: any) {
53 | const event = getEventEmitter();
54 | event.emit(OPEN_EXCALIDRAW_SETTING_MODAL, data);
55 | }
56 |
--------------------------------------------------------------------------------
/src/extensions/Twitter/components/RichTextTwitter.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { ActionButton, IconComponent, Popover, PopoverContent, PopoverTrigger } from '@/components';
4 | import FormEditLinkTwitter from '@/extensions/Twitter/components/FormEditLinkTwitter';
5 | import { Twitter } from '@/extensions/Twitter/Twitter';
6 | import { useToggleActive } from '@/hooks/useActive';
7 | import { useButtonProps } from '@/hooks/useButtonProps';
8 | import { useEditorInstance } from '@/store/editor';
9 |
10 | export function RichTextTwitter() {
11 | const [open, setOpen] = useState(false);
12 | const editor = useEditorInstance();
13 |
14 | const buttonProps = useButtonProps(Twitter.name);
15 |
16 | const {
17 | icon = undefined,
18 | tooltip = undefined,
19 | tooltipOptions = {},
20 | action = undefined,
21 | isActive = undefined,
22 | } = buttonProps?.componentProps ?? {};
23 |
24 | const { editorDisabled } = useToggleActive(isActive);
25 |
26 | function onSetLink(src: string) {
27 | if (editorDisabled) return;
28 |
29 | if (action) {
30 | action(src);
31 | setOpen(false);
32 | }
33 | }
34 |
35 | return (
36 |
41 |
42 |
48 |
49 |
50 |
51 |
52 |
57 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/utils/is-mobile.ts:
--------------------------------------------------------------------------------
1 | interface HttpRequestHeadersInterfaceMock {
2 | [id: string]: string | string[] | undefined
3 | }
4 |
5 | interface HttpRequestInterfaceMock {
6 | headers: HttpRequestHeadersInterfaceMock
7 | [id: string]: any
8 | }
9 |
10 | export interface IsMobileOptions {
11 | ua?: string | HttpRequestInterfaceMock
12 | tablet?: boolean
13 | featureDetect?: boolean
14 | }
15 |
16 | const mobileRE
17 | = /(android|bb\d+|meego).+mobile|armv7l|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|samsungbrowser.*mobile|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i;
18 | const notMobileRE = /CrOS/;
19 | const tabletRE = /android|ipad|playbook|silk/i;
20 |
21 | /**
22 | * Determines if the current device is a mobile or tablet device.
23 | * @param opts - Options for the detection.
24 | * @returns `true` if the device is mobile or tablet, `false` otherwise.
25 | */
26 | export function isMobile(opts: IsMobileOptions = {}): boolean {
27 | let ua = opts.ua || (typeof navigator !== 'undefined' && navigator.userAgent);
28 |
29 | if (ua && typeof ua === 'object' && ua.headers && typeof ua.headers['user-agent'] === 'string') {
30 | ua = ua.headers['user-agent'];
31 | }
32 |
33 | if (typeof ua !== 'string') {
34 | return false;
35 | }
36 |
37 | if (mobileRE.test(ua) && !notMobileRE.test(ua)) {
38 | return true;
39 | }
40 |
41 | if (opts.tablet && tabletRE.test(ua)) {
42 | return true;
43 | }
44 |
45 | if (
46 | opts.tablet
47 | && opts.featureDetect
48 | && navigator
49 | && navigator.maxTouchPoints > 1
50 | && ua.includes('Macintosh')
51 | && ua.includes('Safari')
52 | ) {
53 | return true;
54 | }
55 |
56 | return false;
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as TogglePrimitive from '@radix-ui/react-toggle';
5 | import { type VariantProps, cva } from 'class-variance-authority';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const toggleVariants = cva(
10 | 'richtext-inline-flex richtext-items-center richtext-justify-center richtext-rounded-md richtext-text-sm richtext-font-medium richtext-ring-offset-background richtext-transition-colors hover:richtext-bg-muted hover:richtext-text-muted-foreground focus-visible:richtext-outline-none focus-visible:richtext-ring-2 focus-visible:richtext-ring-ring focus-visible:richtext-ring-offset-1 disabled:richtext-pointer-events-none disabled:richtext-opacity-50 data-[state=on]:richtext-bg-accent data-[state=on]:richtext-text-accent-foreground',
11 | {
12 | variants: {
13 | variant: {
14 | default: 'richtext-bg-transparent',
15 | outline:
16 | 'richtext-border richtext-border-input richtext-bg-transparent hover:richtext-bg-accent hover:richtext-text-accent-foreground',
17 | },
18 | size: {
19 | default: 'richtext-h-10 richtext-px-3',
20 | sm: 'richtext-h-9 richtext-px-2',
21 | lg: 'richtext-h-11 richtext-px-5',
22 | },
23 | },
24 | defaultVariants: {
25 | variant: 'default',
26 | size: 'default',
27 | },
28 | },
29 | );
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ));
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName;
44 |
45 | export { Toggle, toggleVariants };
46 |
--------------------------------------------------------------------------------
/src/extensions/TaskList/TaskList.ts:
--------------------------------------------------------------------------------
1 | // import type { TaskItemOptions } from '@tiptap/extension-task-item';
2 | // import { TaskItem } from '@tiptap/extension-task-item';
3 | // import type { TaskListOptions as TiptapTaskListOptions } from '@tiptap/extension-task-list';
4 | // import { TaskList as TiptapTaskList } from '@tiptap/extension-task-list';
5 |
6 | import { TaskList as TiptapTaskList, TaskItem, type TaskItemOptions, type TaskListOptions as TiptapTaskListOptions } from '@tiptap/extension-list';
7 |
8 | import type { GeneralOptions } from '@/types';
9 |
10 | export * from './components/RichTextTaskList';
11 |
12 | /**
13 | * Represents the interface for task list options, extending TiptapTaskListOptions and GeneralOptions.
14 | */
15 | export interface TaskListOptions extends TiptapTaskListOptions, GeneralOptions {
16 | /** options for task items */
17 | taskItem: Partial
18 | }
19 |
20 | export const TaskList = /* @__PURE__ */ TiptapTaskList.extend({
21 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
22 | //@ts-expect-error
23 | addOptions() {
24 | return {
25 | ...this.parent?.(),
26 | HTMLAttributes: {
27 | class: 'task-list',
28 | },
29 | taskItem: {
30 | HTMLAttributes: {
31 | class: 'task-list-item',
32 | },
33 | },
34 | button: ({ editor, t, extension }) => ({
35 | componentProps: {
36 | action: () => editor.commands.toggleTaskList(),
37 | isActive: () => editor.isActive('taskList'),
38 | disabled: false,
39 | icon: 'ListTodo',
40 | shortcutKeys: extension.options.shortcutKeys ?? ['shift', 'mod', '9'],
41 | tooltip: t('editor.tasklist.tooltip'),
42 | },
43 | }),
44 | };
45 | },
46 |
47 | addExtensions() {
48 | return [TaskItem.configure(this.options.taskItem)];
49 | },
50 | });
51 |
--------------------------------------------------------------------------------
/src/extensions/Color/components/RichTextColor.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import { ActionButton, ColorPicker } from '@/components';
4 | import { IconComponent } from '@/components/icons';
5 | import { IconColorFill } from '@/components/icons/IconColorFill';
6 | import { Color } from '@/extensions/Color/Color';
7 | import { useActive } from '@/hooks/useActive';
8 | import { useButtonProps } from '@/hooks/useButtonProps';
9 |
10 | export function RichTextColor() {
11 | const buttonProps = useButtonProps(Color.name);
12 |
13 | const {
14 | tooltip = undefined,
15 | isActive = undefined,
16 | defaultColor = undefined,
17 | colors,
18 | action
19 | } = buttonProps?.componentProps ?? {};
20 |
21 | const { disabled, dataState } = useActive(isActive);
22 |
23 | const [selectedColor, setSelectedColor] = useState(defaultColor);
24 |
25 | useEffect(() => {
26 | setSelectedColor(dataState);
27 | }, [dataState]);
28 |
29 | function onChange(color: any) {
30 | if (disabled) return;
31 |
32 | if (action) {
33 | action?.(color);
34 | setSelectedColor(color);
35 | }
36 | }
37 |
38 | if (!buttonProps) {
39 | return <>>;
40 | }
41 |
42 | return (
43 |
49 |
54 |
55 |
56 |
57 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/docs/extensions/Clear/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Clear
3 |
4 | next:
5 | text: Code
6 | link: /extensions/Code/index.md
7 | ---
8 |
9 | # Clear
10 |
11 | The Clear extension allows you to clear the editor content.
12 |
13 | ## Usage
14 |
15 |
16 | ```tsx
17 | import { RichTextProvider } from 'reactjs-tiptap-editor'
18 |
19 | // Base Kit
20 | import { Document } from '@tiptap/extension-document'
21 | import { Text } from '@tiptap/extension-text'
22 | import { Paragraph } from '@tiptap/extension-paragraph'
23 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
24 | import { HardBreak } from '@tiptap/extension-hard-break'
25 | import { TextStyle } from '@tiptap/extension-text-style';
26 | import { ListItem } from '@tiptap/extension-list';
27 |
28 | // Extension
29 | import { Clear, RichTextClear } from 'reactjs-tiptap-editor/clear'; // [!code ++]
30 | // ... other extensions
31 |
32 |
33 | // Import CSS
34 | import 'reactjs-tiptap-editor/style.css';
35 |
36 | const extensions = [
37 | // Base Extensions
38 | Document,
39 | Text,
40 | Dropcursor,
41 | Gapcursor,
42 | HardBreak,
43 | Paragraph,
44 | TrailingNode,
45 | ListItem,
46 | TextStyle,
47 | Placeholder.configure({
48 | placeholder: 'Press \'/\' for commands',
49 | })
50 |
51 | ...
52 | // Import Extensions Here
53 | Clear// [!code ++]
54 | ];
55 |
56 | const RichTextToolbar = () => {
57 | return (
58 |
59 | {/* [!code ++] */}
60 |
61 | )
62 | }
63 |
64 | const App = () => {
65 | const editor = useEditor({
66 | textDirection: 'auto', // global text direction
67 | extensions,
68 | });
69 |
70 | return (
71 |
74 |
75 |
76 |
79 |
80 | );
81 | };
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/extensions/Emoji/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Emoji
3 |
4 | next:
5 | text: Excalidraw
6 | link: /extensions/Excalidraw/index.md
7 | ---
8 |
9 | # Emoji
10 |
11 | The Document extension allows you to add a emoji to your editor.
12 |
13 | ## Usage
14 |
15 |
16 | ```tsx
17 | import { RichTextProvider } from 'reactjs-tiptap-editor'
18 |
19 | // Base Kit
20 | import { Document } from '@tiptap/extension-document'
21 | import { Text } from '@tiptap/extension-text'
22 | import { Paragraph } from '@tiptap/extension-paragraph'
23 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
24 | import { HardBreak } from '@tiptap/extension-hard-break'
25 | import { TextStyle } from '@tiptap/extension-text-style';
26 | import { ListItem } from '@tiptap/extension-list';
27 |
28 | // Extension
29 | import { Emoji, RichTextEmoji } from 'reactjs-tiptap-editor/emoji'; // [!code ++]
30 | // ... other extensions
31 |
32 |
33 | // Import CSS
34 | import 'reactjs-tiptap-editor/style.css';
35 |
36 | const extensions = [
37 | // Base Extensions
38 | Document,
39 | Text,
40 | Dropcursor,
41 | Gapcursor,
42 | HardBreak,
43 | Paragraph,
44 | TrailingNode,
45 | ListItem,
46 | TextStyle,
47 | Placeholder.configure({
48 | placeholder: 'Press \'/\' for commands',
49 | })
50 |
51 | ...
52 | // Import Extensions Here
53 | Emoji// [!code ++]
54 | ];
55 |
56 | const RichTextToolbar = () => {
57 | return (
58 |
59 | {/* [!code ++] */}
60 |
61 | )
62 | }
63 |
64 | const App = () => {
65 | const editor = useEditor({
66 | textDirection: 'auto', // global text direction
67 | extensions,
68 | });
69 |
70 | return (
71 |
74 |
75 |
76 |
79 |
80 | );
81 | };
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/extensions/Iframe/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Iframe
3 |
4 | next:
5 | text: Image
6 | link: /extensions/Image/index.md
7 | ---
8 |
9 | # Iframe
10 |
11 | The Iframe extension allows you to add an Iframe to your editor.
12 |
13 | ## Usage
14 |
15 |
16 | ```tsx
17 | import { RichTextProvider } from 'reactjs-tiptap-editor'
18 |
19 | // Base Kit
20 | import { Document } from '@tiptap/extension-document'
21 | import { Text } from '@tiptap/extension-text'
22 | import { Paragraph } from '@tiptap/extension-paragraph'
23 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
24 | import { HardBreak } from '@tiptap/extension-hard-break'
25 | import { TextStyle } from '@tiptap/extension-text-style';
26 | import { ListItem } from '@tiptap/extension-list';
27 |
28 | // Extension
29 | import { Iframe, RichTextIframe } from 'reactjs-tiptap-editor/iframe'; // [!code ++]
30 | // ... other extensions
31 |
32 |
33 | // Import CSS
34 | import 'reactjs-tiptap-editor/style.css';
35 |
36 | const extensions = [
37 | // Base Extensions
38 | Document,
39 | Text,
40 | Dropcursor,
41 | Gapcursor,
42 | HardBreak,
43 | Paragraph,
44 | TrailingNode,
45 | ListItem,
46 | TextStyle,
47 | Placeholder.configure({
48 | placeholder: 'Press \'/\' for commands',
49 | })
50 |
51 | ...
52 | // Import Extensions Here
53 | Iframe// [!code ++]
54 | ];
55 |
56 | const RichTextToolbar = () => {
57 | return (
58 |
59 | {/* [!code ++] */}
60 |
61 | )
62 | }
63 |
64 | const App = () => {
65 | const editor = useEditor({
66 | textDirection: 'auto', // global text direction
67 | extensions,
68 | });
69 |
70 | return (
71 |
74 |
75 |
76 |
79 |
80 | );
81 | };
82 | ```
83 |
--------------------------------------------------------------------------------
/src/extensions/Highlight/components/RichTextHighlight.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { ActionButton, ColorPicker } from '@/components';
4 | import { IconComponent } from '@/components/icons';
5 | import { IconHighlightFill } from '@/components/icons/IconHighlightFill';
6 | import { Highlight } from '@/extensions/Highlight/Highlight';
7 | import { useActive } from '@/hooks/useActive';
8 | import { useButtonProps } from '@/hooks/useButtonProps';
9 |
10 | export function RichTextHighlight() {
11 | const buttonProps = useButtonProps(Highlight.name);
12 |
13 | const {
14 | tooltip = undefined,
15 | isActive = undefined,
16 | defaultColor = undefined,
17 | colors,
18 | action,
19 | shortcutKeys
20 | } = buttonProps?.componentProps ?? {};
21 |
22 | const { editorDisabled } = useActive(isActive);
23 |
24 | const [selectedColor, setSelectedColor] = useState(defaultColor);
25 |
26 | function onChange(color: any) {
27 | if (editorDisabled) return;
28 |
29 | if (action) {
30 | action?.(color);
31 | setSelectedColor(color);
32 | }
33 | }
34 |
35 | if (!buttonProps) {
36 | return <>>;
37 | }
38 |
39 | return (
40 |
47 |
53 |
54 |
55 |
56 |
59 |
60 |
61 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/docs/extensions/ExportWord/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Export Word
3 |
4 | next:
5 | text: FontFamily
6 | link: /extensions/FontFamily/index.md
7 | ---
8 |
9 | # Export Word
10 |
11 | - Export Word Extension for Tiptap Editor.
12 |
13 | ## Usage
14 |
15 |
16 | ```tsx
17 | import { RichTextProvider } from 'reactjs-tiptap-editor'
18 |
19 | // Base Kit
20 | import { Document } from '@tiptap/extension-document'
21 | import { Text } from '@tiptap/extension-text'
22 | import { Paragraph } from '@tiptap/extension-paragraph'
23 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
24 | import { HardBreak } from '@tiptap/extension-hard-break'
25 | import { TextStyle } from '@tiptap/extension-text-style';
26 | import { ListItem } from '@tiptap/extension-list';
27 |
28 | // Extension
29 | import { ExportWord, RichTextExportWord } from 'reactjs-tiptap-editor/exportword'; // [!code ++]
30 | // ... other extensions
31 |
32 |
33 | // Import CSS
34 | import 'reactjs-tiptap-editor/style.css';
35 |
36 | const extensions = [
37 | // Base Extensions
38 | Document,
39 | Text,
40 | Dropcursor,
41 | Gapcursor,
42 | HardBreak,
43 | Paragraph,
44 | TrailingNode,
45 | ListItem,
46 | TextStyle,
47 | Placeholder.configure({
48 | placeholder: 'Press \'/\' for commands',
49 | })
50 |
51 | ...
52 | // Import Extensions Here
53 | ExportWord// [!code ++]
54 | ];
55 |
56 | const RichTextToolbar = () => {
57 | return (
58 |
59 | {/* [!code ++] */}
60 |
61 | )
62 | }
63 |
64 | const App = () => {
65 | const editor = useEditor({
66 | textDirection: 'auto', // global text direction
67 | extensions,
68 | });
69 |
70 | return (
71 |
74 |
75 |
76 |
79 |
80 | );
81 | };
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/guide/toolbar.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Toolbar
3 |
4 | next:
5 | text: Bubble Menu
6 | link: /guide/bubble-menu.md
7 | ---
8 |
9 | # Toolbar
10 |
11 | Toolbar is a component that is used to display buttons that are used to perform actions on the editor.
12 |
13 | ## Usage
14 |
15 | ```tsx
16 | import { RichTextProvider } from 'reactjs-tiptap-editor'
17 |
18 | // Base Kit
19 | import { Document } from '@tiptap/extension-document'
20 | import { Text } from '@tiptap/extension-text'
21 | import { Paragraph } from '@tiptap/extension-paragraph'
22 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
23 | import { HardBreak } from '@tiptap/extension-hard-break'
24 | import { TextStyle } from '@tiptap/extension-text-style';
25 | import { ListItem } from '@tiptap/extension-list';
26 |
27 | // Extension
28 | import { History, RichTextUndo, RichTextRedo } from 'reactjs-tiptap-editor/history';
29 | // ... other extensions
30 |
31 |
32 | // Import CSS
33 | import 'reactjs-tiptap-editor/style.css';
34 |
35 | const extensions = [
36 | // Base Extensions
37 | Document,
38 | Text,
39 | Dropcursor,
40 | Gapcursor,
41 | HardBreak,
42 | Paragraph,
43 | TrailingNode,
44 | ListItem,
45 | TextStyle,
46 | Placeholder.configure({
47 | placeholder: 'Press \'/\' for commands',
48 | })
49 |
50 | ...
51 | // Import Extensions Here
52 | History
53 | ];
54 |
55 | const RichTextToolbar = () => {
56 | return (
57 |
58 |
59 |
60 |
61 | )
62 | }
63 |
64 | const App = () => {
65 | const editor = useEditor({
66 | textDirection: 'auto', // global text direction
67 | extensions,
68 | });
69 |
70 | return (
71 |
74 |
75 |
76 |
79 |
80 | );
81 | };
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/extensions/CodeView/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: CodeView
3 |
4 | next:
5 | text: Color
6 | link: /extensions/Color/index.md
7 | ---
8 |
9 | # CodeView
10 |
11 | - The `CodeView` extension allows you view html code of the editor.
12 |
13 | ## Usage
14 |
15 |
16 | ```tsx
17 | import { RichTextProvider } from 'reactjs-tiptap-editor'
18 |
19 | // Base Kit
20 | import { Document } from '@tiptap/extension-document'
21 | import { Text } from '@tiptap/extension-text'
22 | import { Paragraph } from '@tiptap/extension-paragraph'
23 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
24 | import { HardBreak } from '@tiptap/extension-hard-break'
25 | import { TextStyle } from '@tiptap/extension-text-style';
26 | import { ListItem } from '@tiptap/extension-list';
27 |
28 | // Extension
29 | import { CodeView, RichTextCodeView } from 'reactjs-tiptap-editor/codeview'; // [!code ++]
30 | // ... other extensions
31 |
32 |
33 | // Import CSS
34 | import 'reactjs-tiptap-editor/style.css';
35 |
36 | const extensions = [
37 | // Base Extensions
38 | Document,
39 | Text,
40 | Dropcursor,
41 | Gapcursor,
42 | HardBreak,
43 | Paragraph,
44 | TrailingNode,
45 | ListItem,
46 | TextStyle,
47 | Placeholder.configure({
48 | placeholder: 'Press \'/\' for commands',
49 | })
50 |
51 | ...
52 | // Import Extensions Here
53 | CodeView// [!code ++]
54 | ];
55 |
56 | const RichTextToolbar = () => {
57 | return (
58 |
59 | {/* [!code ++] */}
60 |
61 | )
62 | }
63 |
64 | const App = () => {
65 | const editor = useEditor({
66 | textDirection: 'auto', // global text direction
67 | extensions,
68 | });
69 |
70 | return (
71 |
74 |
75 |
76 |
79 |
80 | );
81 | };
82 | ```
83 |
84 |
85 |
--------------------------------------------------------------------------------
/src/store/fast-context.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useRef,
3 | createContext,
4 | useContext,
5 | useCallback,
6 | useSyncExternalStore,
7 | } from 'react';
8 |
9 | export default function createFastContext(initialState: Store) {
10 | function useStoreData(): {
11 | get: () => Store;
12 | set: (value: Partial) => void;
13 | subscribe: (callback: () => void) => () => void; } {
14 | const store = useRef(initialState);
15 |
16 | const get = useCallback(() => store.current, []);
17 |
18 | const subscribers = useRef(new Set<() => void>());
19 |
20 | const set = useCallback((value: Partial) => {
21 | store.current = { ...store.current, ...value };
22 | subscribers.current.forEach((callback) => callback());
23 | }, []);
24 |
25 | const subscribe = useCallback((callback: () => void) => {
26 | subscribers.current.add(callback);
27 | return () => subscribers.current.delete(callback);
28 | }, []);
29 |
30 | return {
31 | get,
32 | set,
33 | subscribe,
34 | };
35 | }
36 |
37 | type UseStoreDataReturnType = ReturnType;
38 |
39 | const StoreContext = createContext(null);
40 |
41 | function Provider({ children }: { children: React.ReactNode }) {
42 | return (
43 |
44 | {children}
45 |
46 | );
47 | }
48 |
49 | function useStore(
50 | selector: (store: Store) => SelectorOutput
51 | ): [SelectorOutput, (value: Partial) => void] {
52 | const store = useContext(StoreContext);
53 | if (!store) {
54 | throw new Error('Store not found');
55 | }
56 |
57 | const state = useSyncExternalStore(
58 | store.subscribe,
59 | () => selector(store.get()),
60 | () => selector(initialState),
61 | );
62 |
63 | return [state, store.set];
64 | }
65 |
66 | return {
67 | Provider,
68 | useStore,
69 | };
70 | }
71 |
--------------------------------------------------------------------------------
/docs/extensions/Drawer/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Drawer
3 |
4 |
5 | next:
6 | text: Emoji
7 | link: /extensions/Emoji/index.md
8 | ---
9 |
10 | # Drawer
11 |
12 | Drawer is a node extension that allows you to add an Drawer to your editor.
13 |
14 | - [Drawer](https://easydrawer.vercel.app/)
15 |
16 | ## Usage
17 |
18 |
19 | ```tsx
20 | import { RichTextProvider } from 'reactjs-tiptap-editor'
21 |
22 | // Base Kit
23 | import { Document } from '@tiptap/extension-document'
24 | import { Text } from '@tiptap/extension-text'
25 | import { Paragraph } from '@tiptap/extension-paragraph'
26 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
27 | import { HardBreak } from '@tiptap/extension-hard-break'
28 | import { TextStyle } from '@tiptap/extension-text-style';
29 | import { ListItem } from '@tiptap/extension-list';
30 |
31 | // Extension
32 | import { Drawer, RichTextDrawer } from 'reactjs-tiptap-editor/drawer'; // [!code ++]
33 | // ... other extensions
34 |
35 |
36 | // Import CSS
37 | import 'reactjs-tiptap-editor/style.css';
38 |
39 | const extensions = [
40 | // Base Extensions
41 | Document,
42 | Text,
43 | Dropcursor,
44 | Gapcursor,
45 | HardBreak,
46 | Paragraph,
47 | TrailingNode,
48 | ListItem,
49 | TextStyle,
50 | Placeholder.configure({
51 | placeholder: 'Press \'/\' for commands',
52 | })
53 |
54 | ...
55 | // Import Extensions Here
56 | Drawer// [!code ++]
57 | ];
58 |
59 | const RichTextToolbar = () => {
60 | return (
61 |
62 | {/* [!code ++] */}
63 |
64 | )
65 | }
66 |
67 | const App = () => {
68 | const editor = useEditor({
69 | textDirection: 'auto', // global text direction
70 | extensions,
71 | });
72 |
73 | return (
74 |
77 |
78 |
79 |
82 |
83 | );
84 | };
85 | ```
86 |
--------------------------------------------------------------------------------
/src/components/icons/Animas.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Animal() {
4 | return (
5 |
20 |
21 | );
22 | }
23 |
24 | export default Animal;
25 |
--------------------------------------------------------------------------------
/src/extensions/LineHeight/components/RichTextLightHeight.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 |
3 | import {
4 | ActionButton,
5 | DropdownMenu,
6 | DropdownMenuCheckboxItem,
7 | DropdownMenuContent,
8 | DropdownMenuSeparator,
9 | DropdownMenuTrigger,
10 | IconComponent,
11 | } from '@/components';
12 | import { LineHeight } from '@/extensions/LineHeight/LineHeight';
13 | import { useActive } from '@/hooks/useActive';
14 | import { useButtonProps } from '@/hooks/useButtonProps';
15 |
16 | export function RichTextLineHeight() {
17 | const buttonProps = useButtonProps(LineHeight.name);
18 |
19 | const {
20 | tooltip = undefined,
21 | items,
22 | icon,
23 | isActive = undefined,
24 | } = buttonProps?.componentProps ?? {};
25 |
26 | const { editorDisabled, dataState } = useActive(isActive);
27 |
28 | if (!buttonProps) {
29 | return <>>;
30 | }
31 |
32 | return (
33 |
34 |
37 |
43 |
46 |
47 |
48 |
49 |
50 | {items?.map((item: any, index: any) => {
51 | return (
52 |
53 | item?.action()}
56 | >
57 | {item.label}
58 |
59 |
60 | {item.value === 'Default' && }
61 |
62 | );
63 | })}
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/docs/extensions/SlashCommand/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: SlashCommand
3 |
4 | next:
5 | text: Strike
6 | link: /extensions/Strike/index.md
7 | ---
8 |
9 | # Slash Command
10 |
11 | The Slash Command extension allows you to add slash commands to your editor.
12 | - type `/` to trigger the slash command menu.
13 |
14 | ## Usage
15 |
16 |
17 | ```tsx
18 | import { RichTextProvider } from 'reactjs-tiptap-editor'
19 |
20 | // Base Kit
21 | import { Document } from '@tiptap/extension-document'
22 | import { Text } from '@tiptap/extension-text'
23 | import { Paragraph } from '@tiptap/extension-paragraph'
24 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
25 | import { HardBreak } from '@tiptap/extension-hard-break'
26 | import { TextStyle } from '@tiptap/extension-text-style';
27 | import { ListItem } from '@tiptap/extension-list';
28 |
29 | // Extension
30 | import { SlashCommand, SlashCommandList } from 'reactjs-tiptap-editor/slashcommand'; // [!code ++]
31 | // ... other extensions
32 |
33 | // Import CSS
34 | import 'reactjs-tiptap-editor/style.css';
35 |
36 | const extensions = [
37 | // Base Extensions
38 | Document,
39 | Text,
40 | Dropcursor,
41 | Gapcursor,
42 | HardBreak,
43 | Paragraph,
44 | TrailingNode,
45 | ListItem,
46 | TextStyle,
47 | Placeholder.configure({
48 | placeholder: 'Press \'/\' for commands',
49 | })
50 |
51 | ...
52 | // Import Extensions Here
53 | SlashCommand// [!code ++]
54 | ];
55 |
56 | const RichTextToolbar = () => {
57 | return (
58 |
59 | {/* [!code ++] */}
60 |
61 | )
62 | }
63 |
64 | const App = () => {
65 | const editor = useEditor({
66 | textDirection: 'auto', // global text direction
67 | extensions,
68 | });
69 |
70 | return (
71 |
74 |
75 |
76 |
79 |
80 | );
81 | };
82 | ```
83 |
--------------------------------------------------------------------------------
/src/extensions/History/components/RichTextHistory.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2 | import React from 'react';
3 |
4 | import { ActionButton, icons } from '@/components';
5 | import { History } from '@/extensions/History/History';
6 | import { useActive } from '@/hooks/useActive';
7 | import { useButtonProps } from '@/hooks/useButtonProps';
8 |
9 | export function RichTextUndo() {
10 | const buttonProps = useButtonProps(History.name);
11 |
12 | const {
13 | icon = undefined,
14 | tooltip = undefined,
15 | shortcutKeys = undefined,
16 | tooltipOptions = {},
17 | action = undefined,
18 | isActive = undefined,
19 | } = buttonProps?.componentProps?.undo ?? {};
20 |
21 | const { disabled } = useActive(isActive);
22 |
23 | const Icon = icons[icon as string];
24 |
25 | const onAction = () => {
26 | if (disabled) return;
27 |
28 | if (action) action();
29 | };
30 |
31 | if (!buttonProps || !Icon) {
32 | return <>>;
33 | }
34 |
35 | return (
36 |
44 | );
45 | }
46 |
47 | export function RichTextRedo() {
48 | const buttonProps = useButtonProps(History.name);
49 |
50 | const {
51 | icon = undefined,
52 | tooltip = undefined,
53 | shortcutKeys = undefined,
54 | tooltipOptions = {},
55 | action = undefined,
56 | isActive = undefined,
57 | } = buttonProps?.componentProps?.redo ?? {};
58 |
59 | const { disabled } = useActive(isActive);
60 |
61 | const onAction = () => {
62 | if (disabled) return;
63 |
64 | if (action) action();
65 | };
66 |
67 | if (!buttonProps) {
68 | return <>>;
69 | }
70 |
71 | return (
72 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/docs/extensions/Excalidraw/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Excalidraw
3 |
4 | next:
5 | text: ExportPdf
6 | link: /extensions/ExportPdf/index.md
7 | ---
8 |
9 | # Excalidraw
10 |
11 | The Excalidraw extension allows you to add an Excalidraw to your editor.
12 |
13 | - Based on Excalidraw. [Excalidraw](https://excalidraw.com/)
14 |
15 | ## Usage
16 |
17 |
18 | ```tsx
19 | import { RichTextProvider } from 'reactjs-tiptap-editor'
20 |
21 | // Base Kit
22 | import { Document } from '@tiptap/extension-document'
23 | import { Text } from '@tiptap/extension-text'
24 | import { Paragraph } from '@tiptap/extension-paragraph'
25 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
26 | import { HardBreak } from '@tiptap/extension-hard-break'
27 | import { TextStyle } from '@tiptap/extension-text-style';
28 | import { ListItem } from '@tiptap/extension-list';
29 |
30 | // Extension
31 | import { Excalidraw, RichTextExcalidraw } from 'reactjs-tiptap-editor/excalidraw'; // [!code ++]
32 | // ... other extensions
33 |
34 |
35 | // Import CSS
36 | import 'reactjs-tiptap-editor/style.css';
37 |
38 | const extensions = [
39 | // Base Extensions
40 | Document,
41 | Text,
42 | Dropcursor,
43 | Gapcursor,
44 | HardBreak,
45 | Paragraph,
46 | TrailingNode,
47 | ListItem,
48 | TextStyle,
49 | Placeholder.configure({
50 | placeholder: 'Press \'/\' for commands',
51 | })
52 |
53 | ...
54 | // Import Extensions Here
55 | Excalidraw// [!code ++]
56 | ];
57 |
58 | const RichTextToolbar = () => {
59 | return (
60 |
61 | {/* [!code ++] */}
62 |
63 | )
64 | }
65 |
66 | const App = () => {
67 | const editor = useEditor({
68 | textDirection: 'auto', // global text direction
69 | extensions,
70 | });
71 |
72 | return (
73 |
76 |
77 |
78 |
81 |
82 | );
83 | };
84 | ```
85 |
--------------------------------------------------------------------------------
/src/extensions/Link/components/RichTextLink.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { ActionButton, IconComponent, Popover, PopoverContent, PopoverTrigger } from '@/components';
4 | import LinkEditBlock from '@/extensions/Link/components/LinkEditBlock';
5 | import { Link } from '@/extensions/Link/Link';
6 | import { useToggleActive } from '@/hooks/useActive';
7 | import { useButtonProps } from '@/hooks/useButtonProps';
8 | import { useEditorInstance } from '@/store/editor';
9 |
10 | export function RichTextLink() {
11 | const [open, setOpen] = useState(false);
12 | const editor = useEditorInstance();
13 | const buttonProps = useButtonProps(Link.name);
14 |
15 | const {
16 | isActive,
17 | icon,
18 | tooltip,
19 | target,
20 | action
21 | } = buttonProps?.componentProps ?? {};
22 |
23 | const { dataState, editorDisabled, update } = useToggleActive(isActive);
24 |
25 | function onSetLink(link: string, text?: string, openInNewTab?: boolean) {
26 | if (editorDisabled) return;
27 |
28 | if (action) {
29 | action({ link, text, openInNewTab });
30 | setOpen(false);
31 | update();
32 | }
33 | }
34 |
35 | if (!buttonProps) {
36 | return <>>;
37 | }
38 |
39 | return (
40 |
44 |
48 |
53 |
54 |
55 |
56 |
57 |
62 |
67 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/store/editor.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { type Editor } from '@tiptap/core';
4 | import { useCurrentEditor, useEditorState as useEditorStateTiptap } from '@tiptap/react';
5 |
6 | /**
7 | * Hook that provides access to a Tiptap editor instance.
8 | *
9 | * Accepts an optional editor instance directly, or falls back to retrieving
10 | * the editor from the Tiptap context if available. This allows components
11 | * to work both when given an editor directly and when used within a Tiptap
12 | * editor context.
13 | *
14 | * @param providedEditor - Optional editor instance to use instead of the context editor
15 | * @returns The provided editor or the editor from context, whichever is available
16 | */
17 | export function useTiptapEditor(providedEditor?: Editor | null): {
18 | editor: Editor | null
19 | editorState?: Editor['state']
20 | canCommand?: Editor['can']
21 | } {
22 | const { editor: coreEditor } = useCurrentEditor();
23 | const mainEditor = React.useMemo(
24 | () => providedEditor || coreEditor,
25 | [providedEditor, coreEditor]
26 | );
27 |
28 | const editorState = useEditorStateTiptap({
29 | editor: mainEditor,
30 | selector(context) {
31 | if (!context.editor) {
32 | return {
33 | editor: null,
34 | editorState: undefined,
35 | canCommand: undefined,
36 | };
37 | }
38 |
39 | return {
40 | editor: context.editor,
41 | editorState: context.editor.state,
42 | canCommand: context.editor.can,
43 | };
44 | },
45 | });
46 |
47 | return editorState || { editor: null };
48 | }
49 |
50 | function useEditorInstance () {
51 | const editor = useTiptapEditor().editor;
52 | return editor as Editor;
53 | }
54 |
55 | function useEditorState () {
56 | const editorState = useTiptapEditor().editorState;
57 | return editorState;
58 | }
59 |
60 | function useCanCommand () {
61 | const canCommand = useTiptapEditor().canCommand;
62 | return canCommand;
63 | }
64 |
65 | export {
66 | useEditorInstance,
67 | useEditorState,
68 | useCanCommand,
69 | };
70 |
--------------------------------------------------------------------------------
/docs/extensions/Link/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Link
3 |
4 | next:
5 | text: Mention
6 | link: /extensions/Mention/index.md
7 | ---
8 |
9 | # Link
10 |
11 | Link is a node extension that allows you to add a horizontal rule to your editor.
12 |
13 | - Based on TipTap's Link extension. [@tiptap/extension-link](https://tiptap.dev/docs/editor/extensions/marks/link)
14 |
15 | ## Usage
16 |
17 |
18 | ```tsx
19 | import { RichTextProvider } from 'reactjs-tiptap-editor'
20 |
21 | // Base Kit
22 | import { Document } from '@tiptap/extension-document'
23 | import { Text } from '@tiptap/extension-text'
24 | import { Paragraph } from '@tiptap/extension-paragraph'
25 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
26 | import { HardBreak } from '@tiptap/extension-hard-break'
27 | import { TextStyle } from '@tiptap/extension-text-style';
28 | import { ListItem } from '@tiptap/extension-list';
29 |
30 | // Extension
31 | import { Link, RichTextLink } from 'reactjs-tiptap-editor/link'; // [!code ++]
32 | // ... other extensions
33 |
34 |
35 | // Import CSS
36 | import 'reactjs-tiptap-editor/style.css';
37 |
38 | const extensions = [
39 | // Base Extensions
40 | Document,
41 | Text,
42 | Dropcursor,
43 | Gapcursor,
44 | HardBreak,
45 | Paragraph,
46 | TrailingNode,
47 | ListItem,
48 | TextStyle,
49 | Placeholder.configure({
50 | placeholder: 'Press \'/\' for commands',
51 | })
52 |
53 | ...
54 | // Import Extensions Here
55 | Link// [!code ++]
56 | ];
57 |
58 | const RichTextToolbar = () => {
59 | return (
60 |
61 | {/* [!code ++] */}
62 |
63 | )
64 | }
65 |
66 | const App = () => {
67 | const editor = useEditor({
68 | textDirection: 'auto', // global text direction
69 | extensions,
70 | });
71 |
72 | return (
73 |
76 |
77 |
78 |
81 |
82 | );
83 | };
84 | ```
85 |
--------------------------------------------------------------------------------
/docs/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: How to install reactjs-tiptap-editor
3 |
4 | next:
5 | text: Toolbar
6 | link: /guide/toolbar.md
7 | ---
8 |
9 | # Installation
10 |
11 | ::: code-group
12 |
13 | ```sh [npm]
14 | npm install reactjs-tiptap-editor@latest
15 | ```
16 |
17 | ```sh [pnpm]
18 | pnpm install reactjs-tiptap-editor@latest
19 | ```
20 |
21 | ```sh [yarn]
22 | yarn add reactjs-tiptap-editor@latest
23 | ```
24 | :::
25 |
26 | ## Usage
27 |
28 | ```tsx
29 | import { RichTextProvider } from 'reactjs-tiptap-editor'
30 |
31 | // Base Kit
32 | import { Document } from '@tiptap/extension-document'
33 | import { Text } from '@tiptap/extension-text'
34 | import { Paragraph } from '@tiptap/extension-paragraph'
35 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
36 | import { HardBreak } from '@tiptap/extension-hard-break'
37 | import { TextStyle } from '@tiptap/extension-text-style';
38 | import { ListItem } from '@tiptap/extension-list';
39 |
40 |
41 | // Import CSS
42 | import 'reactjs-tiptap-editor/style.css';
43 |
44 | const extensions = [
45 | // Base Extensions
46 | Document,
47 | Text,
48 | Dropcursor,
49 | Gapcursor,
50 | HardBreak,
51 | Paragraph,
52 | TrailingNode,
53 | ListItem,
54 | TextStyle,
55 | Placeholder.configure({
56 | placeholder: 'Press \'/\' for commands',
57 | })
58 |
59 | ...
60 | // Import Extensions Here
61 | ];
62 |
63 | const App = () => {
64 | const editor = useEditor({
65 | textDirection: 'auto', // global text direction
66 | extensions,
67 | });
68 |
69 | return (
70 |
73 |
76 |
77 | );
78 | };
79 | ```
80 |
81 | ## Props
82 |
83 | ```ts
84 | /**
85 | * Interface for RichTextEditor component props
86 | */
87 | export interface IProviderRichTextProps {
88 | editor: Editor | null
89 | dark: boolean
90 | }
91 | ```
92 |
93 | ## Full Source Code Demo
94 |
95 | [Full Source Code Demo](https://github.com/hunghg255/reactjs-tiptap-editor-demo)
96 |
--------------------------------------------------------------------------------
/docs/extensions/Table/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | description: Table
3 |
4 | next:
5 | text: TaskList
6 | link: /extensions/TaskList/index.md
7 | ---
8 |
9 | # Table
10 |
11 | The Table extension allows you to add tables to your editor.
12 |
13 | - Based on TipTap's table extension. [@tiptap/extension-table](https://tiptap.dev/docs/editor/extensions/nodes/table)
14 |
15 | ## Usage
16 |
17 |
18 | ```tsx
19 | import { RichTextProvider } from 'reactjs-tiptap-editor'
20 |
21 | // Base Kit
22 | import { Document } from '@tiptap/extension-document'
23 | import { Text } from '@tiptap/extension-text'
24 | import { Paragraph } from '@tiptap/extension-paragraph'
25 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
26 | import { HardBreak } from '@tiptap/extension-hard-break'
27 | import { TextStyle } from '@tiptap/extension-text-style';
28 | import { ListItem } from '@tiptap/extension-list';
29 |
30 | // Extension
31 | import { Table, RichTextTable } from 'reactjs-tiptap-editor/table'; // [!code ++]
32 | // ... other extensions
33 |
34 |
35 | // Import CSS
36 | import 'reactjs-tiptap-editor/style.css';
37 |
38 | const extensions = [
39 | // Base Extensions
40 | Document,
41 | Text,
42 | Dropcursor,
43 | Gapcursor,
44 | HardBreak,
45 | Paragraph,
46 | TrailingNode,
47 | ListItem,
48 | TextStyle,
49 | Placeholder.configure({
50 | placeholder: 'Press \'/\' for commands',
51 | })
52 |
53 | ...
54 | // Import Extensions Here
55 | Table// [!code ++]
56 | ];
57 |
58 | const RichTextToolbar = () => {
59 | return (
60 |
61 | {/* [!code ++] */}
62 |
63 | )
64 | }
65 |
66 | const App = () => {
67 | const editor = useEditor({
68 | textDirection: 'auto', // global text direction
69 | extensions,
70 | });
71 |
72 | return (
73 |
76 |
77 |
78 |
81 |
82 | );
83 | };
84 | ```
85 |
86 |
87 |
--------------------------------------------------------------------------------