import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { InputContainer } from '@os-design/core'; import type { WithSize } from '@os-design/styles'; import { useForwardedRef, useForwardedState } from '@os-design/utils'; import { Editor as DraftEditor, type EditorProps as DraftEditorProps, EditorState, } from 'draft-js'; import { forwardRef, useCallback, useState } from 'react'; import BlockToolbar from './BlockToolbar.js'; import StyleToolbar from './StyleToolbar.js'; import type { BlockToolbarItem } from './blocks/types.js'; import useBlockToolbarProps from './hooks/useBlockToolbarProps.js'; import usePastedTextHandler from './hooks/usePastedTextHandler.js'; import useReturnHandler from './hooks/useReturnHandler.js'; import useStyleToolbarProps from './hooks/useStyleToolbarProps.js'; import defaultDraftJsStyles from './styles/defaultDraftJsStyles.js'; import overrideDraftJsStyles from './styles/overrideDraftJsStyles.js'; import createEmptyEditorState from './utils/createEmptyEditorState.js'; import defaultStyleToolbarItems, { type StyleToolbarItem, } from './utils/defaultStyleToolbarItems.js'; export interface EditorProps extends Omit, WithSize { /** * Whether the editor is disabled. * @default false */ disabled?: boolean; /** * Available styles in the toolbar. * @default undefined */ styleToolbarItems?: StyleToolbarItem[]; /** * Available blocks in the toolbar. * @default undefined */ blockToolbarItems?: BlockToolbarItem[]; /** * The editor state. * @default undefined */ value?: EditorState; /** * The default value. * @default undefined */ defaultValue?: EditorState; /** * The change event handler. * @default undefined */ onChange?: (value: EditorState) => void; } const disabledStyles = (p) => p.disabled && css` cursor: not-allowed; `; const Container = styled(InputContainer)` height: unset; cursor: text; padding: ${(p) => p.theme.editorPaddingVertical}em ${(p) => p.theme.inputPaddingHorizontal}em; min-height: ${(p) => p.theme.editorMinHeight}em; ${defaultDraftJsStyles}; ${overrideDraftJsStyles}; ${disabledStyles}; `; /** * Rich text editor based on the Draft.js. */ const Editor = forwardRef( ( { disabled = false, styleToolbarItems = defaultStyleToolbarItems, blockToolbarItems = [], value, defaultValue = createEmptyEditorState(), onChange, size, readOnly, handleReturn = () => 'not-handled', handlePastedText = () => 'not-handled', ...rest }, ref ) => { const [editorRef, mergedEditorRef] = useForwardedRef(ref); const [forwardedValue, setForwardedValue] = useForwardedState({ value, defaultValue, onChange, }); // Used by ImageBlock to make the editor read-only // while an image is being uploaded const [innerReadOnly, setInnerReadOnly] = useState(false); // Get the trigger and visible props for the toolbars const styleToolbarProps = useStyleToolbarProps( forwardedValue || defaultValue ); const blockToolbarProps = useBlockToolbarProps( forwardedValue || defaultValue, (blockToolbarItems || []).length > 0 ); // Move the caret to the end of the content when the focus event was fired const setEditorState = useCallback( (editorState: EditorState) => { if (!editorState.getSelection().getHasFocus()) { setForwardedValue(EditorState.moveSelectionToEnd(editorState)); return; } setForwardedValue(editorState); }, [setForwardedValue] ); const blockRenderer = useCallback( (block) => { const blockConfig = blockToolbarItems.find( (item) => item.type === block.getType() ); if (!blockConfig) return null; return { component: blockConfig.component }; }, [blockToolbarItems] ); // Custom handlers const returnHandler = useReturnHandler(setForwardedValue, handleReturn); const pastedTextHandler = usePastedTextHandler( setForwardedValue, handlePastedText ); return ( <> { if (disabled || !editorRef.current) return; editorRef.current.focus(); }} > ); } ); Editor.displayName = 'Editor'; export default Editor;