我在 React typescript 和 tailwinds 项目中实现 Lexical 时遇到问题

问题描述 投票:0回答:1

我正在实现一个基本的富文本编辑器。我正在尝试实现粗体、斜体、下划线和删除线。几个问题:

  • 下划线和删除线不显示。即使当我检查元素时。
  • 粗体和斜体不能同时使用
ToolbarPlugin.tsx
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
import { INSERT_UNORDERED_LIST_COMMAND, insertList } from '@lexical/list';
import {
    $getSelection,
    $isRangeSelection,
    CAN_REDO_COMMAND,
    CAN_UNDO_COMMAND,
    FORMAT_ELEMENT_COMMAND,
    FORMAT_TEXT_COMMAND,
    REDO_COMMAND,
    SELECTION_CHANGE_COMMAND,
    UNDO_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useRef, useState } from 'react';
import { ToolbarButton } from '.';
import {
    faRotateRight,
    faRotateLeft,
    faBold,
    faItalic,
    faUnderline,
    faStrikethrough,
    faAlignLeft,
    faAlignCenter,
    faAlignRight,
    faListDots,
} from '@fortawesome/free-solid-svg-icons';
import {
    $patchStyleText,
    $getSelectionStyleValueForProperty,
} from '@lexical/selection';
import ColorPicker from '../Color/ColorPicker';

const LowPriority = 1;

export const ToolbarPlugin: React.FC = () => {
    const [editor] = useLexicalComposerContext();
    const toolbarRef = useRef(null);
    const [canUndo, setCanUndo] = useState(false);
    const [canRedo, setCanRedo] = useState(false);
    const [isBold, setIsBold] = useState(false);
    const [isItalic, setIsItalic] = useState(false);
    const [isUnderline, setIsUnderline] = useState(false);
    const [isStrikethrough, setIsStrikethrough] = useState(false);

    const [fontColor, setFontColor] = useState<string>('#000');

    const applyStyleText = useCallback(
        (styles: Record<string, string>, skipHistoryStack?: boolean) => {
            editor.update(
                () => {
                    const selection = $getSelection();
                    if (selection !== null) {
                        $patchStyleText(selection, styles);
                    }
                },
                skipHistoryStack ? { tag: 'historic' } : {},
            );
        },
        [editor],
    );

    const onFontColorSelect = useCallback(
        (value: string) => {
            applyStyleText({ color: value });
        },
        [applyStyleText],
    );

    const updateToolbar = useCallback(() => {
        const selection = $getSelection();
        if ($isRangeSelection(selection)) {
            // Update text format
            setIsBold(selection.hasFormat('bold'));
            setIsItalic(selection.hasFormat('italic'));
            setIsUnderline(selection.hasFormat('underline'));
            setIsStrikethrough(selection.hasFormat('strikethrough'));
            setFontColor(
                $getSelectionStyleValueForProperty(selection, 'color', '#000'),
            );
        }

    }, []);

    useEffect(() => {
        return mergeRegister(
            editor.registerUpdateListener(({ editorState }) => {
                editorState.read(() => {
                    updateToolbar();
                });
            }),
            editor.registerCommand(
                SELECTION_CHANGE_COMMAND,
                () => {
                    updateToolbar();
                    return false;
                },
                LowPriority,
            ),
            editor.registerCommand(
                FORMAT_TEXT_COMMAND,
                (format) => {
                    console.log('format', format);
                    const selection = $getSelection();
                    if (!$isRangeSelection(selection)) {
                        console.error('Invalid selection');
                        return false;
                    }
                    selection.formatText(format);
                    updateToolbar();
                    return true;
                },
                LowPriority,
            ),
            editor.registerCommand(
                CAN_UNDO_COMMAND,
                (payload) => {
                    setCanUndo(payload);
                    return false;
                },
                LowPriority,
            ),
            editor.registerCommand(
                CAN_REDO_COMMAND,
                (payload) => {
                    setCanRedo(payload);
                    return false;
                },
                LowPriority,
            ),
            editor.registerCommand(
                INSERT_UNORDERED_LIST_COMMAND,
                () => {
                    insertList(editor, 'bullet');
                    return false;
                },
                LowPriority,
            ),
        );
    }, [editor, updateToolbar]);

    const Divider: React.FC = () => {
        return <div className="w-1 bg-gray-300 mx-4" />;
    };

    return (
        <div
            className={`flex mb-1 p-4 items-center bg-transparent grid grid-cols-12 md:grid-cols-8 gap-2`}
            ref={toolbarRef}
        >
            <ToolbarButton
                onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
                isDisabled={!canRedo}
                label="Redo"
                iconName={faRotateRight}
            />
            <ToolbarButton
                onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
                isDisabled={!canUndo}
                label="Undo"
                iconName={faRotateLeft}
            />
            <Divider />
            <ToolbarButton
                onClick={() =>
                    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
                }
                isDisabled={false}
                isActive={isBold}
                label="Bold"
                iconName={faBold}
            />
            <ToolbarButton
                onClick={() =>
                    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
                }
                isDisabled={false}
                isActive={isItalic}
                label="Italic"
                iconName={faItalic}
            />
            <ToolbarButton
                onClick={() =>
                    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')
                }
                isDisabled={false}
                isActive={isUnderline}
                label="Underline"
                iconName={faUnderline}
            />
            <ToolbarButton
                onClick={() =>
                    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')
                }
                isDisabled={false}
                isActive={isStrikethrough}
                label="Strikethrough"
                iconName={faStrikethrough}
            />
            <Divider />
            <ToolbarButton
                onClick={() =>
                    editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left')
                }
                label="Left Align"
                isDisabled={false}
                iconName={faAlignLeft}
            />
            <ToolbarButton
                onClick={() =>
                    editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center')
                }
                label="Center Align"
                isDisabled={false}
                iconName={faAlignCenter}
            />
            <ToolbarButton
                onClick={() =>
                    editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right')
                }
                label="Right Align"
                isDisabled={false}
                iconName={faAlignRight}
            />
            {/*<ToolbarButton
                //Figure out lists
                onClick={() =>
                    editor.dispatchCommand(
                        INSERT_UNORDERED_LIST_COMMAND,
                        undefined,
                    )
                }
                label="BulletList"
                isDisabled={false}
                iconName={faListDots}
            /><Divider />*/}

            <ColorPicker color={fontColor} onChange={onFontColorSelect} />
        </div>
    );
};

export default ToolbarPlugin;
Theme
export const theme = () => {
    return {
        code: 'bg-gray-200 font-mono block px-8 py-8 pt-0.5 text-sm md:text-base',
        heading: {
            h1: 'text-2xl font-semibold mt-0 mb-1',
            h2: 'text-lg font-semibold mt-1 uppercase',
            h3: 'font-semibold mt-0 mb-1',
            h4: 'font-semibold mt-0 mb-1',
            h5: 'font-semibold mt-0 mb-1',
        },
        image: 'editor-image',
        link: 'text-blue-500 underline text-bold',
        list: {
            nested: {
                listitem: 'ml-4', // Indent nested list items by 1rem
                ol: 'list-decimal pl-8',
                ul: 'list-disc pl-8',
            },
            ol: 'list-decimal pl-8', // Use decimal numbers for ordered lists and add left padding of 2rem
            ul: 'list-disc pl-8', // Use disc bullets for unordered lists and add left padding of 2rem
            listitem: 'mb-2', // Add bottom margin of 0.5rem to list items
            listitemChecked: 'line-through text-gray-500', // Style checked list items with line-through and gray text
            listitemUnchecked: '', // Unchecked list items don't require additional styling
        },
        ltr: 'text-left',
        paragraph: 'mt-0 mb-2',
        quote: 'ml-4 italic border-l-4 border-gray-300 pl-4 py-2',
        rtl: 'text-right',
        text: {
            bold: 'font-bold',
            code: 'bg-gray-300 px-1 font-mono text-sm',
            hashtag: 'text-blue-600',
            italic: 'italic',
            overflowed: 'editor-text-overflowed',
            strikethrough: 'line-through',
            underline: 'underline',
            underlineStrikethrough: 'underline line-through',
        }
    };
};
RichText.tsx
import { useContext, useMemo, PropsWithChildren, useEffect, useState } from 'react';
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
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 { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import ToolbarPlugin from './Toolbar/ToolbarPlugin';
import { ThemeContext, ThemeContextType } from '@contexts';
import { AutoLinkNode } from '@lexical/link';
import { AutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin';
import { MATCHERS, theme } from './Helpers';
import { LexicalEditor } from 'lexical';
import { EditorRefPlugin } from '@lexical/react/LexicalEditorRefPlugin';
import InitPlugin from './InitPlugin';
import EditablePlugin from './EditablePlugin';
import { ListNode, ListItemNode } from '@lexical/list';
import { ListPlugin } from '@lexical/react/LexicalListPlugin';

interface RichTextProps {
    editorRef: React.MutableRefObject<LexicalEditor | undefined>;
    editable: boolean;
    initStringId: number;
}

const RichText: React.FC<PropsWithChildren<RichTextProps>> = ({
    editorRef,
    editable,
    initStringId,
}: RichTextProps) => {
    const { isDarkMode } = useContext(ThemeContext) as ThemeContextType;
    const [initialized, setInitialized] = useState<boolean>(true);

    const secondaryBackground = useMemo(
        () => (isDarkMode ? 'bg-neutral-800' : 'bg-neutral-300'),
        [isDarkMode],
    );
    const textColor = useMemo(
        () => (isDarkMode ? 'text-neutral-200' : 'text-neutral-800'),
        [isDarkMode],
    );

    const Editor: React.FC = () => {
        const Placeholder: React.FC = () => {
            return (
                <div className="editor-placeholder">
                    Enter some rich text...
                </div>
            );
        };

        const editorConfig = {
            namespace: 'Phormulary',
            nodes: [AutoLinkNode, ListNode, ListItemNode],
            theme,
            onError(error: Error) {
                () => console.error(error);
            },
            editable: false,
        };

        return (
            <LexicalComposer initialConfig={editorConfig}>
                <div
                    className={`flex flex-col justify-center items-center font-normal text-left ${textColor}`}
                >
                    {editable && <ToolbarPlugin />}
                    <div
                        className={`min-h-150 mt-2 w-full text-sm outline-none px-10 py-15 caret-black rounded-md border-2 border-solid ${secondaryBackground}`}
                    >
                        <RichTextPlugin
                            contentEditable={
                                <ContentEditable
                                    className={`min-h-150 outline-none pt-2 bg-transparent`}
                                />
                            }
                            ErrorBoundary={LexicalErrorBoundary}
                            placeholder={<Placeholder />}
                        />
                        <EditorRefPlugin editorRef={editorRef} />
                        <AutoLinkPlugin matchers={MATCHERS} />
                        <HistoryPlugin />
                        <AutoFocusPlugin />
                        <ListPlugin />
                        <InitPlugin InitStringId={initStringId} />
                        <EditablePlugin editable={editable} />
                    </div>
                </div>
            </LexicalComposer>
        );
    };

    useEffect(() => {
        if (initialized)
            setInitialized(false);
    }, [initialized]);

    return (
        <div className={`w-full rounded-md bg-transparent`}>
            <Editor />
        </div>
    );
};

export default RichText;

我尝试寻找答案。我查看了操场并尝试模仿代码。我已经看过基本的反应示例并且也遵循了。

reactjs tailwind-css lexicaljs
1个回答
0
投票

我今天花了一些时间处理这个问题,发现 css 文件的导入没有加载到组件中。我的解决方法是将 App.css 文件放在公共文件夹或 CDN 中,然后从那里使用它,它的效果非常好。 github 上有一个关于此问题的持续错误报告。

© www.soinside.com 2019 - 2024. All rights reserved.