mirror of
https://github.com/purocean/yn.git
synced 2026-09-24 15:46:02 +08:00
适配 macOS 快捷键
This commit is contained in:
@@ -8,6 +8,7 @@ import Tabs from './Tabs.vue'
|
||||
import File from '../useful/file'
|
||||
import { defineComponent, onBeforeMount, onBeforeUnmount, ref, toRefs, watch } from 'vue'
|
||||
import { Components } from '../types'
|
||||
import { isAction } from '../useful/shortcut'
|
||||
|
||||
const blankUri = File.toUri(null)
|
||||
|
||||
@@ -72,14 +73,14 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// 快捷键切换最近文档
|
||||
if (e.altKey && e.ctrlKey) {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
const prev = findTab(-1)
|
||||
prev && switchTab(prev)
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
const next = findTab(1)
|
||||
next && switchTab(next)
|
||||
}
|
||||
if (isAction(e, 'file-tabs-switch-left')) {
|
||||
const prev = findTab(-1)
|
||||
prev && switchTab(prev)
|
||||
}
|
||||
|
||||
if (isAction(e, 'file-tabs-switch-right')) {
|
||||
const next = findTab(1)
|
||||
next && switchTab(next)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { useStore } from 'vuex'
|
||||
import { computed, defineComponent, onMounted, onUnmounted, ref, toRef } from 'vue'
|
||||
import { encodeMarkdownLink } from '../useful/utils'
|
||||
import { getCurrentAction } from '../useful/shortcut'
|
||||
import { useBus } from '../useful/bus'
|
||||
import XMask from './Mask.vue'
|
||||
import QuickOpen from './QuickOpen.vue'
|
||||
@@ -24,26 +25,29 @@ export default defineComponent({
|
||||
const withMarked = ref(true)
|
||||
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
if (e.key === 'i' && e.ctrlKey && e.altKey) {
|
||||
callback.value = (f: any) => {
|
||||
const file = currentFile.value
|
||||
if (file) {
|
||||
const relativePath = f.path.replace(file.path.substr(0, file.path.lastIndexOf('/')), '.')
|
||||
bus.emit('editor-insert-value', `[${f.name.replace(/\.[^.]+$/, '')}](${encodeMarkdownLink(relativePath)})`)
|
||||
switch (getCurrentAction(e)) {
|
||||
case 'insert-document':
|
||||
callback.value = (f: any) => {
|
||||
const file = currentFile.value
|
||||
if (file) {
|
||||
const relativePath = f.path.replace(file.path.substr(0, file.path.lastIndexOf('/')), '.')
|
||||
bus.emit('editor-insert-value', `[${f.name.replace(/\.[^.]+$/, '')}](${encodeMarkdownLink(relativePath)})`)
|
||||
}
|
||||
callback.value = null
|
||||
}
|
||||
callback.value = null
|
||||
}
|
||||
withMarked.value = false
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
} else if (e.key === 'p' && e.ctrlKey) {
|
||||
callback.value = (f: any) => {
|
||||
store.commit('setCurrentFile', f)
|
||||
callback.value = null
|
||||
}
|
||||
withMarked.value = true
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
withMarked.value = false
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
break
|
||||
case 'show-quick-open':
|
||||
callback.value = (f: any) => {
|
||||
store.commit('setCurrentFile', f)
|
||||
callback.value = null
|
||||
}
|
||||
withMarked.value = true
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import env from '../useful/env'
|
||||
|
||||
const models: {[key: string]: monaco.editor.ITextModel} = {}
|
||||
|
||||
const keys: {[key: string]: boolean} = {}
|
||||
let keys: {[key: string]: boolean} = {}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'monaco-editor',
|
||||
@@ -80,17 +80,17 @@ export default defineComponent({
|
||||
const KM = getMonaco().KeyMod
|
||||
const KC = getMonaco().KeyCode
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_F, addAttachment)
|
||||
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_F, addAttachment)
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_D, () => {
|
||||
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_D, () => {
|
||||
insert(dayjs().format('YYYY-MM-DD'))
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_T, () => {
|
||||
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_T, () => {
|
||||
insert(dayjs().format('HH:mm:ss'))
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_R, () => {
|
||||
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_R, () => {
|
||||
emit('xterm-run', getEditor().getModel()!!.getValueInRange(getEditor().getSelection()!!))
|
||||
})
|
||||
|
||||
@@ -156,7 +156,7 @@ export default defineComponent({
|
||||
function paste (e: ClipboardEvent) {
|
||||
if (getEditor().hasTextFocus()) {
|
||||
const items = e.clipboardData!!.items
|
||||
if (keys.m || keys.M) { // 粘贴 HTML 转为 markdown
|
||||
if (keys.d || keys.D) { // 粘贴 HTML 转为 markdown
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.match(/^text\/html$/i)) {
|
||||
items[i].getAsString(str => {
|
||||
@@ -172,7 +172,7 @@ export default defineComponent({
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const fileType = items[i].type
|
||||
if (fileType.match(/^image\/(png|jpg|jpeg|gif)$/i)) {
|
||||
const asBase64 = keys.b || keys.B // Ctrl + v 粘贴的同时 按下了 B 键,就粘贴 base64 图像
|
||||
const asBase64 = keys.b || keys.B // 粘贴的同时 按下了 B 键,就粘贴 base64 图像
|
||||
emit('paste-img', items[i].getAsFile(), asBase64)
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export default defineComponent({
|
||||
if (e.type === 'keydown') {
|
||||
keys[e.key] = true
|
||||
} else {
|
||||
keys[e.key] = false
|
||||
keys = {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ import 'highlight.js/styles/atom-one-dark.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { useToast } from '../useful/toast'
|
||||
import { hasCtrlCmd, isAction } from '../useful/shortcut'
|
||||
|
||||
HighlightLineNumber.addStyles()
|
||||
|
||||
const markdown = Markdown({
|
||||
@@ -311,7 +313,7 @@ export default defineComponent({
|
||||
|
||||
async function keydownHandler (e: KeyboardEvent) {
|
||||
// 转换所有外链图片到本地
|
||||
if (e.key === 'l' && e.ctrlKey && e.altKey) {
|
||||
if (isAction(e, 'transform-img-link')) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const result = []
|
||||
@@ -420,14 +422,19 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// 复制标题链接
|
||||
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && e.ctrlKey) {
|
||||
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && hasCtrlCmd(e)) {
|
||||
bus.emit('copy-text', encodeMarkdownLink(filePath.value) + '#' + encodeMarkdownLink(decodeURIComponent(target.id)))
|
||||
return preventEvent()
|
||||
}
|
||||
|
||||
// 复制内容
|
||||
if (target.classList.contains('copy-inner-text')) {
|
||||
bus.emit('copy-text', target.innerText)
|
||||
}
|
||||
|
||||
if (target.tagName === 'IMG') {
|
||||
const img = target as HTMLImageElement
|
||||
if (e.ctrlKey && e.shiftKey) { // 转换外链图片到本地
|
||||
if (isAction(e, 'transform-img-link-by-click')) { // 转换外链图片到本地
|
||||
const data = await transformImgOutLink(img)
|
||||
if (data) {
|
||||
bus.emit('tree-refresh')
|
||||
@@ -450,7 +457,7 @@ export default defineComponent({
|
||||
return
|
||||
}
|
||||
|
||||
if (target.tagName === 'TD' && target.classList.contains('yank-td') && e.ctrlKey) {
|
||||
if (target.tagName === 'TD' && target.classList.contains('yank-td') && hasCtrlCmd(e)) {
|
||||
const start = parseInt(target.dataset.sourceLine || '0')
|
||||
const end = parseInt(target.dataset.sourceLineEnd || '0')
|
||||
const td = target as HTMLTableDataCellElement
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
<span>字符数:{{documentInfo.textLength}}</span>
|
||||
<span v-if="documentInfo.selectedLength > 0">已选中:{{documentInfo.selectedLength}}</span>
|
||||
</div>
|
||||
<div class="action" @click="toggleSide" title="Alt + e">切换侧栏</div>
|
||||
<div class="action" @click="toggleWrap" title="Alt + w">切换换行</div>
|
||||
<div class="action" @click="toggleView" title="Alt + v">切换预览</div>
|
||||
<div class="action" @click="toggleXterm" title="Alt + o">切换终端</div>
|
||||
<div class="action" @click="toggleReadme" title="Alt + h">README</div>
|
||||
<div class="action" @click="toggleSide" :title="getActionLabel('toggle-side')">切换侧栏</div>
|
||||
<div class="action" @click="toggleWrap" :title="getActionLabel('toggle-wrap')">切换换行</div>
|
||||
<div class="action" @click="toggleView" :title="getActionLabel('toggle-view')">切换预览</div>
|
||||
<div class="action" @click="toggleXterm" :title="getActionLabel('toggle-xterm')">切换终端</div>
|
||||
<div class="action" @click="toggleReadme" :title="getActionLabel('toggle-readme')">README</div>
|
||||
<div class="action" @click="toggleFeature">特色功能说明</div>
|
||||
<div class="action" @click="toggleRender">{{autoPreview ? '同步渲染-已开启' : '同步渲染-已关闭'}}</div>
|
||||
<svg-icon v-if="!autoPreview" class="action action-icon" name="sync-alt-solid" @click="rerenderView" title="强制重新渲染" />
|
||||
@@ -27,6 +27,7 @@ import { useStore } from 'vuex'
|
||||
import { useBus } from '../useful/bus'
|
||||
import RepositorySwitch from './RepositorySwitch.vue'
|
||||
import SvgIcon from '../components/SvgIcon.vue'
|
||||
import { isAction, getActionLabel } from '../useful/shortcut'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'status-bar',
|
||||
@@ -69,31 +70,31 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
if (e.key === 'e' && e.altKey) {
|
||||
if (isAction(e, 'toggle-side')) {
|
||||
toggleSide()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (e.key === 'v' && e.altKey) {
|
||||
if (isAction(e, 'toggle-view')) {
|
||||
toggleView()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (e.key === 'o' && e.altKey) {
|
||||
if (isAction(e, 'toggle-xterm')) {
|
||||
toggleXterm()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (e.key === 'w' && e.altKey) {
|
||||
if (isAction(e, 'toggle-wrap')) {
|
||||
toggleWrap()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (e.key === 'h' && e.altKey) {
|
||||
if (isAction(e, 'toggle-readme')) {
|
||||
toggleReadme()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -119,6 +120,7 @@ export default defineComponent({
|
||||
toggleFeature,
|
||||
toggleRender,
|
||||
rerenderView,
|
||||
getActionLabel,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { getKeyLabel } from '@/useful/shortcut'
|
||||
import Markdown from 'markdown-it'
|
||||
|
||||
export default (md: Markdown) => {
|
||||
const Fun = (fn: Function) => (tokens: any, idx: any, options: any, env: any, slf: any) => {
|
||||
if (tokens[idx].attrIndex('title') < 0) {
|
||||
tokens[idx].attrPush(['title', 'Ctrl + 单击复制代码'])
|
||||
tokens[idx].attrPush(['onclick', "event.ctrlKey && window.globalBus.emit('copy-text', this.innerText)"])
|
||||
tokens[idx].attrJoin('class', 'copy-inner-text')
|
||||
tokens[idx].attrPush(['title', getKeyLabel('CtrlCmd') + ' + 单击复制'])
|
||||
}
|
||||
|
||||
return (fn)(tokens, idx, options, env, slf)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// https://github.com/Oktavilla/markdown-it-table-of-contents
|
||||
|
||||
import Markdown from 'markdown-it'
|
||||
import { getKeyLabel } from '@/useful/shortcut'
|
||||
|
||||
import { injectLineNumbers } from './SourceLinePlugin'
|
||||
|
||||
@@ -185,7 +186,7 @@ export default (md: Markdown, o: any) => {
|
||||
}
|
||||
|
||||
if (header.attrIndex('title') < 0) {
|
||||
header.attrSet('title', 'Ctrl + 单击复制链接')
|
||||
header.attrSet('title', getKeyLabel('CtrlCmd') + ' + 单击复制链接')
|
||||
}
|
||||
|
||||
return injectLineNumbers(tokens, idx, opt, env, slf)
|
||||
|
||||
@@ -28,7 +28,7 @@ const openAlwaysOnTopWindow = (url: string, target = '_blank') => {
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === '0') {
|
||||
webContents.setZoomFactor(1)
|
||||
} else if (e.key === '=') {
|
||||
@@ -40,7 +40,7 @@ const openAlwaysOnTopWindow = (url: string, target = '_blank') => {
|
||||
})
|
||||
|
||||
window.addEventListener('mousewheel', e => {
|
||||
if (e.ctrlKey) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
changeZoomFactor(e.deltaY < 0)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { getLogger } from './utils'
|
||||
|
||||
const logger = getLogger('shortcut')
|
||||
|
||||
const isMacOS = /macintosh|mac os x/i.test(navigator.userAgent)
|
||||
const Ctrl = 'Ctrl'
|
||||
const CtrlCmd = 'CtrlCmd'
|
||||
const Alt = 'Alt'
|
||||
const Shift = 'Shift'
|
||||
const LeftClick = 0
|
||||
|
||||
type XKey = typeof Ctrl | typeof CtrlCmd | typeof Alt | typeof Shift
|
||||
|
||||
const actions = {
|
||||
'toggle-side': [Alt, 'e'],
|
||||
'toggle-wrap': [Alt, 'w'],
|
||||
'toggle-view': [Alt, 'v'],
|
||||
'toggle-xterm': [Alt, 't'],
|
||||
'toggle-readme': [Alt, 'h'],
|
||||
'insert-document': [CtrlCmd, Alt, 'i'],
|
||||
'show-quick-open': [CtrlCmd, 'p'],
|
||||
'transform-img-link-by-click': [CtrlCmd, Shift, LeftClick],
|
||||
'transform-img-link': [CtrlCmd, Shift, 'l'],
|
||||
'file-tabs-switch-left': [Ctrl, Alt, 'ArrowLeft'],
|
||||
'file-tabs-switch-right': [Ctrl, Alt, 'ArrowRight'],
|
||||
}
|
||||
|
||||
type ActionName = keyof typeof actions
|
||||
|
||||
export const getKeyLabel = (key: XKey | string | number) => {
|
||||
switch (key) {
|
||||
case CtrlCmd:
|
||||
return isMacOS ? 'Cmd' : 'Ctrl'
|
||||
|
||||
case Alt:
|
||||
return isMacOS ? 'Option' : 'Ctrl'
|
||||
|
||||
default:
|
||||
return key.toString().toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
export const getActionLabel = (name: ActionName) => {
|
||||
const keys: any[] = actions[name]
|
||||
return keys.map(getKeyLabel).join(' + ')
|
||||
}
|
||||
|
||||
export const hasCtrlCmd = (e: KeyboardEvent | MouseEvent) => isMacOS ? e.metaKey : e.ctrlKey
|
||||
|
||||
export const matchKeys = (e: KeyboardEvent | MouseEvent, keys: (string | number)[]) => {
|
||||
for (const key of keys) {
|
||||
switch (key) {
|
||||
case CtrlCmd:
|
||||
if (!hasCtrlCmd(e)) return false
|
||||
break
|
||||
case Alt:
|
||||
if (!e.altKey) return false
|
||||
break
|
||||
case Ctrl:
|
||||
if (!e.ctrlKey) return false
|
||||
break
|
||||
case Shift:
|
||||
if (!e.shiftKey) return false
|
||||
break
|
||||
default:
|
||||
if (e instanceof KeyboardEvent) {
|
||||
if (key !== e.key && `Key${key.toString().toUpperCase()}` !== e.code) return false
|
||||
} else {
|
||||
if (key !== e.button) return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const getCurrentAction = (e: KeyboardEvent | MouseEvent) => {
|
||||
logger.debug('getCurrentAction', { e })
|
||||
return (Object.keys(actions) as ActionName[]).find(action => matchKeys(e, actions[action]))
|
||||
}
|
||||
|
||||
export const isAction = (e: KeyboardEvent | MouseEvent, name: ActionName) => {
|
||||
logger.debug('isAction', name, e)
|
||||
return matchKeys(e, actions[name])
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export default function startup () {
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === '0') {
|
||||
webContents.setZoomFactor(1)
|
||||
} else if (e.key === '=') {
|
||||
@@ -49,7 +49,7 @@ export default function startup () {
|
||||
window.addEventListener('mousewheel', event => {
|
||||
const e = event as WheelEvent
|
||||
|
||||
if (e.ctrlKey) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
changeZoomFactor(e.deltaY < 0)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -44,3 +44,18 @@ export const dataURItoBlobLink = (dataURI: string) => {
|
||||
const blob = new Blob([ab], { type: mimeString })
|
||||
return window.URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
export const getLogger = (subject: string) => {
|
||||
const logger = (level: string) => (...args: any) => {
|
||||
const time = `${new Date().toLocaleString()}.${Date.now() % 1000}`
|
||||
;(console as any)[level](`[${time}] ${subject} >`, ...args)
|
||||
}
|
||||
|
||||
return {
|
||||
debug: logger('debug'),
|
||||
log: logger('log'),
|
||||
info: logger('info'),
|
||||
warn: logger('warn'),
|
||||
error: logger('error')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user