我正在实现一个基本的富文本编辑器。我正在尝试实现粗体、斜体、下划线和删除线。几个问题:
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;
我尝试寻找答案。我查看了操场并尝试模仿代码。我已经看过基本的反应示例并且也遵循了。
我今天花了一些时间处理这个问题,发现 css 文件的导入没有加载到组件中。我的解决方法是将 App.css 文件放在公共文件夹或 CDN 中,然后从那里使用它,它的效果非常好。 github 上有一个关于此问题的持续错误报告。