import React, { forwardRef, useCallback, useMemo, useRef } from 'react' import { $generateHtmlFromNodes } from '@lexical/html' import { ListItemNode, ListNode } from '@lexical/list' import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin' import type { InitialConfigType } from '@lexical/react/LexicalComposer' import { LexicalComposer } from '@lexical/react/LexicalComposer' import { ContentEditable } from '@lexical/react/LexicalContentEditable' import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' import { HeadingNode } from '@lexical/rich-text' import type { Theme } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles' import { Container } from '@toptal/picasso-container' import { Typography } from '@toptal/picasso-typography' import type { BaseProps } from '@toptal/picasso-shared' import { noop } from '@toptal/picasso-utils' import type { LexicalEditor as LexicalEditorType } from 'lexical' import { $getRoot } from 'lexical' import ToolbarPlugin from '../LexicalEditorToolbarPlugin' import { RTEPluginContextProvider } from '../plugins/api' import { ListPlugin, TextLengthPlugin, HeadingsReplacementPlugin, TriggerInitialOnChangePlugin, FocusOnLabelClickPlugin, } from '../plugins' import type { ASTType } from '../RichText' import { useOnFocus, useTypographyClasses } from './hooks' import { useComponentPlugins } from './hooks/useComponentPlugins/useComponentPlugins' import styles from './styles' import type { ChangeHandler, EditorPlugin, TextLengthChangeHandler, } from './types' import { cleanupHtmlOutput, createLexicalTheme, setEditorValue } from './utils' import type { CustomEmojiGroup } from '../plugins/EmojiPlugin' const useStyles = makeStyles(styles, { name: 'LexicalEditor', }) const removeAttributesFromString = (htmlString: string) => { return htmlString.replace(/\s(class|dir|value)="[^"]*"/g, '') } export type Props = BaseProps & { /** Indicates that an element is to be focused on page load */ autoFocus?: boolean /** Default value in [HAST](https://github.com/syntax-tree/hast) format */ defaultValue?: ASTType /** * This Boolean attribute indicates that the user cannot interact with the control. */ disabled?: boolean /** unique identifier */ id: string /** Name attribute of the input element */ // name?: string /** * Callback on text change */ onChange?: ChangeHandler /** * Callback for blur event */ onBlur?: () => void /** * Callback for focus event */ onFocus?: () => void /** * Callback on text length change */ onTextLengthChange: TextLengthChangeHandler /** The placeholder attribute specifies a short hint that describes the expected value of a text editor. */ placeholder?: string testIds?: { editor?: string headerSelect?: string boldButton?: string italicButton?: string unorderedListButton?: string orderedListButton?: string } customEmojis?: CustomEmojiGroup[] /** List of plugins to enable on the editor */ plugins?: EditorPlugin[] hiddenInputId?: string } const useLexicalTheme = (classes: Record) => { const bodyTypographyClassName = useTypographyClasses({ variant: 'body', size: 'medium', }) const headingTypographyClassName = useTypographyClasses({ variant: 'heading', size: 'medium', }) const theme = useMemo( () => createLexicalTheme({ typographyClassNames: { root: bodyTypographyClassName, heading: headingTypographyClassName, }, classes, }), [bodyTypographyClassName, headingTypographyClassName, classes] ) return theme } const LexicalEditor = forwardRef(function LexicalEditor( props: Props, ref ) { const { plugins = [], autoFocus = false, defaultValue, disabled = false, id, onChange = noop, onTextLengthChange = noop, onFocus = noop, onBlur = noop, placeholder, testIds, customEmojis, hiddenInputId, } = props const classes = useStyles() const toolbarRef = useRef(null) const theme = useLexicalTheme(classes) const { componentPlugins, lexicalNodes } = useComponentPlugins( plugins, customEmojis ) const editorConfig: InitialConfigType = useMemo( () => ({ editorState: (editor: LexicalEditorType) => setEditorValue(editor, defaultValue), theme, onError(error: Error) { throw error }, namespace: 'editor', nodes: [ListNode, ListItemNode, HeadingNode, ...lexicalNodes], editable: !disabled, }), [defaultValue, theme, disabled, lexicalNodes] ) const handleChange = useCallback( (editorState, editor) => { editorState.read(() => { const root = $getRoot() const topLevelChildren = root.getChildren() const hasNoChildren = topLevelChildren.length === 0 const hasOneEmptyChild = topLevelChildren.length === 1 && topLevelChildren[0].isEmpty() if (hasNoChildren || hasOneEmptyChild) { onChange('') return } const htmlValue = $generateHtmlFromNodes(editor, null) const [cleanedValue] = [htmlValue] .map(removeAttributesFromString) .map(cleanupHtmlOutput) onChange(cleanedValue) }) }, [onChange] ) const { focused, handleFocus, handleBlur } = useOnFocus({ onFocus, onBlur, internalRefs: [toolbarRef], }) return (
{defaultValue ? ( ) : null} {autoFocus && } {hiddenInputId && ( )} {componentPlugins}
} placeholder={ {placeholder} } ErrorBoundary={LexicalErrorBoundary} />
) }) LexicalEditor.displayName = 'LexicalEditor' export default LexicalEditor