Skip to content

Commit

Permalink
feat: support custom toolbars
Browse files Browse the repository at this point in the history
  • Loading branch information
condorheroblog committed Jan 8, 2025
1 parent ea806f6 commit 7ba0fed
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 45 deletions.
4 changes: 4 additions & 0 deletions docs/.vitepress/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export function getLocaleConfig(lang: string) {
text: t('Getting Started'),
link: `${urlPrefix}/guide/getting-started`,
},
{
text: t('Toolbar'),
link: `${urlPrefix}/guide/toolbar`,
},
{
text: t('Bubble Menu'),
link: `${urlPrefix}/guide/bubble-menu`,
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
description: How to install reactjs-tiptap-editor

next:
text: Bubble Menu
link: /guide/bubble-menu.md
text: Toolbar
link: /guide/toolbar.md
---

# Installation
Expand Down
55 changes: 55 additions & 0 deletions docs/guide/toolbar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
description: Toolbar

next:
text: Bubble Menu
link: /guide/bubble-menu.md
---

# Toolbar

The toolbar of the rich text editor.

## Usage

Hide a certain toolbar:

```js
Bold.configure({
toolbar: false,
})
```

## Customizing the Toolbar

```jsx
<RichTextEditor
toolbar={{
render: (props, dom, domContent, containerDom) => {
return containerDom(domContent)
}
}}
/>
```

## ToolbarProps

```ts
export interface ToolbarItemProps {
button: {
component: React.ComponentType<any>
componentProps: Record<string, any>
}
divider: boolean
spacer: boolean
type: string
name: string
}
export interface ToolbarRenderProps {
editor: Editor
disabled: boolean
}
export interface ToolbarProps {
render?: (props: ToolbarRenderProps, dom: ToolbarItemProps[], domContent: React.ReactNode, containerDom: (innerContent: React.ReactNode) => React.ReactNode) => React.ReactNode
}
```
6 changes: 4 additions & 2 deletions src/components/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { UseEditorOptions } from '@tiptap/react'
import { EditorContent, useEditor } from '@tiptap/react'
import { differenceBy, throttle } from 'lodash-es'

import type { BubbleMenuProps } from '@/types'
import type { BubbleMenuProps, ToolbarProps } from '@/types'
import { BubbleMenu, Toolbar, TooltipProvider } from '@/components'
import { EDITOR_UPDATE_WATCH_THROTTLE_WAIT_TIME } from '@/constants'
import { RESET_CSS } from '@/constants/resetCSS'
Expand Down Expand Up @@ -59,6 +59,8 @@ export interface RichTextEditorProps {
onChangeContent?: (val: any) => void
/** Bubble menu props */
bubbleMenu?: BubbleMenuProps
/** Toolbar props */
toolbar?: ToolbarProps

/** Use editor options */
useEditorOptions?: UseEditorOptions
Expand Down Expand Up @@ -168,7 +170,7 @@ function RichTextEditor(props: RichTextEditorProps, ref: React.ForwardedRef<{ ed
<div className="richtext-rounded-[0.5rem] richtext-bg-background richtext-shadow richtext-overflow-hidden richtext-outline richtext-outline-1">

<div className="richtext-flex richtext-flex-col richtext-w-full richtext-max-h-full">
{!props?.hideToolbar && <Toolbar editor={editor} disabled={!!props?.disabled} />}
{!props?.hideToolbar && <Toolbar editor={editor} disabled={!!props?.disabled} toolbar={props.toolbar} />}

<EditorContent className={`richtext-relative ${props?.contentClass || ''}`} editor={editor} />

Expand Down
97 changes: 56 additions & 41 deletions src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
/* eslint-disable react/no-duplicate-key */
import React, { useMemo } from 'react'
import type { Editor } from '@tiptap/core'
import type { ToolbarItemProps, ToolbarProps } from '@/types'

import { Separator } from '@/components'
import { useLocale } from '@/locales'
import { isFunction } from '@/utils/utils'

export interface ToolbarProps {
export interface ToolbarComponentProps {
editor: Editor
disabled?: boolean
toolbar?: ToolbarProps
}

interface ToolbarItemProps {
button: {
component: React.ComponentType<any>
componentProps: Record<string, any>
}
divider: boolean
spacer: boolean
}

function Toolbar({ editor, disabled }: ToolbarProps) {
function Toolbar({ editor, disabled, toolbar }: ToolbarComponentProps) {
const { t, lang } = useLocale()

const items = useMemo(() => {
Expand All @@ -34,7 +26,12 @@ function Toolbar({ editor, disabled }: ToolbarProps) {
let menus: ToolbarItemProps[] = []

for (const extension of sortExtensions) {
const { button, divider = false, spacer = false, toolbar = true } = extension.options as any
const {
button,
divider = false,
spacer = false,
toolbar = true,
} = extension.options
if (!button || !isFunction(button) || !toolbar) {
continue
}
Expand All @@ -50,44 +47,62 @@ function Toolbar({ editor, disabled }: ToolbarProps) {
button: k,
divider: i === _button.length - 1 ? divider : false,
spacer: i === 0 ? spacer : false,
type: extension.type,
name: extension.name,
}))
menus = [...menus, ...menu]
continue
}

menus.push({ button: _button, divider, spacer })
menus.push({
button: _button,
divider,
spacer,
type: extension.type,
name: extension.name,
})
}
return menus
}, [editor, t, lang])

return (
<div
className="richtext-px-1 richtext-py-2 !richtext-border-b"
style={{
pointerEvents: disabled ? 'none' : 'auto',
opacity: disabled ? 0.5 : 1,
}}
>
<div className="richtext-relative richtext-flex richtext-flex-wrap richtext-h-auto richtext-gap-y-1 richtext-gap-x-1">
{items.map((item: ToolbarItemProps, key) => {
const ButtonComponent = item.button.component

return (
<div className="richtext-flex richtext-items-center" key={`toolbar-item-${key}`}>
{item?.spacer && <Separator orientation="vertical" className="!richtext-h-[16px] !richtext-mx-[10px]" />}

<ButtonComponent
{...item.button.componentProps}
disabled={disabled || item?.button?.componentProps?.disabled}
/>

{item?.divider && <Separator orientation="vertical" className="!richtext-h-auto !richtext-mx-2" />}
</div>
)
})}
const containerDom = (innerContent: React.ReactNode) => {
return (
<div
className="richtext-px-1 richtext-py-2 !richtext-border-b"
style={{
pointerEvents: disabled ? 'none' : 'auto',
opacity: disabled ? 0.5 : 1,
}}
>
<div className="richtext-relative richtext-flex richtext-flex-wrap richtext-h-auto richtext-gap-y-1 richtext-gap-x-1">
{innerContent}
</div>
</div>
</div>
)
)
}

const domContent = items.map((item: ToolbarItemProps, key) => {
const ButtonComponent = item.button.component

return (
<div className="richtext-flex richtext-items-center" key={`toolbar-item-${key}`}>
{item?.spacer && <Separator orientation="vertical" className="!richtext-h-[16px] !richtext-mx-[10px]" />}

<ButtonComponent
{...item.button.componentProps}
disabled={disabled || item?.button?.componentProps?.disabled}
/>

{item?.divider && <Separator orientation="vertical" className="!richtext-h-auto !richtext-mx-2" />}
</div>
)
})

if (toolbar && toolbar?.render) {
return toolbar.render({ editor, disabled: disabled || false }, items, domContent, containerDom)
}

return containerDom(domContent)
}

export { Toolbar }
22 changes: 22 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,28 @@ export interface BubbleMenuProps {
render?: (props: BubbleMenuRenderProps, dom: React.ReactNode) => React.ReactNode
}

/**
* Represents the ToolbarItemProps.
*/
export interface ToolbarItemProps {
button: {
component: React.ComponentType<any>
componentProps: Record<string, any>
}
divider: boolean
spacer: boolean
type: string
name: string
}

export interface ToolbarRenderProps {
editor: Editor
disabled: boolean
}
export interface ToolbarProps {
render?: (props: ToolbarRenderProps, dom: ToolbarItemProps[], domContent: React.ReactNode, containerDom: (innerContent: React.ReactNode) => React.ReactNode) => React.ReactNode
}

export interface NameValueOption<T = string> {
name: string
value: T
Expand Down

0 comments on commit 7ba0fed

Please sign in to comment.