fix(web): Editor input type add blur event
This commit is contained in:
@@ -1,13 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2025-12-23 12:29:46
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-03-03 10:12:48
|
||||||
|
*/
|
||||||
import { createCommand, type LexicalCommand } from 'lexical';
|
import { createCommand, type LexicalCommand } from 'lexical';
|
||||||
import type { Suggestion } from '../plugin/AutocompletePlugin';
|
import type { Suggestion } from '../plugin/AutocompletePlugin';
|
||||||
|
|
||||||
|
// Payload interface for inserting variable command
|
||||||
export interface InsertVariableCommandPayload {
|
export interface InsertVariableCommandPayload {
|
||||||
data: Suggestion;
|
data: Suggestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Command to insert a variable into the editor
|
||||||
export const INSERT_VARIABLE_COMMAND: LexicalCommand<InsertVariableCommandPayload> = createCommand('INSERT_VARIABLE_COMMAND');
|
export const INSERT_VARIABLE_COMMAND: LexicalCommand<InsertVariableCommandPayload> = createCommand('INSERT_VARIABLE_COMMAND');
|
||||||
|
|
||||||
|
// Command to clear all editor content
|
||||||
export const CLEAR_EDITOR_COMMAND: LexicalCommand<void> = createCommand('CLEAR_EDITOR_COMMAND');
|
export const CLEAR_EDITOR_COMMAND: LexicalCommand<void> = createCommand('CLEAR_EDITOR_COMMAND');
|
||||||
|
|
||||||
|
// Command to focus the editor
|
||||||
export const FOCUS_EDITOR_COMMAND: LexicalCommand<void> = createCommand('FOCUS_EDITOR_COMMAND');
|
export const FOCUS_EDITOR_COMMAND: LexicalCommand<void> = createCommand('FOCUS_EDITOR_COMMAND');
|
||||||
|
|
||||||
|
// Command to close the autocomplete dropdown
|
||||||
|
export const CLOSE_AUTOCOMPLETE_COMMAND: LexicalCommand<void> = createCommand('CLOSE_AUTOCOMPLETE_COMMAND');
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2025-12-23 16:22:51
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-03-03 10:11:48
|
||||||
|
*/
|
||||||
import { type FC, useState, useEffect, useMemo } from 'react';
|
import { type FC, useState, useEffect, useMemo } from 'react';
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
@@ -19,6 +25,7 @@ import LineNumberPlugin from './plugin/LineNumberPlugin';
|
|||||||
import BlurPlugin from './plugin/BlurPlugin';
|
import BlurPlugin from './plugin/BlurPlugin';
|
||||||
import { VariableNode } from './nodes/VariableNode'
|
import { VariableNode } from './nodes/VariableNode'
|
||||||
|
|
||||||
|
// Props interface for Lexical Editor component
|
||||||
export interface LexicalEditorProps {
|
export interface LexicalEditorProps {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
@@ -34,6 +41,7 @@ export interface LexicalEditorProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default theme for editor
|
||||||
const theme = {
|
const theme = {
|
||||||
paragraph: 'editor-paragraph',
|
paragraph: 'editor-paragraph',
|
||||||
text: {
|
text: {
|
||||||
@@ -42,6 +50,7 @@ const theme = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Theme with Jinja2 syntax highlighting
|
||||||
const jinja2Theme = {
|
const jinja2Theme = {
|
||||||
...theme,
|
...theme,
|
||||||
code: 'jinja2-expression',
|
code: 'jinja2-expression',
|
||||||
@@ -51,7 +60,8 @@ const jinja2Theme = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Editor: FC<LexicalEditorProps> =({
|
// Main Lexical Editor component
|
||||||
|
const Editor: FC<LexicalEditorProps> =(({
|
||||||
placeholder = "请输入内容...",
|
placeholder = "请输入内容...",
|
||||||
value = "",
|
value = "",
|
||||||
onChange,
|
onChange,
|
||||||
@@ -67,6 +77,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
const [enableJinja2, setEnableJinja2] = useState(false)
|
const [enableJinja2, setEnableJinja2] = useState(false)
|
||||||
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
const [enableLineNumbers, setEnableLineNumbers] = useState(false)
|
||||||
|
|
||||||
|
// Setup Jinja2 mode and inject styles when language changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const needsLineNumbers = language === 'jinja2';
|
const needsLineNumbers = language === 'jinja2';
|
||||||
setEnableJinja2(language === 'jinja2');
|
setEnableJinja2(language === 'jinja2');
|
||||||
@@ -139,11 +150,12 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
}
|
}
|
||||||
}, [language])
|
}, [language])
|
||||||
|
|
||||||
|
// Lexical editor configuration
|
||||||
const initialConfig = {
|
const initialConfig = {
|
||||||
namespace: 'AutocompleteEditor',
|
namespace: 'AutocompleteEditor',
|
||||||
theme: enableJinja2 ? jinja2Theme : theme,
|
theme: enableJinja2 ? jinja2Theme : theme,
|
||||||
nodes: enableJinja2 ? [
|
nodes: enableJinja2 ? [
|
||||||
// 当启用jinja2时,不使用VariableNode,使用普通文本
|
// When Jinja2 is enabled, use plain text instead of VariableNode
|
||||||
] : [
|
] : [
|
||||||
// HeadingNode,
|
// HeadingNode,
|
||||||
// QuoteNode,
|
// QuoteNode,
|
||||||
@@ -157,18 +169,26 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate minimum height based on type and size
|
||||||
const minheight = useMemo(() => {
|
const minheight = useMemo(() => {
|
||||||
if (type === 'input') {
|
if (type === 'input') {
|
||||||
return `${height ? height : size === 'small' ? 28 : 30}px`
|
return `${height ? height : size === 'small' ? 28 : 30}px`
|
||||||
}
|
}
|
||||||
return `${height ? height : size === 'small' ? 60 : 120}px`
|
return `${height ? height : size === 'small' ? 60 : 120}px`
|
||||||
}, [type, size, height])
|
}, [type, size, height])
|
||||||
|
|
||||||
|
// Calculate font size based on size prop
|
||||||
const fontSize = useMemo(() => {
|
const fontSize = useMemo(() => {
|
||||||
return `${size === 'small' ? 12 : 14}px`
|
return `${size === 'small' ? 12 : 14}px`
|
||||||
}, [size])
|
}, [size])
|
||||||
|
|
||||||
|
// Calculate line height based on size prop
|
||||||
const lineHeight = useMemo(() => {
|
const lineHeight = useMemo(() => {
|
||||||
return `${height ? height : size === 'small' ? 16 : 20}px`
|
return `${height ? height : size === 'small' ? 16 : 20}px`
|
||||||
}, [size])
|
}, [size])
|
||||||
|
|
||||||
|
// Calculate placeholder minimum height
|
||||||
const placeHolderMinheight = useMemo(() => {
|
const placeHolderMinheight = useMemo(() => {
|
||||||
return `${height ? height : size === 'small' ? 16 : 30}px`
|
return `${height ? height : size === 'small' ? 16 : 30}px`
|
||||||
}, [type, size, height])
|
}, [type, size, height])
|
||||||
@@ -179,6 +199,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
enableLineNumbers ? (
|
enableLineNumbers ? (
|
||||||
|
// Editor with line numbers for Jinja2 mode
|
||||||
<div className="editor-with-line-numbers" style={{
|
<div className="editor-with-line-numbers" style={{
|
||||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
@@ -203,6 +224,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
// Standard editor without line numbers
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
style={{
|
style={{
|
||||||
minHeight: minheight,
|
minHeight: minheight,
|
||||||
@@ -235,6 +257,7 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
}
|
}
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
/>
|
/>
|
||||||
|
{/* Editor plugins */}
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
{language === 'jinja2' && <Jinja2HighlightPlugin />}
|
{language === 'jinja2' && <Jinja2HighlightPlugin />}
|
||||||
@@ -242,10 +265,10 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
<AutocompletePlugin options={options} enableJinja2={enableJinja2} />
|
||||||
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
<CharacterCountPlugin setCount={(count) => { setCount(count) }} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} enableLineNumbers={enableLineNumbers} />
|
<InitialValuePlugin value={value} options={options} enableLineNumbers={enableLineNumbers} />
|
||||||
{enableJinja2 && <BlurPlugin />}
|
<BlurPlugin enableJinja2={enableJinja2} />
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default Editor;
|
export default Editor;
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2025-12-23 16:22:51
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-03-03 10:12:33
|
||||||
|
*/
|
||||||
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 { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical';
|
import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, KEY_ENTER_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical';
|
||||||
|
|
||||||
import { INSERT_VARIABLE_COMMAND } from '../commands';
|
import { INSERT_VARIABLE_COMMAND, CLOSE_AUTOCOMPLETE_COMMAND } from '../commands';
|
||||||
import type { NodeProperties } from '../../../types'
|
import type { NodeProperties } from '../../../types'
|
||||||
|
|
||||||
|
// Suggestion item interface for autocomplete dropdown
|
||||||
export interface Suggestion {
|
export interface Suggestion {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -13,16 +20,18 @@ export interface Suggestion {
|
|||||||
value: string;
|
value: string;
|
||||||
group?: string
|
group?: string
|
||||||
nodeData: NodeProperties;
|
nodeData: NodeProperties;
|
||||||
isContext?: boolean; // 标记是否为context变量
|
isContext?: boolean; // Flag for context variable
|
||||||
disabled?: boolean; // 标记是否禁用
|
disabled?: boolean; // Flag for disabled state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Autocomplete plugin for variable suggestions triggered by '/' character
|
||||||
const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => {
|
const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> = ({ options, enableJinja2 = false }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });
|
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });
|
||||||
|
|
||||||
|
// Listen to editor updates and show suggestions when '/' is typed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerUpdateListener(({ editorState }) => {
|
return editor.registerUpdateListener(({ editorState }) => {
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
@@ -49,6 +58,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate popup position to keep it within viewport bounds
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
const domSelection = window.getSelection();
|
const domSelection = window.getSelection();
|
||||||
if (domSelection && domSelection.rangeCount > 0) {
|
if (domSelection && domSelection.rangeCount > 0) {
|
||||||
@@ -84,9 +94,22 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
});
|
});
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
|
// Register command to close autocomplete popup
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerCommand(
|
||||||
|
CLOSE_AUTOCOMPLETE_COMMAND,
|
||||||
|
() => {
|
||||||
|
setShowSuggestions(false);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_HIGH
|
||||||
|
);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
// Insert selected suggestion into editor
|
||||||
const insertMention = (suggestion: Suggestion) => {
|
const insertMention = (suggestion: Suggestion) => {
|
||||||
if (enableJinja2) {
|
if (enableJinja2) {
|
||||||
// 在jinja2模式下,插入{{variable}}格式的文本
|
// In Jinja2 mode, insert {{variable}} format text
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
if ($isRangeSelection(selection)) {
|
if ($isRangeSelection(selection)) {
|
||||||
@@ -94,7 +117,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
const anchorOffset = selection.anchor.offset;
|
const anchorOffset = selection.anchor.offset;
|
||||||
const nodeText = anchorNode.getTextContent();
|
const nodeText = anchorNode.getTextContent();
|
||||||
|
|
||||||
// 移除触发字符'/'
|
// Remove trigger character '/'
|
||||||
const textBefore = nodeText.substring(0, anchorOffset - 1);
|
const textBefore = nodeText.substring(0, anchorOffset - 1);
|
||||||
const textAfter = nodeText.substring(anchorOffset);
|
const textAfter = nodeText.substring(anchorOffset);
|
||||||
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
|
const newText = textBefore + `{{${suggestion.value}}}` + textAfter;
|
||||||
@@ -103,19 +126,20 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
anchorNode.setTextContent(newText);
|
anchorNode.setTextContent(newText);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置光标位置到插入文本之后
|
// Set cursor position after inserted text
|
||||||
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
|
const newOffset = textBefore.length + `{{${suggestion.value}}}`.length;
|
||||||
selection.anchor.offset = newOffset;
|
selection.anchor.offset = newOffset;
|
||||||
selection.focus.offset = newOffset;
|
selection.focus.offset = newOffset;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 普通模式下使用VariableNode
|
// In normal mode, use VariableNode
|
||||||
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
|
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, { data: suggestion });
|
||||||
}
|
}
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Group suggestions by node ID
|
||||||
const groupedSuggestions = options.reduce((groups: Record<string, any[]>, suggestion) => {
|
const groupedSuggestions = options.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||||
const { nodeData } = suggestion
|
const { nodeData } = suggestion
|
||||||
const nodeId = nodeData.id as string;
|
const nodeId = nodeData.id as string;
|
||||||
@@ -126,6 +150,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
return groups;
|
return groups;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
// Handle Enter key to select suggestion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showSuggestions) return;
|
if (!showSuggestions) return;
|
||||||
|
|
||||||
@@ -148,11 +173,13 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
);
|
);
|
||||||
}, [showSuggestions, selectedIndex, groupedSuggestions, insertMention, editor]);
|
}, [showSuggestions, selectedIndex, groupedSuggestions, insertMention, editor]);
|
||||||
|
|
||||||
|
// Handle keyboard navigation (Arrow Up/Down, Escape)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showSuggestions) return;
|
if (!showSuggestions) return;
|
||||||
|
|
||||||
const allOptions = Object.values(groupedSuggestions).flat();
|
const allOptions = Object.values(groupedSuggestions).flat();
|
||||||
|
|
||||||
|
// Navigate down through suggestions, skip disabled items
|
||||||
const unregisterArrowDown = editor.registerCommand(
|
const unregisterArrowDown = editor.registerCommand(
|
||||||
KEY_ARROW_DOWN_COMMAND,
|
KEY_ARROW_DOWN_COMMAND,
|
||||||
(event) => {
|
(event) => {
|
||||||
@@ -172,6 +199,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
COMMAND_PRIORITY_HIGH
|
COMMAND_PRIORITY_HIGH
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Navigate up through suggestions, skip disabled items
|
||||||
const unregisterArrowUp = editor.registerCommand(
|
const unregisterArrowUp = editor.registerCommand(
|
||||||
KEY_ARROW_UP_COMMAND,
|
KEY_ARROW_UP_COMMAND,
|
||||||
(event) => {
|
(event) => {
|
||||||
@@ -191,6 +219,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
COMMAND_PRIORITY_HIGH
|
COMMAND_PRIORITY_HIGH
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Close suggestions on Escape key
|
||||||
const unregisterEscape = editor.registerCommand(
|
const unregisterEscape = editor.registerCommand(
|
||||||
KEY_ESCAPE_COMMAND,
|
KEY_ESCAPE_COMMAND,
|
||||||
(event) => {
|
(event) => {
|
||||||
@@ -239,7 +268,9 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }>
|
|||||||
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
|
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
|
||||||
return (
|
return (
|
||||||
<div key={nodeId}>
|
<div key={nodeId}>
|
||||||
|
{/* Divider between groups */}
|
||||||
{groupIndex > 0 && <div style={{ height: '1px', background: '#f0f0f0', margin: '4px 0' }} />}
|
{groupIndex > 0 && <div style={{ height: '1px', background: '#f0f0f0', margin: '4px 0' }} />}
|
||||||
|
{/* Group header with node name */}
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,39 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-01-20 10:42:13
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-03-03 10:12:10
|
||||||
|
*/
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { $setSelection } from 'lexical';
|
import { $setSelection } from 'lexical';
|
||||||
|
import { CLOSE_AUTOCOMPLETE_COMMAND } from '../commands';
|
||||||
|
|
||||||
export default function BlurPlugin() {
|
// Plugin to handle blur events and close autocomplete when clicking outside
|
||||||
|
export default function BlurPlugin({ enableJinja2 }: { enableJinja2: boolean }) {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Close autocomplete when clicking outside the popup
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (target?.closest('[data-autocomplete-popup="true"]')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.dispatchCommand(CLOSE_AUTOCOMPLETE_COMMAND, undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
|
||||||
return editor.registerRootListener((rootElement) => {
|
return editor.registerRootListener((rootElement) => {
|
||||||
if (rootElement) {
|
if (rootElement) {
|
||||||
const handleBlur = (e: FocusEvent) => {
|
const handleBlur = (e: FocusEvent) => {
|
||||||
// 检查是否点击了自动完成弹窗
|
if (enableJinja2) {
|
||||||
const target = e.target as HTMLElement;
|
// Check if autocomplete popup was clicked
|
||||||
console.log('target', target)
|
const target = e.target as HTMLElement;
|
||||||
if (target?.closest('[data-autocomplete-popup="true"]')) {
|
if (target?.closest('[data-autocomplete-popup="true"]')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否是粘贴操作导致的焦点变化
|
// Check if blur was caused by paste operation
|
||||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||||
if (!relatedTarget || relatedTarget === document.body) {
|
if (!relatedTarget || relatedTarget === document.body) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.update(() => {
|
// Clear selection on blur
|
||||||
$setSelection(null);
|
editor.update(() => {
|
||||||
});
|
$setSelection(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
rootElement.addEventListener('blur', handleBlur);
|
rootElement.addEventListener('blur', handleBlur);
|
||||||
return () => {
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
rootElement.removeEventListener('blur', handleBlur);
|
rootElement.removeEventListener('blur', handleBlur);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}, [editor]);
|
}, [editor, enableJinja2]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const EditableTable: FC<EditableTableProps> = ({
|
|||||||
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
||||||
const hasType = typeOptions.length > 0;
|
const hasType = typeOptions.length > 0;
|
||||||
const cellClassName="rb:p-1!"
|
const cellClassName="rb:p-1!"
|
||||||
const contentClassName ="rb:w-[108px]! rb:text-[12px]!"
|
const contentClassName ="rb:w-[108px]! rb:text-[12px]! rb:overflow-hidden!"
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user