feat(desktop): 完成编辑快捷键与警告框菜单
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveEditorShortcut } from './lifecycle'
|
||||
|
||||
const key = (code: string, options: Partial<Pick<KeyboardEvent, 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>> = {}) =>
|
||||
resolveEditorShortcut({ code, ctrlKey: false, metaKey: false, altKey: false, shiftKey: false, ...options })
|
||||
|
||||
describe('桌面编辑快捷键', () => {
|
||||
it('映射正文与六级标题', () => {
|
||||
expect(key('Digit0', { ctrlKey: true })).toEqual({ id: 'editor.paragraph' })
|
||||
expect(key('Digit6', { ctrlKey: true })).toEqual({ id: 'editor.heading', params: 6 })
|
||||
})
|
||||
|
||||
it('映射格式、列表与警告框', () => {
|
||||
expect(key('Digit5', { altKey: true, shiftKey: true })?.id).toBe('editor.strikethrough')
|
||||
expect(key('BracketRight', { ctrlKey: true, shiftKey: true })?.id).toBe('editor.bullet-list')
|
||||
expect(key('KeyC', { ctrlKey: true, altKey: true })).toMatchObject({ id: 'editor.callout', params: { type: 'note' } })
|
||||
expect(key('KeyK', { ctrlKey: true })?.id).toBe('editor.link')
|
||||
expect(key('KeyT', { ctrlKey: true, altKey: true })?.id).toBe('editor.table')
|
||||
})
|
||||
})
|
||||
@@ -6,24 +6,70 @@ import { executeEditorCommand, updateNativeEditorMenu } from '@/services/editorC
|
||||
import { watch } from 'vue'
|
||||
import { isDesktop } from './desktop'
|
||||
|
||||
type ShortcutCommand = { id: Parameters<typeof executeEditorCommand>[0]; params?: unknown }
|
||||
|
||||
/** 桌面菜单快捷键表;使用 code 避免数字键受键盘布局影响。 */
|
||||
export function resolveEditorShortcut(event: Pick<KeyboardEvent, 'code' | 'ctrlKey' | 'metaKey' | 'altKey' | 'shiftKey'>): ShortcutCommand | undefined {
|
||||
const mod = event.ctrlKey || event.metaKey
|
||||
if (mod && !event.altKey && !event.shiftKey && /^Digit[0-6]$/.test(event.code)) {
|
||||
const level = Number(event.code.at(-1))
|
||||
return level === 0 ? { id: 'editor.paragraph' } : { id: 'editor.heading', params: level }
|
||||
}
|
||||
const key = `${mod ? 'M' : ''}${event.altKey ? 'A' : ''}${event.shiftKey ? 'S' : ''}:${event.code}`
|
||||
const commands: Record<string, ShortcutCommand> = {
|
||||
'AS:Digit5': { id: 'editor.strikethrough' },
|
||||
'M:Backquote': { id: 'editor.inline-code' },
|
||||
'M:KeyK': { id: 'editor.link' },
|
||||
'MS:KeyK': { id: 'editor.code-block' },
|
||||
'MS:KeyM': { id: 'editor.math-block' },
|
||||
'MA:KeyC': { id: 'editor.callout', params: { type: 'note', body: '提示内容' } },
|
||||
'MA:KeyT': { id: 'editor.table' },
|
||||
'MS:KeyL': { id: 'editor.inline-math' },
|
||||
'MS:KeyH': { id: 'editor.horizontal-rule' },
|
||||
'MS:BracketLeft': { id: 'editor.ordered-list' },
|
||||
'MS:BracketRight': { id: 'editor.bullet-list' },
|
||||
'MS:KeyX': { id: 'editor.task-list' },
|
||||
'MS:KeyQ': { id: 'editor.blockquote' },
|
||||
}
|
||||
return commands[key]
|
||||
}
|
||||
|
||||
async function executeMetadataCommand() {
|
||||
const result = await executeEditorCommand('editor.import-note-properties')
|
||||
if (!result.ok) await executeEditorCommand('editor.metadata.edit')
|
||||
}
|
||||
|
||||
export async function installDesktopLifecycle() {
|
||||
if (!isDesktop()) return
|
||||
const activeEditor = useEditorStore()
|
||||
// 保存属于窗口级命令;编辑区原生处理撤销/重做,避免重复派发。
|
||||
window.addEventListener('keydown', event => {
|
||||
if (event.defaultPrevented || event.isComposing || event.repeat || event.altKey || event.shiftKey) return
|
||||
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return
|
||||
if (event.defaultPrevented || event.isComposing || event.repeat) return
|
||||
const target = event.target as HTMLElement | null
|
||||
if (target?.closest('input, textarea, select, dialog[open], [role="dialog"]')) return
|
||||
if ((event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 's') {
|
||||
if (!activeEditor.currentFilePath || ['saving', 'conflict', 'external_changed'].includes(activeEditor.saveStatus)) return
|
||||
event.preventDefault()
|
||||
void activeEditor.save()
|
||||
return
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.altKey && !event.shiftKey && event.code === 'KeyP') {
|
||||
event.preventDefault()
|
||||
void executeMetadataCommand()
|
||||
return
|
||||
}
|
||||
const shortcut = resolveEditorShortcut(event)
|
||||
if (!shortcut) return
|
||||
if (!activeEditor.currentFilePath || ['saving', 'conflict', 'external_changed'].includes(activeEditor.saveStatus)) return
|
||||
event.preventDefault()
|
||||
void activeEditor.save()
|
||||
void executeEditorCommand(shortcut.id, shortcut.params)
|
||||
})
|
||||
watch(() => [activeEditor.currentFilePath, activeEditor.saveStatus, activeEditor.mode], updateNativeEditorMenu, { flush: 'post' })
|
||||
await listen<string>('editor-command', event => {
|
||||
const focused = document.activeElement as HTMLElement | null
|
||||
if (focused?.closest('input, textarea, select, dialog[open], [role="dialog"]')) return
|
||||
void executeEditorCommand(event.payload)
|
||||
if (event.payload === 'editor.import-note-properties') void executeMetadataCommand()
|
||||
else void executeEditorCommand(event.payload)
|
||||
})
|
||||
let closing = false
|
||||
await listen('host-close-requested', async () => {
|
||||
|
||||
Reference in New Issue
Block a user