feat: implement command handling and custom rendering in editor and message components

This commit is contained in:
2026-04-21 07:25:11 +10:00
parent 8060d50a6f
commit e5f74ec13e
11 changed files with 270 additions and 74 deletions

View File

@@ -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}