29 |
setOpen(x => !x)}
33 | >
34 |
35 |
36 | Add an image
37 |
38 |
39 |
40 |
47 | {state => (
48 |
56 |
61 |
62 | )}
63 |
64 |
65 | {open && (
66 |
setOpen(false)}
69 | />
70 | )}
71 |
72 | )
73 | }
74 |
75 | export default ImageSelector
76 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/image/components/ImageUploader.tsx:
--------------------------------------------------------------------------------
1 | import { type ChangeEvent, type FormEvent, useCallback, useContext, useRef, useState } from 'react'
2 |
3 | import { ColorsContext } from '../../..'
4 |
5 | import type { ImageUploaderProps, Mode } from '../types'
6 |
7 | function ImageUploader({ maxFileSize, onSubmitFile, onSubmitUrl }: ImageUploaderProps) {
8 | const { primaryColor, primaryColorDark, primaryColorTransparent } = useContext(ColorsContext)
9 |
10 | const fileInputRef = useRef
(null)
11 | const [mode, setMode] = useState('upload')
12 | const [url, setUrl] = useState('')
13 |
14 | const handleUploadClick = useCallback(() => {
15 | fileInputRef.current?.click()
16 | }, [])
17 |
18 | const handleUploadChange = useCallback((event: ChangeEvent) => {
19 | const file = event.target.files?.[0]
20 |
21 | if (!file) return
22 |
23 | onSubmitFile(file)
24 | }, [onSubmitFile])
25 |
26 | const handleUrlSubmit = useCallback((event: FormEvent) => {
27 | event.preventDefault()
28 |
29 | if (!url) return
30 |
31 | onSubmitUrl(url)
32 | }, [url, onSubmitUrl])
33 |
34 | const renderTabItem = useCallback((label: string, itemMode: Mode) => (
35 | setMode(itemMode)}
38 | >
39 |
40 | {label}
41 |
42 | {mode === itemMode && (
43 |
44 | )}
45 |
46 | ), [mode])
47 |
48 | const renderUpload = useCallback(() => (
49 | <>
50 |
57 |
64 | {!!maxFileSize && (
65 |
66 | The maximum size per file is
67 | {' '}
68 | {maxFileSize}
69 | .
70 |
71 | )}
72 | >
73 | ), [maxFileSize, handleUploadClick, handleUploadChange])
74 |
75 | const renderUrl = useCallback(() => (
76 |
101 | ), [primaryColor, primaryColorTransparent, primaryColorDark, url, handleUrlSubmit])
102 |
103 | return (
104 |
105 |
106 | {renderTabItem('Upload', 'upload')}
107 | {renderTabItem('Embed link', 'url')}
108 |
109 |
110 | {mode === 'upload' && renderUpload()}
111 | {mode === 'url' && renderUrl()}
112 |
113 |
114 | )
115 | }
116 |
117 | export default ImageUploader
118 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/image/components/ResizableImage.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | import type { ResizableImageProps } from '../types'
4 |
5 | function ResizableImage({ src, width, ratio, progress, setWidth, setRatio }: ResizableImageProps) {
6 | const [image, setImage] = useState(null)
7 | console.log(false && setWidth)
8 |
9 | useEffect(() => {
10 | if (!src) return
11 |
12 | const img = new Image()
13 |
14 | img.src = src
15 |
16 | img.addEventListener('load', () => {
17 | setImage(img)
18 | setRatio(img.width / img.height)
19 | })
20 | }, [src, setRatio])
21 |
22 | return (
23 |
24 |
28 |
29 | {!!image && (
30 |

35 | )}
36 |
44 |
45 | {progress < 1 && (
46 |
47 |
48 |
52 |
53 | {Math.round(progress * 100)}
54 | %
55 |
56 |
57 |
58 | )}
59 |
60 | )
61 | }
62 |
63 | export default ResizableImage
64 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/image/index.ts:
--------------------------------------------------------------------------------
1 | import imagePlugin from './plugin'
2 |
3 | export default imagePlugin
4 |
5 | export type { ReactBlockTextImagePluginSubmition, ReactBlockTextImagePluginSubmitter } from './types'
6 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/image/plugin.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactBlockTextPlugins } from '../../types'
2 |
3 | import type { PluginOptions } from './types'
4 | import BlockContent from './components/BlockContent'
5 | import Icon from './components/Icon'
6 |
7 | function imagePlugin(options: PluginOptions): ReactBlockTextPlugins {
8 | if (typeof options.onSubmitFile !== 'function') throw new Error('Image plugin: you must provide a "onSubmitFile" function to handle file uploads.')
9 | if (typeof options.onSubmitUrl !== 'function') throw new Error('Image plugin: you must provide a "onSubmitUrl" function to handle url uploads.')
10 | if (typeof options.getUrl !== 'function') throw new Error('Image plugin: you must provide a "getUrl" function to handle image downloads.')
11 |
12 | return [
13 | ({ onChange }) => ({
14 | type: 'image',
15 | blockCategory: 'media',
16 | title: 'Image',
17 | label: 'Upload or embed with a link.',
18 | shortcuts: 'img,photo,picture',
19 | icon: ,
20 | isConvertibleToText: false,
21 | paddingTop: 5,
22 | paddingBottom: 5,
23 | BlockContent: props => (
24 |
32 | ),
33 | }),
34 | ]
35 | }
36 |
37 | export default imagePlugin
38 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/image/types.ts:
--------------------------------------------------------------------------------
1 | import type { Dispatch, SetStateAction } from 'react'
2 |
3 | import type { BlockContentProps as ReactBlockTextBlockContentProps, ReactBlockTextOnChange } from '../../types'
4 |
5 | export type Metadata = {
6 | imageKey: string
7 | width: number // between 0 and 1
8 | ratio: number // width / height
9 | }
10 |
11 | export type ReactBlockTextImagePluginSubmition = {
12 | progress: number // Between 0 and 1
13 | imageKey?: string // The reference to the image once it's uploaded
14 | isError?: boolean
15 | }
16 |
17 | export type ReactBlockTextImagePluginSubmitter = () => ReactBlockTextImagePluginSubmition
18 |
19 | export type PluginOptions = {
20 | maxFileSize?: string
21 | onSubmitFile: (file: File) => Promise
22 | onSubmitUrl: (url: string) => Promise
23 | getUrl: (imageKey: string) => Promise
24 | }
25 |
26 | export type BlockContentProps = ReactBlockTextBlockContentProps & {
27 | maxFileSize?: string
28 | onItemChange: ReactBlockTextOnChange
29 | onSubmitFile: (file: File) => Promise
30 | onSubmitUrl: (url: string) => Promise
31 | getUrl: (imageKey: string) => Promise
32 | }
33 |
34 | export type ImageSelectorProps = {
35 | maxFileSize?: string
36 | onSubmitFile: (file: File) => void
37 | onSubmitUrl: (url: string) => void
38 | }
39 |
40 | export type Mode = 'upload' | 'url'
41 |
42 | export type ImageUploaderProps = {
43 | maxFileSize?: string
44 | onSubmitFile: (file: File) => void
45 | onSubmitUrl: (url: string) => void
46 | }
47 |
48 | export type LoadingImageProps = {
49 | file?: File | null
50 | url?: string
51 | }
52 |
53 | export type ResizableImageProps = {
54 | src?: string
55 | width: number
56 | ratio: number
57 | progress: number
58 | setWidth: Dispatch>
59 | setRatio: Dispatch>
60 | }
61 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/list/components/BlockContent.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { toRoman } from 'roman-numerals'
3 | import { toAbc } from 'abc-list'
4 |
5 | import type { BlockContentProps } from '../../../types'
6 |
7 | const depthToBullet = ['•', '◦', '▪']
8 |
9 | function BlockContentList(props: BlockContentProps) {
10 | const { item, onBlockSelection, onRectSelectionMouseDown, BlockContentText } = props
11 |
12 | const { index, depth } = useMemo(() => {
13 | try {
14 | const { index, depth } = JSON.parse(item.metadata)
15 |
16 | return { index, depth }
17 | }
18 | catch {
19 | return { index: 0, depth: 0 }
20 | }
21 | }, [item.metadata])
22 |
23 | const label = useMemo(() => {
24 | const cycledDepth = depth % 3
25 |
26 | if (cycledDepth === 0) return `${index + 1}`
27 | if (cycledDepth === 1) return toAbc(index)
28 |
29 | return toRoman(index + 1).toLowerCase()
30 | }, [index, depth])
31 |
32 | return (
33 |
34 |
39 | {item.metadata.length ? (
40 |
47 | ) : (
48 |
52 | )}
53 |
54 |
59 |
60 |
64 |
65 |
66 | )
67 | }
68 |
69 | export default BlockContentList
70 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/list/components/BulletedListIcon.tsx:
--------------------------------------------------------------------------------
1 | function BulletedListIcon() {
2 | return (
3 |
13 | )
14 | }
15 |
16 | export default BulletedListIcon
17 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/list/components/NumberedListIcon.tsx:
--------------------------------------------------------------------------------
1 | function NumberedListIcon() {
2 | return (
3 |
4 |
5 | 1
6 |
7 | .
8 |
9 |
10 |
15 |
16 | )
17 | }
18 |
19 | export default NumberedListIcon
20 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/list/index.ts:
--------------------------------------------------------------------------------
1 | import listPlugin from './plugin'
2 |
3 | export default listPlugin
4 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/list/plugin.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactBlockTextPlugins } from '../../types'
2 |
3 | import type { PluginOptions } from './types'
4 |
5 | import applyMetadatas from './utils/applyMetadata'
6 |
7 | import BlockContent from './components/BlockContent'
8 | import BulletedListIcon from './components/BulletedListIcon'
9 | import NumberedListIcon from './components/NumberedListIcon'
10 |
11 | const TYPES = ['bulleted-list', 'numbered-list']
12 | const TITLES = ['Bulleted list', 'Numbered list']
13 | const LABELS = ['Create a simple bulleted list.', 'Create a list with numbering.']
14 | const ICONS = [, ]
15 |
16 | function listPlugin(options?: PluginOptions): ReactBlockTextPlugins {
17 | const bulleted = options?.bulleted ?? true
18 | const numbered = options?.numbered ?? true
19 |
20 | const plugins: ReactBlockTextPlugins = []
21 |
22 | if (bulleted) {
23 | plugins.push(() => ({
24 | type: TYPES[0],
25 | blockCategory: 'basic',
26 | title: TITLES[0],
27 | label: LABELS[0],
28 | icon: ICONS[0],
29 | isConvertibleToText: true,
30 | isNewItemOfSameType: true,
31 | shortcuts: 'task',
32 | maxIndent: 5,
33 | applyMetadatas,
34 | BlockContent,
35 | }))
36 | }
37 |
38 | if (numbered) {
39 | plugins.push(() => ({
40 | type: TYPES[1],
41 | blockCategory: 'basic',
42 | title: TITLES[1],
43 | label: LABELS[1],
44 | icon: ICONS[1],
45 | isConvertibleToText: true,
46 | isNewItemOfSameType: true,
47 | shortcuts: 'task',
48 | maxIndent: 5,
49 | applyMetadatas,
50 | BlockContent,
51 | }))
52 | }
53 |
54 | return plugins
55 | }
56 |
57 | export default listPlugin
58 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/list/types.ts:
--------------------------------------------------------------------------------
1 | export type PluginOptions = {
2 | bulleted?: boolean
3 | numbered?: boolean
4 | }
5 |
6 | export type NumberedListMetada = {
7 | index: number
8 | depth: number
9 | }
10 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/list/utils/applyMetadata.ts:
--------------------------------------------------------------------------------
1 | import type { ReactBlockTextDataItem } from '../../../types'
2 |
3 | function applyMetadatas(_index: number, value: ReactBlockTextDataItem[]) {
4 | const nextValue = [...value]
5 |
6 | for (let i = 0; i < nextValue.length; i++) {
7 | const { type } = nextValue[i]
8 |
9 | if (type === 'numbered-list') nextValue[i] = applyNumberedListMetadata(nextValue, i)
10 | if (type === 'bulleted-list') nextValue[i] = applyBulletedListMetadata(nextValue, i)
11 | }
12 |
13 | return nextValue
14 | }
15 |
16 | function applyNumberedListMetadata(value: ReactBlockTextDataItem[], index: number) {
17 | const item = value[index]
18 | const previousListItem = findPreviousListItem(value, index)
19 |
20 | if (previousListItem) {
21 | const { index: previousIndex, depth } = JSON.parse(previousListItem.metadata)
22 | const isIndented = previousListItem.indent < item.indent
23 |
24 | return {
25 | ...item,
26 | metadata: JSON.stringify({
27 | index: isIndented ? 0 : previousIndex + 1,
28 | depth: isIndented ? depth + 1 : depth,
29 | }),
30 | }
31 | }
32 |
33 | const previousItem = value[index - 1]
34 |
35 | if (previousItem.type === 'numbered-list') {
36 | return {
37 | ...item,
38 | indent: previousItem.indent + 1,
39 | metadata: JSON.stringify({
40 | index: 0,
41 | depth: previousItem.indent + 1,
42 | }),
43 | }
44 | }
45 |
46 | return {
47 | ...item,
48 | indent: 0,
49 | metadata: JSON.stringify({
50 | index: 0,
51 | depth: 0,
52 | }),
53 | }
54 |
55 | }
56 |
57 | function applyBulletedListMetadata(value: ReactBlockTextDataItem[], index: number) {
58 | const item = value[index]
59 | const previousListItem = findPreviousListItem(value, index)
60 |
61 | if (previousListItem) return item
62 |
63 | const previousItem = value[index - 1]
64 |
65 | if (previousItem && previousItem.type === 'bulleted-list') {
66 | return {
67 | ...item,
68 | indent: Math.min(previousItem.indent + 1, item.indent),
69 | }
70 | }
71 |
72 | return {
73 | ...item,
74 | indent: 0,
75 | }
76 | }
77 |
78 | function findPreviousListItem(value: ReactBlockTextDataItem[], index: number) {
79 | const item = value[index]
80 | let lastIndent = item.indent
81 |
82 | for (let i = index - 1; i >= 0; i--) {
83 | if (value[i].indent > lastIndent) continue
84 | if (value[i].type !== item.type) return null
85 | if (value[i].indent === item.indent) return value[i]
86 |
87 | lastIndent = value[i].indent
88 | }
89 |
90 | return null
91 | }
92 |
93 | export default applyMetadatas
94 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/quote/components/BlockContent.tsx:
--------------------------------------------------------------------------------
1 | import type { BlockContentProps } from '../../../types'
2 |
3 | function BlockContent(props: BlockContentProps) {
4 | const { onBlockSelection, onRectSelectionMouseDown, BlockContentText } = props
5 |
6 | return (
7 |
8 |
13 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default BlockContent
26 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/quote/components/Icon.tsx:
--------------------------------------------------------------------------------
1 | function Icon() {
2 | return (
3 |
4 |
5 |
6 | To be
7 |
8 | or not
9 |
10 | to be
11 |
12 |
13 | )
14 | }
15 |
16 | export default Icon
17 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/quote/index.ts:
--------------------------------------------------------------------------------
1 | import quotePlugin from './plugin'
2 |
3 | export default quotePlugin
4 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/quote/plugin.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactBlockTextPlugins } from '../../types'
2 |
3 | import BlockContent from './components/BlockContent'
4 | import Icon from './components/Icon'
5 |
6 | function quotePlugin(): ReactBlockTextPlugins {
7 | return [
8 | () => ({
9 | type: 'quote',
10 | blockCategory: 'basic',
11 | title: 'Quote',
12 | label: 'Capture a quote.',
13 | shortcuts: 'citation',
14 | icon: ,
15 | isConvertibleToText: true,
16 | paddingTop: 5,
17 | paddingBottom: 5,
18 | BlockContent,
19 | }),
20 | ]
21 | }
22 |
23 | export default quotePlugin
24 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/text/components/BlockContent.tsx:
--------------------------------------------------------------------------------
1 | import type { BlockContentProps } from '../../../types'
2 |
3 | function BlockContent(props: BlockContentProps) {
4 | const { BlockContentText } = props
5 |
6 | return (
7 |
8 | )
9 |
10 | }
11 |
12 | export default BlockContent
13 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/text/components/Icon.tsx:
--------------------------------------------------------------------------------
1 | function Icon() {
2 | return (
3 |
4 | Aa
5 |
6 | )
7 | }
8 |
9 | export default Icon
10 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/text/index.ts:
--------------------------------------------------------------------------------
1 | import textPlugin from './plugin'
2 |
3 | export default textPlugin
4 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/text/plugin.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactBlockTextPlugins } from '../../types'
2 |
3 | import BlockContent from './components/BlockContent'
4 | import Icon from './components/Icon'
5 |
6 | function textPlugin(): ReactBlockTextPlugins {
7 | return [
8 | () => ({
9 | type: 'text',
10 | blockCategory: 'basic',
11 | title: 'Text',
12 | label: 'Just start writing with plain text.',
13 | shortcuts: 'txt',
14 | icon: ,
15 | BlockContent,
16 | }),
17 | ]
18 | }
19 |
20 | export default textPlugin
21 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/components/BlockContent.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 |
3 | import type { BlockContentProps } from '../types'
4 |
5 | import applyTodoStyle from '../utils/applyTodoStyle'
6 |
7 | import Checkbox from './Checkbox'
8 |
9 | function BlockContent(props: BlockContentProps) {
10 | const { item, editorState, readOnly, BlockContentText, onItemChange, forceBlurContent } = props
11 |
12 | const handleCheck = useCallback((checked: boolean) => {
13 | if (readOnly) return
14 |
15 | const nextItem = { ...item, metadata: checked ? 'true' : 'false' }
16 | const nextEditorState = applyTodoStyle(nextItem, editorState, false)
17 |
18 | onItemChange(nextItem, nextEditorState)
19 |
20 | // Blur the to-do on next render
21 | forceBlurContent()
22 | }, [readOnly, item, editorState, onItemChange, forceBlurContent])
23 |
24 | return (
25 |
26 |
27 |
33 |
34 |
39 |
40 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default BlockContent
50 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/components/CheckIcon.tsx:
--------------------------------------------------------------------------------
1 | import { SVGAttributes } from 'react'
2 |
3 | function CheckIcon(props: SVGAttributes) {
4 | return (
5 |
13 | )
14 | }
15 |
16 | export default CheckIcon
17 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/components/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useContext } from 'react'
2 | import _ from 'clsx'
3 |
4 | import type { CheckboxProps } from '../types'
5 |
6 | import { ColorsContext } from '../../..'
7 |
8 | import CheckIcon from './CheckIcon'
9 |
10 | function Checkbox({ checked, onCheck, ...props }: CheckboxProps) {
11 | const { primaryColor } = useContext(ColorsContext)
12 |
13 | const handleClick = useCallback(() => {
14 | onCheck(!checked)
15 | }, [checked, onCheck])
16 |
17 | return (
18 |
39 | )
40 | }
41 |
42 | export default Checkbox
43 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/components/Icon.tsx:
--------------------------------------------------------------------------------
1 | import Checkbox from './Checkbox'
2 |
3 | function Icon() {
4 | return (
5 |
6 |
7 | {}}
10 | />
11 |
12 |
17 |
18 | )
19 | }
20 |
21 | export default Icon
22 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/constants.ts:
--------------------------------------------------------------------------------
1 | export const INLINE_STYLES = {
2 | TODO_CHECKED: 'TODO_CHECKED',
3 | }
4 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/index.ts:
--------------------------------------------------------------------------------
1 | import todoPlugin from './plugin'
2 |
3 | export default todoPlugin
4 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/plugin.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactBlockTextPlugins } from '../../types'
2 |
3 | import { INLINE_STYLES } from './constants'
4 |
5 | import applyTodoStyle from './utils/applyTodoStyle'
6 |
7 | import BlockContent from './components/BlockContent'
8 | import Icon from './components/Icon'
9 |
10 | function todoPlugin(): ReactBlockTextPlugins {
11 | return [
12 | ({ onChange }) => ({
13 | type: 'todo',
14 | blockCategory: 'basic',
15 | title: 'To-do list',
16 | label: 'Track tasks with a to-do list.',
17 | shortcuts: 'todo',
18 | icon: ,
19 | isConvertibleToText: true,
20 | isNewItemOfSameType: true,
21 | paddingTop: 5,
22 | paddingBottom: 5,
23 | styleMap: {
24 | [INLINE_STYLES.TODO_CHECKED]: {
25 | color: '#9ca3af',
26 | textDecoration: 'line-through',
27 | textDecorationThickness: 'from-font',
28 | },
29 | },
30 | applyStyles: applyTodoStyle,
31 | BlockContent: props => (
32 |
36 | ),
37 | }),
38 | ]
39 | }
40 |
41 | export default todoPlugin
42 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/types.ts:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from 'react'
2 |
3 | import type { BlockContentProps as ReactBlockTextBlockContentProps, ReactBlockTextOnChange } from '../../types'
4 |
5 | export type CheckboxProps = HTMLAttributes & {
6 | checked: boolean
7 | onCheck: (checked: boolean) => void
8 | }
9 |
10 | export type BlockContentProps = ReactBlockTextBlockContentProps & {
11 | onItemChange: ReactBlockTextOnChange
12 | }
13 |
--------------------------------------------------------------------------------
/react-block-text/src/plugins/todo/utils/applyTodoStyle.ts:
--------------------------------------------------------------------------------
1 | import { EditorState, Modifier, SelectionState } from 'draft-js'
2 |
3 | import { ReactBlockTextDataItem } from '../../../types'
4 |
5 | import { INLINE_STYLES } from '../constants'
6 |
7 | function applyTodoStyle(item: ReactBlockTextDataItem, editorState: EditorState, skipSelection = true) {
8 | let currentSelection = editorState.getSelection()
9 | const contentState = editorState.getCurrentContent()
10 | const firstBlock = contentState.getFirstBlock()
11 | const lastBlock = contentState.getLastBlock()
12 | const selection = SelectionState.createEmpty(firstBlock.getKey()).merge({
13 | anchorKey: firstBlock.getKey(),
14 | anchorOffset: 0,
15 | focusKey: lastBlock.getKey(),
16 | focusOffset: lastBlock.getText().length,
17 | })
18 | const modify = item.metadata === 'true' ? Modifier.applyInlineStyle : Modifier.removeInlineStyle
19 | const nextContentState = modify(contentState, selection, INLINE_STYLES.TODO_CHECKED)
20 | const nextEditorState = EditorState.push(editorState, nextContentState, 'change-inline-style')
21 |
22 | if (skipSelection) return EditorState.forceSelection(nextEditorState, currentSelection)
23 |
24 | if (currentSelection.getAnchorOffset() !== currentSelection.getFocusOffset()) {
25 | currentSelection = currentSelection.merge({
26 | anchorOffset: currentSelection.getFocusOffset(),
27 | })
28 | }
29 |
30 | return EditorState.forceSelection(nextEditorState, currentSelection)
31 | }
32 |
33 | export default applyTodoStyle
34 |
--------------------------------------------------------------------------------
/react-block-text/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { CSSProperties, ComponentType, MouseEvent as ReactMouseEvent, ReactNode } from 'react'
2 | import type { DraftHandleValue, Editor, EditorState } from 'draft-js'
3 |
4 | export type ReactBlockTextDataItemType = 'text' | string
5 |
6 | export type ReactBlockTextDataItem = {
7 | reactBlockTextVersion: string
8 | id: string
9 | type: ReactBlockTextDataItemType
10 | data: string
11 | metadata: string
12 | indent: number
13 | }
14 |
15 | export type ReactBlockTextData = ReactBlockTextDataItem[]
16 |
17 | export type ReactBlockTextOnChange = (item: ReactBlockTextDataItem, editorState: EditorState) => void
18 |
19 | export type ReactBlockTextPluginOptions = {
20 | onChange: ReactBlockTextOnChange
21 | }
22 |
23 | export type BlockCategory = 'basic' | 'media' | 'database' | 'advanced' | 'inline' | 'embed'
24 |
25 | export type ReactBlockTextPluginData = {
26 | type: string
27 | blockCategory: BlockCategory
28 | title: string
29 | label: string
30 | shortcuts: string
31 | icon: ReactNode
32 | isConvertibleToText?: boolean
33 | isNewItemOfSameType?: boolean
34 | maxIndent?: number // default: 0
35 | paddingTop?: number // default: 3
36 | paddingBottom?: number // default: 3
37 | iconsPaddingTop?: number // default: 0
38 | styleMap?: Record
39 | applyStyles?: (item: ReactBlockTextDataItem, editorState: EditorState) => EditorState
40 | applyMetadatas?: (index: number, value: ReactBlockTextDataItem[], editorStates: ReactBlockTextEditorStates) => ReactBlockTextDataItem[]
41 | BlockContent: ComponentType
42 | }
43 |
44 | export type ReactBlockTextPlugin = (options: ReactBlockTextPluginOptions) => ReactBlockTextPluginData
45 |
46 | export type ReactBlockTextPlugins = ReactBlockTextPlugin[]
47 |
48 | export type ReactBlockTextEditorStates = Record
49 |
50 | export type ReactBlockTextProps = {
51 | value?: string
52 | plugins?: ReactBlockTextPlugins
53 | readOnly?: boolean
54 | paddingTop?: number
55 | paddingBottom?: number
56 | paddingLeft?: number
57 | paddingRight?: number
58 | primaryColor?: string | null | undefined
59 | textColor?: string | null | undefined
60 | className?: string
61 | style?: CSSProperties
62 | onChange?: (value: string) => void
63 | onSave?: () => void
64 | }
65 |
66 | export type BlockProps = {
67 | children: ReactNode
68 | pluginsData: ReactBlockTextPluginData[]
69 | item: ReactBlockTextDataItem
70 | index: number
71 | readOnly: boolean
72 | selected: boolean
73 | hovered: boolean
74 | isDraggingTop: boolean | null
75 | paddingLeft?: number
76 | paddingRight?: number
77 | noPadding?: boolean
78 | registerSelectionRef: (ref: any) => void
79 | onAddItem: () => void
80 | onDeleteItem: () => void
81 | onDuplicateItem: () => void
82 | onMouseDown: () => void
83 | onMouseMove: () => void
84 | onMouseLeave: () => void
85 | onRectSelectionMouseDown: (event: ReactMouseEvent) => void
86 | onDragStart: () => void
87 | onDrag: (index: number, isTop: boolean | null) => void
88 | onDragEnd: () => void
89 | onBlockMenuOpen: () => void
90 | onBlockMenuClose: () => void
91 | focusContent: () => void
92 | focusContentAtStart: () => void
93 | focusContentAtEnd: () => void
94 | focusNextContent: () => void
95 | blurContent: () => void
96 | blockContentProps: BlockContentProps
97 | }
98 |
99 | export type BlockContentProps = {
100 | BlockContentText: ComponentType
101 | pluginsData: ReactBlockTextPluginData[]
102 | item: ReactBlockTextDataItem
103 | index: number
104 | editorState: EditorState
105 | readOnly: boolean
106 | focused: boolean
107 | isSelecting: boolean
108 | placeholder: string
109 | fallbackPlaceholder: string
110 | registerRef: (ref: any) => void
111 | onChange: (editorState: EditorState) => void
112 | onKeyCommand: (command: string) => DraftHandleValue
113 | onReturn: (event: any) => DraftHandleValue
114 | onUpArrow: (event: any) => void
115 | onDownArrow: (event: any) => void
116 | onFocus: () => void
117 | onBlur: () => void
118 | onPaste: () => DraftHandleValue
119 | onBlockSelection: () => void
120 | onRectSelectionMouseDown: (event: ReactMouseEvent) => void
121 | focusContent: () => void
122 | focusContentAtStart: () => void
123 | focusContentAtEnd: () => void
124 | focusNextContent: () => void
125 | blurContent: () => void
126 | forceBlurContent: () => void
127 | }
128 |
129 | export type BlockCommonProps = {
130 | [K in keyof BlockProps & keyof BlockContentProps]: BlockProps[K] | BlockContentProps[K]
131 | }
132 |
133 | export type QueryMenuProps = {
134 | pluginsData: ReactBlockTextPluginData[]
135 | query: string
136 | top?: number
137 | bottom?: number
138 | left: number
139 | onSelect: (command: ReactBlockTextDataItemType) => void
140 | onClose: () => void
141 | }
142 |
143 | export type QueryMenuItemProps = {
144 | title: string
145 | label: string
146 | icon: ReactNode
147 | active: boolean
148 | shouldScrollIntoView: boolean
149 | resetShouldScrollIntoView: () => void
150 | onClick: () => void
151 | onMouseEnter: () => void
152 | onMouseLeave: () => void
153 | }
154 |
155 | export type QueryMenuIconProps = {
156 | children: ReactNode
157 | }
158 |
159 | export type BlockMenuProps = {
160 | top: number
161 | left: number
162 | onDeleteItem: () => void
163 | onDuplicateItem: () => void
164 | onClose: () => void
165 | }
166 |
167 | export type BlockMenuItemProps = {
168 | icon: ReactNode
169 | label: string
170 | onClick: () => void
171 | }
172 |
173 | export type DragLayerProps = {
174 | pluginsData: ReactBlockTextPluginData[]
175 | blockProps: Omit[]
176 | dragIndex: number
177 | }
178 |
179 | export type SelectionRectProps = {
180 | top: number
181 | left: number
182 | width: number
183 | height: number
184 | }
185 |
186 | export type QueryMenuData = {
187 | id: string
188 | query: string
189 | top?: number
190 | bottom?: number
191 | left: number
192 | noSlash?: boolean
193 | }
194 |
195 | export type TopLeft = {
196 | top: number
197 | left: number
198 | }
199 |
200 | export type SelectionTextData = {
201 | items: ReactBlockTextDataItem[]
202 | startId: string
203 | text: string
204 | }
205 |
206 | export type SelectionRectData = SelectionRectProps & {
207 | isSelecting: boolean
208 | anchorTop: number
209 | anchorLeft: number
210 | selectedIds: string[]
211 | }
212 |
213 | export type DragData = {
214 | dragIndex: number
215 | dropIndex: number
216 | isTop: boolean | null
217 | }
218 |
219 | export type EditorRefRegistry = Record
220 |
221 | export type XY = {
222 | x: number
223 | y: number
224 | }
225 |
226 | export type DragAndDropCollect = {
227 | handlerId: string | symbol | null
228 | }
229 |
230 | export type ArrowData = {
231 | isTop: boolean
232 | index: number
233 | offset: number
234 | }
235 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/appendItemData.ts:
--------------------------------------------------------------------------------
1 | import { type EditorState, convertToRaw } from 'draft-js'
2 |
3 | import type { ReactBlockTextDataItem } from '../types'
4 |
5 | function appendItemData(item: Omit, editorState: EditorState) {
6 | return {
7 | metadata: '',
8 | ...item,
9 | data: JSON.stringify(convertToRaw(editorState.getCurrentContent())),
10 | } as ReactBlockTextDataItem
11 | }
12 |
13 | export default appendItemData
14 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/countCharactersOnLastBlockLines.ts:
--------------------------------------------------------------------------------
1 | import findParentBlock from './findParentBlock'
2 | import findChildWithProperty from './findChildWithProperty'
3 |
4 | function countCharactersOnLastBlockLines(id: string, editorElement: HTMLElement | null | undefined, injectionElement: HTMLElement) {
5 | if (!(editorElement && injectionElement)) return 0
6 |
7 | // We want to reconstitute the Block element with its content
8 | const blockElement = findParentBlock(id, editorElement)
9 |
10 | if (!blockElement) return null
11 |
12 | // So we clone it
13 | const blockElementClone = blockElement.cloneNode(true) as HTMLElement
14 | // Find its content
15 | const contentElement = findChildWithProperty(blockElementClone, 'data-contents', 'true')
16 |
17 | if (!contentElement) return null
18 |
19 | injectionElement.appendChild(blockElementClone)
20 |
21 | // Remove first content blocks, keep last one
22 | for (let i = 0; i < contentElement.children.length - 1; i++) {
23 | contentElement.removeChild(contentElement.children[i])
24 | }
25 |
26 | // We count the characters of each line
27 | const count = [0]
28 | const text = contentElement.innerText ?? ''
29 | const words = text.split(/ |-/)
30 | let height = contentElement.offsetHeight
31 |
32 | contentElement.innerText = text
33 |
34 | const { length } = words
35 |
36 | for (let i = 0; i < length; i++) {
37 | const lastWord = words.pop() ?? ''
38 |
39 | count[0] += lastWord.length + 1
40 |
41 | contentElement.innerText = contentElement.innerText.slice(0, -(lastWord.length + 1))
42 |
43 | if (contentElement.offsetHeight < height) {
44 | height = contentElement.offsetHeight
45 | count[0] -= 1
46 |
47 | if (contentElement.innerText.length === 0) break
48 |
49 | count.unshift(0)
50 | }
51 | }
52 |
53 | injectionElement.removeChild(blockElementClone)
54 |
55 | return count
56 | }
57 |
58 | export default countCharactersOnLastBlockLines
59 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/findAttributeInParents.ts:
--------------------------------------------------------------------------------
1 | function findAttributeInParents(element: HTMLElement, attribute: string) {
2 | if (element.hasAttribute(attribute)) return element.getAttribute(attribute)
3 |
4 | if (!element.parentElement) return null
5 |
6 | return findAttributeInParents(element.parentElement, attribute)
7 | }
8 |
9 | export default findAttributeInParents
10 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/findChildWithProperty.ts:
--------------------------------------------------------------------------------
1 | function findChildWithProperty(parent: HTMLElement, name: string, value: string): HTMLElement | null {
2 | if (parent.getAttribute(name) === value) return parent
3 | if (!parent.children) return null
4 |
5 | for (let i = 0; i < parent.children.length; i++) {
6 | const child = parent.children[i] as HTMLElement
7 | const found = findChildWithProperty(child, name, value)
8 |
9 | if (found) return found
10 | }
11 |
12 | return null
13 | }
14 |
15 | export default findChildWithProperty
16 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/findParentBlock.ts:
--------------------------------------------------------------------------------
1 | function findParentBlock(id: string, element: HTMLElement) {
2 | if (element.id === id) return element
3 | if (!element.parentElement) return null
4 |
5 | return findParentBlock(id, element.parentElement)
6 | }
7 |
8 | export default findParentBlock
9 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/findParentWithId.ts:
--------------------------------------------------------------------------------
1 | function findParentWithId(element: HTMLElement, id: string) {
2 | if (!element) return
3 | if (element.id === id) return element
4 | if (!element.parentElement) return null
5 |
6 | return findParentWithId(element.parentElement, id)
7 | }
8 |
9 | export default findParentWithId
10 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/findScrollParent.ts:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/questions/35939886/find-first-scrollable-parent
2 | const properties = ['overflow', 'overflow-x', 'overflow-y']
3 |
4 | const isScrollable = (node: Element) => {
5 | if (!(node instanceof HTMLElement || node instanceof SVGElement)) return false
6 |
7 | const style = getComputedStyle(node)
8 |
9 | return properties.some(propertyName => {
10 | const value = style.getPropertyValue(propertyName)
11 |
12 | return value === 'auto' || value === 'scroll'
13 | })
14 | }
15 |
16 | export const findScrollParent = (node: Element): HTMLElement => {
17 | let currentParent = node.parentElement
18 |
19 | while (currentParent) {
20 | if (isScrollable(currentParent)) return currentParent
21 |
22 | currentParent = currentParent.parentElement
23 | }
24 |
25 | return (document.scrollingElement as HTMLElement) || document.documentElement
26 | }
27 |
28 | export default findScrollParent
29 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/findSelectionRectIds.ts:
--------------------------------------------------------------------------------
1 | import type { SelectionRectData } from '../types'
2 |
3 | // Lookup the ids under the given selectionRect
4 | function findSelectionRectIds(selectionRefs: Record, selectionRect: SelectionRectData): string[] {
5 | if (!selectionRect.width || !selectionRect.height || !selectionRefs) return []
6 |
7 | const ids: string[] = []
8 |
9 | Object.entries(selectionRefs).forEach(([id, element]) => {
10 | if (!element) return
11 |
12 | const contentElement = element.parentElement
13 |
14 | if (!contentElement) return
15 |
16 | if (
17 | selectionRect.left + selectionRect.width < element.offsetLeft + contentElement.offsetLeft
18 | || selectionRect.left > element.offsetLeft + element.offsetWidth + contentElement.offsetLeft
19 | || selectionRect.top + selectionRect.height < element.offsetTop + contentElement.offsetTop
20 | || selectionRect.top > element.offsetTop + element.offsetHeight + contentElement.offsetTop
21 | ) {
22 | return
23 | }
24 |
25 | ids.push(id)
26 | })
27 |
28 | return ids
29 | }
30 |
31 | export default findSelectionRectIds
32 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/forceContentFocus.ts:
--------------------------------------------------------------------------------
1 | import type { EditorRefRegistry } from '../types'
2 |
3 | function forceContentFocus(editorRefs: EditorRefRegistry, id: string) {
4 | if (!editorRefs[id]) return
5 | if (editorRefs[id]?.editorContainer?.contains(document.activeElement)) return
6 |
7 | editorRefs[id]?.focus()
8 | }
9 |
10 | export default forceContentFocus
11 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/getFirstLineFocusOffset.ts:
--------------------------------------------------------------------------------
1 | import findParentBlock from './findParentBlock'
2 | import findChildWithProperty from './findChildWithProperty'
3 |
4 | function getFirstLineFocusOffset(id: string, focusOffset: number, editorElement: HTMLElement | null | undefined, injectionElement: HTMLElement) {
5 | if (!(editorElement && injectionElement)) return 0
6 |
7 | // We want to reconstitute the Block element with its content
8 | const blockElement = findParentBlock(id, editorElement)
9 |
10 | if (!blockElement) return 0
11 |
12 | // So we clone it
13 | const blockElementClone = blockElement.cloneNode(true) as HTMLElement
14 | // Find its content
15 | const contentElement = findChildWithProperty(blockElementClone, 'data-contents', 'true')
16 |
17 | if (!contentElement) return 0
18 |
19 | injectionElement.appendChild(blockElementClone)
20 |
21 | // Remove first content blocks, keep last one
22 | for (let i = 0; i < contentElement.children.length - 1; i++) {
23 | contentElement.removeChild(contentElement.children[i])
24 | }
25 |
26 | // Then we calculate the offset from the start of the last line
27 | const text = contentElement.innerText ?? ''
28 | const words = text.split(/ |-/)
29 | const height = contentElement.offsetHeight
30 |
31 | contentElement.innerText = text
32 |
33 | for (let i = 0; i < words.length; i++) {
34 | const lastWord = words.pop() ?? ''
35 |
36 | contentElement.innerText = contentElement.innerText.slice(0, -(lastWord.length + 1))
37 |
38 | if (contentElement.offsetHeight < height) {
39 | injectionElement.removeChild(blockElementClone)
40 |
41 | return focusOffset + words.join(' ').length + 1
42 | }
43 | }
44 |
45 | injectionElement.removeChild(blockElementClone)
46 |
47 | return focusOffset
48 | }
49 |
50 | export default getFirstLineFocusOffset
51 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/getLastLineFocusOffset.ts:
--------------------------------------------------------------------------------
1 | import findParentBlock from './findParentBlock'
2 | import findChildWithProperty from './findChildWithProperty'
3 |
4 | function getLastLineFocusOffset(id: string, focusOffset: number, editorElement: HTMLElement | null | undefined, injectionElement: HTMLElement) {
5 | if (!(editorElement && injectionElement)) return 0
6 |
7 | // We want to reconstitute the Block element with its content
8 | const blockElement = findParentBlock(id, editorElement)
9 |
10 | if (!blockElement) return 0
11 |
12 | // So we clone it
13 | const blockElementClone = blockElement.cloneNode(true) as HTMLElement
14 | // Find its content
15 | const contentElement = findChildWithProperty(blockElementClone, 'data-contents', 'true')
16 |
17 | if (!contentElement) return 0
18 |
19 | injectionElement.appendChild(blockElementClone)
20 |
21 | // Remove first content blocks, keep last one
22 | for (let i = 0; i < contentElement.children.length - 1; i++) {
23 | contentElement.removeChild(contentElement.children[i])
24 | }
25 |
26 | // Then we calculate the offset from the start of the last line
27 | let offset = 0
28 | let hasAcheivedOffset = false
29 | const text = contentElement.innerText ?? ''
30 | const words = text.split(/ |-/)
31 | const height = contentElement.offsetHeight
32 |
33 | contentElement.innerText = text
34 |
35 | const { length } = words
36 |
37 | for (let i = 0; i < length; i++) {
38 | if (contentElement.offsetHeight < height) {
39 | injectionElement.removeChild(blockElementClone)
40 |
41 | return offset
42 | }
43 |
44 | const lastWord = words.pop() ?? ''
45 |
46 | contentElement.innerText = contentElement.innerText.slice(0, -(lastWord.length + 1))
47 |
48 | if (contentElement.innerText.length < focusOffset) {
49 | if (hasAcheivedOffset) {
50 | offset += lastWord.length + 1
51 | }
52 | else {
53 | offset += focusOffset - contentElement.innerText.length - 1
54 | hasAcheivedOffset = true
55 | }
56 | }
57 | }
58 |
59 | injectionElement.removeChild(blockElementClone)
60 |
61 | return focusOffset
62 | }
63 |
64 | export default getLastLineFocusOffset
65 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/getQueryMenuData.ts:
--------------------------------------------------------------------------------
1 | import type { EditorRefRegistry, QueryMenuData } from '../types'
2 |
3 | import { QUERY_MENU_HEIGHT } from '../constants'
4 |
5 | // Get the query menu position based on the current selection
6 | function getQueryMenuData(editorRefs: EditorRefRegistry, id: string, rootElement: HTMLElement): QueryMenuData | null {
7 | const range = window.getSelection()?.getRangeAt(0)?.cloneRange()
8 |
9 | if (!range) return null
10 |
11 | range.collapse(true)
12 |
13 | const rects = range.getClientRects()
14 | const rootRect = rootElement.getBoundingClientRect()
15 |
16 | if (rects.length) {
17 | return {
18 | id,
19 | query: '',
20 | left: rects[0].right - rootRect.left - 6,
21 | ...getQueryMenuYPosition(rects[0], rootRect, rootElement.offsetTop, false),
22 | }
23 | }
24 |
25 | const editorRef = editorRefs[id]
26 |
27 | if (!editorRef) return null
28 |
29 | const editorRects = editorRef.editorContainer?.getClientRects()
30 |
31 | if (!editorRects?.length) return null
32 |
33 | return {
34 | id,
35 | query: '',
36 | left: editorRects[0].left - rootRect.left - 2,
37 | ...getQueryMenuYPosition(editorRects[0], rootRect, rootElement.offsetTop, true),
38 | }
39 | }
40 |
41 | function getQueryMenuYPosition(rect: DOMRectReadOnly, rootRect: DOMRect, rootOffsetTop: number, isEditorRect: boolean) {
42 | const top = (isEditorRect ? rect.top + 24 : rect.bottom + 4) - rootRect.top
43 |
44 | if (top + rootOffsetTop + QUERY_MENU_HEIGHT < window.innerHeight) return { top }
45 |
46 | const bottom = rootRect.height - rect.top + rootRect.top + 4
47 |
48 | return { bottom }
49 | }
50 |
51 | export default getQueryMenuData
52 |
--------------------------------------------------------------------------------
/react-block-text/src/utils/getRelativeMousePosition.ts:
--------------------------------------------------------------------------------
1 | import type { XY } from '../types'
2 |
3 | function getRelativeMousePosition(element: HTMLElement, mousePosition: XY) {
4 | const rootRect = element.getBoundingClientRect()
5 |
6 | return {
7 | x: mousePosition.x - element.clientLeft - rootRect.left,
8 | y: mousePosition.y - element.clientTop - rootRect.top,
9 | }
10 | }
11 |
12 | export default getRelativeMousePosition
13 |
--------------------------------------------------------------------------------
/react-block-text/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/react-block-text/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | './index.html',
5 | './src/**/*.{js,ts,jsx,tsx}',
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | prefix: 'rbt-',
12 | corePlugins: {
13 | preflight: false,
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/react-block-text/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/react-block-text/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/react-block-text/vite.config.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs'
2 | import path from 'node:path'
3 |
4 | import { defineConfig } from 'vite'
5 | import react from '@vitejs/plugin-react'
6 | import dts from 'vite-plugin-dts'
7 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
8 | import rollupNodePolyFill from 'rollup-plugin-polyfill-node'
9 | import analyze from 'rollup-plugin-analyzer'
10 | import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill'
11 |
12 | const rollupPlugins = [rollupNodePolyFill()]
13 |
14 | if (process.env.BUNDLE_ANALYSIS) {
15 | rollupPlugins.push(analyze({
16 | writeTo: string => {
17 | fs.writeFileSync(path.join(__dirname, 'bundle-analysis.txt'), string)
18 | },
19 | }))
20 | }
21 |
22 | // https://vitejs.dev/config/
23 | export default defineConfig({
24 | plugins: [react(), dts(), cssInjectedByJsPlugin()],
25 | optimizeDeps: {
26 | esbuildOptions: {
27 | plugins: [
28 | nodeModulesPolyfillPlugin({
29 | globals: {
30 | process: true,
31 | Buffer: false,
32 | },
33 | }),
34 | ],
35 | },
36 | },
37 | define: {
38 | global: 'globalThis',
39 | },
40 | build: {
41 | lib: {
42 | entry: 'src/index.ts',
43 | name: 'react-block-text',
44 | },
45 | rollupOptions: {
46 | external: ['react', 'react-dom'],
47 | plugins: rollupPlugins,
48 | output: {
49 | exports: 'named',
50 | globals: {
51 | react: 'React',
52 | 'react-dom': 'ReactDOM',
53 | },
54 | },
55 | },
56 | },
57 | })
58 |
--------------------------------------------------------------------------------