feat: implement command handling and custom rendering in editor and message components
This commit is contained in:
@@ -8,7 +8,7 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Box, Scroll, Text } from 'folds';
|
||||
import { Descendant, Editor, createEditor } from 'slate';
|
||||
import { Descendant, Editor, createEditor, Transforms, Range, Element as SlateElement, Text as SlateText, Point } from 'slate';
|
||||
import {
|
||||
Slate,
|
||||
Editable,
|
||||
@@ -23,6 +23,7 @@ import { RenderElement, RenderLeaf } from './Elements';
|
||||
import { CustomElement } from './slate';
|
||||
import * as css from './Editor.css';
|
||||
import { toggleKeyboardShortcut } from './keyboard';
|
||||
import { createCommandElement } from './utils';
|
||||
|
||||
const initialValue: CustomElement[] = [
|
||||
{
|
||||
@@ -52,8 +53,25 @@ const withVoid = (editor: Editor): Editor => {
|
||||
return editor;
|
||||
};
|
||||
|
||||
type CommandValidatorFn = (commandName: string) => boolean;
|
||||
|
||||
let commandValidator: CommandValidatorFn | null = null;
|
||||
|
||||
export const setCommandValidator = (validator: CommandValidatorFn) => {
|
||||
commandValidator = validator;
|
||||
};
|
||||
|
||||
const withCommandAutoConvert = (editor: Editor): Editor => {
|
||||
// Removed auto-conversion on space to prevent focus issues
|
||||
// Commands are now highlighted visually via decorations
|
||||
// and executed when Enter is pressed
|
||||
return editor;
|
||||
};
|
||||
|
||||
export const useEditor = (): Editor => {
|
||||
const [editor] = useState(() => withInline(withVoid(withReact(withHistory(createEditor())))));
|
||||
const [editor] = useState(() =>
|
||||
withCommandAutoConvert(withInline(withVoid(withReact(withHistory(createEditor())))))
|
||||
);
|
||||
return editor;
|
||||
};
|
||||
|
||||
@@ -97,6 +115,49 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
||||
|
||||
const renderLeaf = useCallback((props: RenderLeafProps) => <RenderLeaf {...props} />, []);
|
||||
|
||||
const decorate = useCallback(([node, path]: [any, number[]]) => {
|
||||
const ranges: any[] = [];
|
||||
|
||||
// Only decorate text nodes in the first paragraph
|
||||
if (
|
||||
path.length === 2 &&
|
||||
path[0] === 0 &&
|
||||
path[1] === 0 &&
|
||||
SlateText.isText(node)
|
||||
) {
|
||||
const firstChild = editor.children[0];
|
||||
|
||||
if (SlateElement.isElement(firstChild) && firstChild.type === BlockType.Paragraph) {
|
||||
const [firstInline, secondInline] = firstChild.children;
|
||||
|
||||
// Don't decorate if we already have a CommandElement
|
||||
if (SlateElement.isElement(secondInline) && secondInline.type === BlockType.Command) {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Check if the text matches /command pattern
|
||||
const text = node.text;
|
||||
const match = text.match(/^(\s*\/\w+)/);
|
||||
|
||||
if (match && commandValidator) {
|
||||
const commandText = match[1].trim();
|
||||
const commandName = commandText.substring(1);
|
||||
|
||||
if (commandValidator(commandName)) {
|
||||
// Create decoration for the command text
|
||||
ranges.push({
|
||||
anchor: { path, offset: 0 },
|
||||
focus: { path, offset: match[1].length },
|
||||
pendingCommand: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}, [editor]);
|
||||
|
||||
const handleKeydown: KeyboardEventHandler = useCallback(
|
||||
(evt) => {
|
||||
onKeyDown?.(evt);
|
||||
@@ -143,6 +204,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
||||
renderPlaceholder={renderPlaceholder}
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
decorate={decorate}
|
||||
onKeyDown={handleKeydown}
|
||||
onKeyUp={onKeyUp}
|
||||
onPaste={onPaste}
|
||||
|
||||
@@ -269,6 +269,13 @@ export function RenderLeaf({ attributes, leaf, children }: RenderLeafProps) {
|
||||
{child}
|
||||
</span>
|
||||
);
|
||||
if (leaf.pendingCommand)
|
||||
child = (
|
||||
<span className={css.Command({ active: true })} {...attributes}>
|
||||
<InlineChromiumBugfix />
|
||||
{child}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (child !== children) return child;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './autocomplete';
|
||||
export * from './utils';
|
||||
export * from './Editor';
|
||||
export { CustomEditor, useEditor, setCommandValidator } from './Editor';
|
||||
export * from './Elements';
|
||||
export * from './keyboard';
|
||||
export * from './output';
|
||||
|
||||
1
src/app/components/editor/slate.d.ts
vendored
1
src/app/components/editor/slate.d.ts
vendored
@@ -18,6 +18,7 @@ export type FormattedText = Text & {
|
||||
strikeThrough?: boolean;
|
||||
code?: boolean;
|
||||
spoiler?: boolean;
|
||||
pendingCommand?: boolean;
|
||||
};
|
||||
|
||||
export type LinkElement = {
|
||||
|
||||
Reference in New Issue
Block a user