fix(web): Editor
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, type FC } from 'react';
|
import { useEffect, useState, type FC } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { $getRoot, $getSelection } from 'lexical';
|
import { $getSelection, $isRangeSelection } from 'lexical';
|
||||||
|
|
||||||
import { INSERT_VARIABLE_COMMAND } from '../commands';
|
import { INSERT_VARIABLE_COMMAND } from '../commands';
|
||||||
import type { NodeProperties } from '../../../types'
|
import type { NodeProperties } from '../../../types'
|
||||||
@@ -23,43 +23,55 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerUpdateListener(({ editorState }) => {
|
return editor.registerUpdateListener(({ editorState }) => {
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const root = $getRoot();
|
const selection = $getSelection();
|
||||||
const text = root.getTextContent();
|
|
||||||
const shouldShow = text.includes('/');
|
if (!selection || !$isRangeSelection(selection)) {
|
||||||
|
setShowSuggestions(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorNode = selection.anchor.getNode();
|
||||||
|
const anchorOffset = selection.anchor.offset;
|
||||||
|
|
||||||
|
// Get the text content of the current node
|
||||||
|
const nodeText = anchorNode.getTextContent();
|
||||||
|
|
||||||
|
// Check if we have a '/' at the current position or after line break
|
||||||
|
const textBeforeCursor = nodeText.substring(0, anchorOffset);
|
||||||
|
const shouldShow = textBeforeCursor.endsWith('/') ||
|
||||||
|
(textBeforeCursor === '/' && anchorOffset === 1);
|
||||||
|
|
||||||
setShowSuggestions(shouldShow);
|
setShowSuggestions(shouldShow);
|
||||||
|
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
const selection = $getSelection();
|
const domSelection = window.getSelection();
|
||||||
if (selection) {
|
if (domSelection && domSelection.rangeCount > 0) {
|
||||||
const domSelection = window.getSelection();
|
const range = domSelection.getRangeAt(0);
|
||||||
if (domSelection && domSelection.rangeCount > 0) {
|
const rect = range.getBoundingClientRect();
|
||||||
const range = domSelection.getRangeAt(0);
|
|
||||||
const rect = range.getBoundingClientRect();
|
|
||||||
|
|
||||||
const popupWidth = 280;
|
const popupWidth = 280;
|
||||||
const popupHeight = 200;
|
const popupHeight = 200;
|
||||||
const viewportWidth = window.innerWidth;
|
const viewportWidth = window.innerWidth;
|
||||||
const viewportHeight = window.innerHeight;
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
let left = rect.left;
|
let left = rect.left;
|
||||||
let top = rect.top - 10;
|
let top = rect.top - 10;
|
||||||
|
|
||||||
if (left + popupWidth > viewportWidth) {
|
if (left + popupWidth > viewportWidth) {
|
||||||
left = viewportWidth - popupWidth - 10;
|
left = viewportWidth - popupWidth - 10;
|
||||||
}
|
|
||||||
if (left < 10) {
|
|
||||||
left = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (top - popupHeight < 10) {
|
|
||||||
top = rect.bottom + 10;
|
|
||||||
if (top + popupHeight > viewportHeight) {
|
|
||||||
top = viewportHeight - popupHeight - 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPopupPosition({ top, left });
|
|
||||||
}
|
}
|
||||||
|
if (left < 10) {
|
||||||
|
left = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top - popupHeight < 10) {
|
||||||
|
top = rect.bottom + 10;
|
||||||
|
if (top + popupHeight > viewportHeight) {
|
||||||
|
top = viewportHeight - popupHeight - 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPopupPosition({ top, left });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -112,7 +124,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
|||||||
<div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}>
|
<div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}>
|
||||||
{nodeName}
|
{nodeName}
|
||||||
</div>
|
</div>
|
||||||
{nodeOptions.map((option, index) => {
|
{nodeOptions.map((option) => {
|
||||||
const globalIndex = Object.values(groupedSuggestions).flat().indexOf(option);
|
const globalIndex = Object.values(groupedSuggestions).flat().indexOf(option);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import {
|
|||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
$createTextNode,
|
$createTextNode,
|
||||||
$getRoot,
|
$getRoot,
|
||||||
|
$getSelection,
|
||||||
$setSelection,
|
$setSelection,
|
||||||
$createRangeSelection,
|
$createRangeSelection,
|
||||||
$isParagraphNode,
|
|
||||||
$isTextNode,
|
$isTextNode,
|
||||||
|
$isRangeSelection,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
|
|
||||||
import { $createVariableNode } from '../nodes/VariableNode';
|
import { $createVariableNode } from '../nodes/VariableNode';
|
||||||
@@ -26,64 +27,45 @@ const CommandPlugin = () => {
|
|||||||
INSERT_VARIABLE_COMMAND,
|
INSERT_VARIABLE_COMMAND,
|
||||||
(payload: InsertVariableCommandPayload) => {
|
(payload: InsertVariableCommandPayload) => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const root = $getRoot();
|
const selection = $getSelection();
|
||||||
const text = root.getTextContent();
|
if (!selection || !$isRangeSelection(selection)) return;
|
||||||
const lastSlashIndex = text.lastIndexOf('/');
|
|
||||||
|
|
||||||
// Find the paragraph and the position to insert
|
const anchorNode = selection.anchor.getNode();
|
||||||
const paragraph = root.getFirstChild();
|
const anchorOffset = selection.anchor.offset;
|
||||||
if (!paragraph || !$isParagraphNode(paragraph)) return;
|
|
||||||
|
|
||||||
const children = paragraph.getChildren();
|
if ($isTextNode(anchorNode)) {
|
||||||
let insertPosition = 0;
|
const nodeText = anchorNode.getTextContent();
|
||||||
let currentTextLength = 0;
|
const textBeforeCursor = nodeText.substring(0, anchorOffset);
|
||||||
|
const textAfterCursor = nodeText.substring(anchorOffset);
|
||||||
// Find where to insert the new tag
|
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
const child = children[i];
|
|
||||||
const childText = child.getTextContent();
|
|
||||||
|
|
||||||
if (currentTextLength + childText.length > lastSlashIndex) {
|
// Find the last '/' position
|
||||||
// Split this text node if needed
|
const lastSlashIndex = textBeforeCursor.lastIndexOf('/');
|
||||||
if ($isTextNode(child)) {
|
|
||||||
const beforeSlash = childText.substring(0, lastSlashIndex - currentTextLength);
|
if (lastSlashIndex !== -1) {
|
||||||
const afterSlash = childText.substring(lastSlashIndex - currentTextLength + 1);
|
// Split the text: before slash, insert variable, after cursor
|
||||||
|
const beforeSlash = textBeforeCursor.substring(0, lastSlashIndex);
|
||||||
if (beforeSlash) {
|
|
||||||
child.setTextContent(beforeSlash);
|
// Update the current text node with text before slash
|
||||||
insertPosition = i + 1;
|
anchorNode.setTextContent(beforeSlash);
|
||||||
} else {
|
|
||||||
insertPosition = i;
|
// Create and insert the variable node
|
||||||
child.remove();
|
const tagNode = $createVariableNode(payload.data);
|
||||||
}
|
const spaceNode = $createTextNode(' ');
|
||||||
|
|
||||||
// Insert tag and space
|
anchorNode.insertAfter(tagNode);
|
||||||
const tagNode = $createVariableNode(payload.data);
|
tagNode.insertAfter(spaceNode);
|
||||||
const spaceNode = $createTextNode(' ');
|
|
||||||
|
// Add remaining text if any
|
||||||
if (insertPosition < paragraph.getChildrenSize()) {
|
if (textAfterCursor) {
|
||||||
paragraph.getChildAtIndex(insertPosition)?.insertBefore(tagNode);
|
spaceNode.insertAfter($createTextNode(textAfterCursor));
|
||||||
tagNode.insertAfter(spaceNode);
|
|
||||||
} else {
|
|
||||||
paragraph.append(tagNode);
|
|
||||||
paragraph.append(spaceNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (afterSlash) {
|
|
||||||
spaceNode.insertAfter($createTextNode(afterSlash));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cursor after space
|
|
||||||
const selection = $createRangeSelection();
|
|
||||||
selection.anchor.set(spaceNode.getKey(), 1, 'text');
|
|
||||||
selection.focus.set(spaceNode.getKey(), 1, 'text');
|
|
||||||
$setSelection(selection);
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
// Set cursor after space
|
||||||
|
const newSelection = $createRangeSelection();
|
||||||
|
newSelection.anchor.set(spaceNode.getKey(), 1, 'text');
|
||||||
|
newSelection.focus.set(spaceNode.getKey(), 1, 'text');
|
||||||
|
$setSelection(newSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTextLength += childText.length;
|
|
||||||
insertPosition = i + 1;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -237,6 +237,20 @@ const Properties: FC<PropertiesProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case 'knowledge-retrieval':
|
||||||
|
const knowledgeKey = `${nodeId}_message`;
|
||||||
|
if (!addedKeys.has(knowledgeKey)) {
|
||||||
|
addedKeys.add(knowledgeKey);
|
||||||
|
variableList.push({
|
||||||
|
key: knowledgeKey,
|
||||||
|
label: 'message',
|
||||||
|
type: 'variable',
|
||||||
|
dataType: 'String',
|
||||||
|
value: `${nodeId}.message`,
|
||||||
|
nodeData: nodeData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user