2025-05-01 15:07:35 +08:00

298 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { $generateNodesFromSerializedNodes } from '@lexical/clipboard'
import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
import { InitialEditorStateType } from '@lexical/react/LexicalComposer'
import { $getRoot, $insertNodes, LexicalEditor } from 'lexical'
import { Pencil, Search, Trash2 } from 'lucide-react'
import { Notice } from 'obsidian'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { TemplateContent } from '../../database/schema'
import { useCommands } from '../../hooks/use-commands'
import { t } from '../../lang/helpers'
import LexicalContentEditable from './chat-input/LexicalContentEditable'
export interface QuickCommand {
id: string
name: string
content: TemplateContent
contentText: string
createdAt: number
updatedAt: number
}
const CommandsView = (
{
selectedSerializedNodes
}: {
selectedSerializedNodes?: BaseSerializedNode[]
}
) => {
const {
createCommand,
deleteCommand,
updateCommand,
commandList,
} = useCommands()
// new command name
const [newCommandName, setNewCommandName] = useState('')
// search term
const [searchTerm, setSearchTerm] = useState('')
// editing command id
const [editingCommandId, setEditingCommandId] = useState<string | null>(null)
const nameInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
const contentEditorRefs = useRef<Map<string, LexicalEditor>>(new Map())
// create refs for each command
const commandEditRefs = useRef<Map<string, {
editorRef: React.RefObject<LexicalEditor>,
contentEditableRef: React.RefObject<HTMLDivElement>
}>>(new Map());
// get or create command edit refs
const getCommandEditRefs = useCallback((id: string) => {
if (!commandEditRefs.current.has(id)) {
commandEditRefs.current.set(id, {
editorRef: React.createRef<LexicalEditor>(),
contentEditableRef: React.createRef<HTMLDivElement>()
});
}
// 由于之前的if语句确保了值存在所以这里不会返回undefined
const refs = commandEditRefs.current.get(id);
if (!refs) {
// 添加保险逻辑创建一个新的refs对象
const newRefs = {
editorRef: React.createRef<LexicalEditor>(),
contentEditableRef: React.createRef<HTMLDivElement>()
};
commandEditRefs.current.set(id, newRefs);
return newRefs;
}
return refs;
}, []);
// update command edit refs when editing command id changes
useEffect(() => {
if (editingCommandId) {
const refs = getCommandEditRefs(editingCommandId);
if (refs.editorRef.current) {
contentEditorRefs.current.set(editingCommandId, refs.editorRef.current);
}
}
}, [editingCommandId, getCommandEditRefs]);
// new command content's editor state
const initialEditorState: InitialEditorStateType = (
editor: LexicalEditor,
) => {
if (!selectedSerializedNodes) return
editor.update(() => {
const parsedNodes = $generateNodesFromSerializedNodes(
selectedSerializedNodes,
)
$insertNodes(parsedNodes)
})
}
// new command content's editor
const editorRef = useRef<LexicalEditor>(null)
// new command content's editable
const contentEditableRef = useRef<HTMLDivElement>(null)
// Create new command
const handleAddCommand = async () => {
const serializedEditorState = editorRef.current.toJSON()
const nodes = serializedEditorState.editorState.root.children
if (nodes.length === 0) {
new Notice(String(t('command.errorContentRequired')))
return
}
if (newCommandName.trim().length === 0) {
new Notice(String(t('command.errorNameRequired')))
return
}
await createCommand(newCommandName, { nodes })
// clear editor content
editorRef.current.update(() => {
const root = $getRoot()
root.clear()
})
setNewCommandName('')
}
// delete command
const handleDeleteCommand = async (id: string) => {
await deleteCommand(id)
}
// edit command
const handleEditCommand = (command: QuickCommand) => {
setEditingCommandId(command.id)
}
// save edited command
const handleSaveEdit = async (id: string) => {
const nameInput = nameInputRefs.current.get(id)
const currContentEditorRef = contentEditorRefs.current.get(id)
if (!currContentEditorRef) {
new Notice(String(t('command.errorContentRequired')))
return
}
const serializedEditorState = currContentEditorRef.toJSON()
const nodes = serializedEditorState.editorState.root.children
if (nodes.length === 0) {
new Notice(String(t('command.errorContentRequired')))
return
}
await updateCommand(
id,
nameInput.value,
{ nodes },
)
setEditingCommandId(null)
}
// handle search
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value)
}
// filter commands list
const filteredCommands = useMemo(() => {
if (!searchTerm.trim()) {
return commandList;
}
return commandList.filter(
command =>
command.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
command.contentText.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [commandList, searchTerm]);
const getCommandEditorState = (commandContent: TemplateContent): InitialEditorStateType => {
return (editor: LexicalEditor) => {
editor.update(() => {
const parsedNodes = $generateNodesFromSerializedNodes(
commandContent.nodes,
)
$insertNodes(parsedNodes)
})
}
}
return (
<div className="infio-commands-container">
{/* header */}
<div className="infio-commands-header">
<div className="infio-commands-new">
<h2 className="infio-commands-header-title">{t('command.createQuickCommand')}</h2>
<div className="infio-commands-label">{t('command.name')}</div>
<input
type="text"
value={newCommandName}
onChange={(e) => setNewCommandName(e.target.value)}
className="infio-commands-input"
/>
<div className="infio-commands-label">{t('command.content')}</div>
<div className="infio-commands-textarea">
<LexicalContentEditable
initialEditorState={initialEditorState}
editorRef={editorRef}
contentEditableRef={contentEditableRef}
/>
</div>
<button
onClick={handleAddCommand}
className="infio-commands-add-btn"
disabled={!newCommandName.trim()}
>
<span>{t('command.createCommand')}</span>
</button>
</div>
</div>
{/* search bar */}
<div className="infio-commands-search">
<Search size={18} className="infio-commands-search-icon" />
<input
type="text"
placeholder={t('command.searchPlaceholder')}
value={searchTerm}
onChange={handleSearch}
className="infio-commands-search-input"
/>
</div>
{/* commands list */}
<div className="infio-commands-list">
{filteredCommands.length === 0 ? (
<div className="infio-commands-empty">
<p>{t('command.noCommandsFound')}</p>
</div>
) : (
filteredCommands.map(command => (
<div key={command.name} className="infio-commands-item">
{editingCommandId === command.id ? (
// edit mode
<div className="infio-commands-edit-mode">
<input
type="text"
defaultValue={command.name}
className="infio-commands-edit-name"
ref={(el) => {
if (el) nameInputRefs.current.set(command.id, el)
}}
/>
<div className="infio-commands-textarea">
<LexicalContentEditable
initialEditorState={getCommandEditorState(command.content)}
editorRef={getCommandEditRefs(command.id).editorRef}
contentEditableRef={getCommandEditRefs(command.id).contentEditableRef}
/>
</div>
<div className="infio-commands-actions">
<button
onClick={() => handleSaveEdit(command.id)}
className="infio-commands-add-btn"
>
<span>{t('command.updateCommand')}</span>
</button>
</div>
</div>
) : (
// view mode
<div className="infio-commands-view-mode">
<div className="infio-commands-name">{command.name}</div>
<div className="infio-commands-content">{command.contentText}</div>
<div className="infio-commands-actions">
<button
onClick={() => handleEditCommand(command)}
className="infio-commands-btn"
>
<Pencil size={16} />
</button>
<button
onClick={() => handleDeleteCommand(command.id)}
className="infio-commands-btn"
>
<Trash2 size={16} />
</button>
</div>
</div>
)}
</div>
))
)}
</div>
</div>
)
}
export default CommandsView