feat: modify shortcutsplugin

This commit is contained in:
yessenia 2025-08-22 16:13:54 +08:00
parent a9ea8cfd1c
commit 465e978209
2 changed files with 80 additions and 63 deletions

View File

@ -56,7 +56,7 @@ import { VariableValueBlockNode } from './plugins/variable-value-block/node'
import { CustomTextNode } from './plugins/custom-text/node' import { CustomTextNode } from './plugins/custom-text/node'
import OnBlurBlock from './plugins/on-blur-or-focus-block' import OnBlurBlock from './plugins/on-blur-or-focus-block'
import UpdateBlock from './plugins/update-block' import UpdateBlock from './plugins/update-block'
import ShortcutsPopupPlugin from './plugins/shortcuts-popup-plugin' import ShortcutsPopupPlugin, { type Hotkey } from './plugins/shortcuts-popup-plugin'
import { textToEditorState } from './utils' import { textToEditorState } from './utils'
import type { import type {
ContextBlockType, ContextBlockType,
@ -97,6 +97,7 @@ export type PromptEditorProps = {
workflowVariableBlock?: WorkflowVariableBlockType workflowVariableBlock?: WorkflowVariableBlockType
hitlInputBlock?: HITLInputBlockType hitlInputBlock?: HITLInputBlockType
isSupportFileVar?: boolean isSupportFileVar?: boolean
shortcutPopups?: Array<{ hotkey: Hotkey; Popup: React.ComponentType<{ onClose: () => void }> }>
} }
const PromptEditor: FC<PromptEditorProps> = ({ const PromptEditor: FC<PromptEditorProps> = ({
@ -121,6 +122,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
workflowVariableBlock, workflowVariableBlock,
hitlInputBlock, hitlInputBlock,
isSupportFileVar, isSupportFileVar,
shortcutPopups = [],
}) => { }) => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const initialConfig = { const initialConfig = {
@ -197,22 +199,11 @@ const PromptEditor: FC<PromptEditorProps> = ({
} }
ErrorBoundary={LexicalErrorBoundary} ErrorBoundary={LexicalErrorBoundary}
/> />
<ShortcutsPopupPlugin> {shortcutPopups?.map(({ hotkey, Popup }, idx) => (
{closePortal => ( <ShortcutsPopupPlugin key={idx} hotkey={hotkey} >
<div> {closePortal => <Popup onClose={closePortal} />}
<div>test content</div> </ShortcutsPopupPlugin>
<button ))}
className='rounded border border-text-secondary text-xs text-text-primary'
onMouseDown={(e) => {
e.preventDefault() // necessary, otherwise the editor will lose focus
closePortal()
}}
>
close
</button>
</div>
)}
</ShortcutsPopupPlugin>
<ComponentPickerBlock <ComponentPickerBlock
triggerString='/' triggerString='/'
contextBlock={contextBlock} contextBlock={contextBlock}

View File

@ -15,7 +15,12 @@ import cn from '@/utils/classnames'
export const SHORTCUTS_EMPTY_CONTENT = 'shortcuts_empty_content' export const SHORTCUTS_EMPTY_CONTENT = 'shortcuts_empty_content'
type Hotkey = string | ((e: KeyboardEvent) => boolean) // Hotkey can be:
// - string: 'mod+/'
// - string[]: ['mod', '/']
// - string[][]: [['mod', '/'], ['mod', 'shift', '/']] (any combo matches)
// - function: custom matcher
export type Hotkey = string | string[] | string[][] | ((e: KeyboardEvent) => boolean)
type ShortcutPopupPluginProps = { type ShortcutPopupPluginProps = {
hotkey?: Hotkey hotkey?: Hotkey
@ -48,58 +53,79 @@ function matchHotkey(event: KeyboardEvent, hotkey?: Hotkey) {
if (typeof hotkey === 'function') if (typeof hotkey === 'function')
return hotkey(event) return hotkey(event)
const parts = hotkey.toLowerCase().split('+').map(t => t.trim()).filter(Boolean) const matchCombo = (tokens: string[]) => {
let expectedKey: string | null = null const parts = tokens.map(t => t.toLowerCase().trim()).filter(Boolean)
let expectedKey: string | null = null
let needMod = false let needMod = false
let needCtrl = false let needCtrl = false
let needMeta = false let needMeta = false
let needAlt = false let needAlt = false
let needShift = false let needShift = false
for (const p of parts) { for (const p of parts) {
if (p === 'mod') { if (p === 'mod') {
needMod = true needMod = true
continue continue
}
if (CTRL_ALIASES.has(p)) {
needCtrl = true
continue
}
if (META_ALIASES.has(p)) {
needMeta = true
continue
}
if (ALT_ALIASES.has(p)) {
needAlt = true
continue
}
if (SHIFT_ALIASES.has(p)) {
needShift = true
continue
}
expectedKey = p
} }
if (CTRL_ALIASES.has(p)) {
needCtrl = true
continue
}
if (META_ALIASES.has(p)) {
needMeta = true
continue
}
if (ALT_ALIASES.has(p)) {
needAlt = true
continue
}
if (SHIFT_ALIASES.has(p)) {
needShift = true
continue
}
expectedKey = p
}
if (needMod && !(event.metaKey || event.ctrlKey)) if (needMod && !(event.metaKey || event.ctrlKey))
return false
if (needCtrl && !event.ctrlKey)
return false
if (needMeta && !event.metaKey)
return false
if (needAlt && !event.altKey)
return false
if (needShift && !event.shiftKey)
return false
if (expectedKey) {
const k = event.key.toLowerCase()
const normalized = k === ' ' ? 'space' : k
if (normalized !== expectedKey)
return false return false
if (needCtrl && !event.ctrlKey)
return false
if (needMeta && !event.metaKey)
return false
if (needAlt && !event.altKey)
return false
if (needShift && !event.shiftKey)
return false
if (expectedKey) {
const k = event.key.toLowerCase()
const normalized = k === ' ' ? 'space' : k
if (normalized !== expectedKey)
return false
}
return true
} }
return true if (Array.isArray(hotkey)) {
const isNested = hotkey.length > 0 && Array.isArray((hotkey as unknown[])[0])
if (isNested) {
const combos = hotkey as string[][]
return combos.some(tokens => matchCombo(tokens))
}
else {
const tokens = hotkey as string[]
return matchCombo(tokens)
}
}
const tokensFromString = hotkey
.toLowerCase()
.split('+')
.map(t => t.trim())
.filter(Boolean)
return matchCombo(tokensFromString)
} }
export default function ShortcutsPopupPlugin({ export default function ShortcutsPopupPlugin({