feat: add v-shortcut directive for keyboard shortcuts (#942)
Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/942 Reviewed-by: dpschen <dpschen@noreply.kolaente.de> Co-authored-by: konrad <k@knt.li> Co-committed-by: konrad <k@knt.li>
This commit is contained in:
parent
db605e0d21
commit
feea191ecf
18 changed files with 251 additions and 394 deletions
|
@ -17,6 +17,7 @@
|
|||
"browserslist:update": "npx browserslist@latest --update-db"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/hotkey": "^1.6.0",
|
||||
"@kyvg/vue3-notification": "2.3.4",
|
||||
"@sentry/tracing": "6.14.3",
|
||||
"@sentry/vue": "6.14.3",
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<template>
|
||||
<button
|
||||
type="button"
|
||||
@click="$store.commit('toggleMenu')"
|
||||
class="menu-show-button"
|
||||
@shortkey="() => $store.commit('toggleMenu')"
|
||||
v-shortkey="['ctrl', 'e']"
|
||||
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="$store.commit('toggleMenu')"
|
||||
class="menu-show-button"
|
||||
@shortkey="() => $store.commit('toggleMenu')"
|
||||
v-shortcut="'Control+e'"
|
||||
:title="$t('keyboardShortcuts.toggleMenu')"
|
||||
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed} from 'vue'
|
||||
import {computed} from 'vue'
|
||||
import {store} from '@/store'
|
||||
|
||||
const menuActive = computed(() => store.menuActive)
|
||||
|
@ -32,6 +33,7 @@ $size: $lineWidth + 1rem;
|
|||
position: relative;
|
||||
|
||||
$transformX: translateX(-50%);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
|
|
|
@ -31,8 +31,7 @@
|
|||
<a
|
||||
class="keyboard-shortcuts-button"
|
||||
@click="showKeyboardShortcuts()"
|
||||
@shortkey="showKeyboardShortcuts()"
|
||||
v-shortkey="['?']"
|
||||
v-shortcut="'?'"
|
||||
>
|
||||
<icon icon="keyboard"/>
|
||||
</a>
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
class="navbar main-theme is-fixed-top"
|
||||
role="navigation"
|
||||
>
|
||||
<router-link :to="{name: 'home'}" class="logo">
|
||||
<Logo width="164" height="48" />
|
||||
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
||||
<Logo width="164" height="48"/>
|
||||
</router-link>
|
||||
<MenuButton class="menu-button" />
|
||||
<MenuButton class="menu-button"/>
|
||||
<div class="list-title" ref="listTitle" v-show="currentList.id">
|
||||
<template v-if="currentList.id">
|
||||
<h1
|
||||
|
@ -26,8 +26,8 @@
|
|||
<a
|
||||
@click="openQuickActions"
|
||||
class="trigger-button pr-0"
|
||||
@shortkey="openQuickActions"
|
||||
v-shortkey="['ctrl', 'k']"
|
||||
v-shortcut="'Control+k'"
|
||||
:title="$t('keyboardShortcuts.quickSearch')"
|
||||
>
|
||||
<icon icon="search"/>
|
||||
</a>
|
||||
|
@ -256,33 +256,33 @@ $vikunja-nav-logo-full-width: 164px;
|
|||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
$edit-icon-width: 1rem;
|
||||
$edit-icon-width: 1rem;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
// We need a fixed width for overflowing ellipsis to work
|
||||
--nav-username-width: 0;
|
||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width} - #{$vikunja-nav-logo-full-width} - var(--nav-username-width));
|
||||
}
|
||||
@media screen and (min-width: $tablet) {
|
||||
// We need a fixed width for overflowing ellipsis to work
|
||||
--nav-username-width: 0;
|
||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width} - #{$vikunja-nav-logo-full-width} - var(--nav-username-width));
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
// We need a fixed width for overflowing ellipsis to work
|
||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width});
|
||||
}
|
||||
@media screen and (max-width: $tablet) {
|
||||
// We need a fixed width for overflowing ellipsis to work
|
||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width});
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.dropdown-trigger) {
|
||||
color: $grey-400;
|
||||
margin-left: 1rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
:deep(.dropdown-trigger) {
|
||||
color: $grey-400;
|
||||
margin-left: 1rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,72 +0,0 @@
|
|||
<template>
|
||||
<modal @close="close()">
|
||||
<card class="has-no-shadow" :title="$t('keyboardShortcuts.title')">
|
||||
<div class="message is-primary">
|
||||
<div class="message-body">
|
||||
{{ $t('keyboardShortcuts.allPages') }}
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<strong>{{ $t('keyboardShortcuts.toggleMenu') }}</strong>
|
||||
<shortcut :keys="['ctrl', 'e']"/>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ $t('keyboardShortcuts.quickSearch') }}</strong>
|
||||
<shortcut :keys="['ctrl', 'k']"/>
|
||||
</p>
|
||||
<h3>{{ $t('list.kanban.title') }}</h3>
|
||||
<div class="message is-primary" v-if="$route.name === 'list.kanban'">
|
||||
<div class="message-body">
|
||||
{{ $t('keyboardShortcuts.currentPageOnly') }}
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<strong>{{ $t('keyboardShortcuts.task.done') }}</strong>
|
||||
<shortcut :keys="['ctrl', 'click']"/>
|
||||
</p>
|
||||
<h3>{{ $t('keyboardShortcuts.task.title') }}</h3>
|
||||
<div
|
||||
class="message is-primary"
|
||||
v-if="$route.name === 'task.detail' || $route.name === 'task.list.detail' || $route.name === 'task.gantt.detail' || $route.name === 'task.kanban.detail' || $route.name === 'task.detail'">
|
||||
<div class="message-body">
|
||||
{{ $t('keyboardShortcuts.currentPageOnly') }}
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<strong>{{ $t('keyboardShortcuts.task.assign') }}</strong>
|
||||
<shortcut :keys="['a']"/>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ $t('keyboardShortcuts.task.labels') }}</strong>
|
||||
<shortcut :keys="['l']"/>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ $t('keyboardShortcuts.task.dueDate') }}</strong>
|
||||
<shortcut :keys="['d']"/>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ $t('keyboardShortcuts.task.attachment') }}</strong>
|
||||
<shortcut :keys="['f']"/>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ $t('keyboardShortcuts.task.related') }}</strong>
|
||||
<shortcut :keys="['r']"/>
|
||||
</p>
|
||||
</card>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
|
||||
import Shortcut from '@/components/misc/shortcut.vue'
|
||||
|
||||
export default {
|
||||
name: 'keyboard-shortcuts',
|
||||
components: {Shortcut},
|
||||
methods: {
|
||||
close() {
|
||||
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
54
src/components/misc/keyboard-shortcuts/index.vue
Normal file
54
src/components/misc/keyboard-shortcuts/index.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<modal @close="close()">
|
||||
<card class="has-background-white has-no-shadow" :title="$t('keyboardShortcuts.title')">
|
||||
<template v-for="(s, i) in shortcuts" :key="i">
|
||||
<h3>{{ $t(s.title) }}</h3>
|
||||
|
||||
<div class="message is-primary">
|
||||
<div class="message-body">
|
||||
{{
|
||||
s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl>
|
||||
<template v-for="(sc, si) in s.shortcuts" :key="si">
|
||||
<dt>{{ $t(sc.title) }}</dt>
|
||||
<shortcut
|
||||
is="dd"
|
||||
:keys="sc.keys"
|
||||
:combination="typeof sc.combination !== 'undefined' ? $t(`keyboardShortcuts.${sc.combination}`) : null"/>
|
||||
</template>
|
||||
</dl>
|
||||
</template>
|
||||
</card>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
|
||||
import Shortcut from '@/components/misc/shortcut.vue'
|
||||
import {KEYBOARD_SHORTCUTS} from './shortcuts'
|
||||
|
||||
export default {
|
||||
name: 'keyboard-shortcuts',
|
||||
components: {Shortcut},
|
||||
data() {
|
||||
return {
|
||||
shortcuts: KEYBOARD_SHORTCUTS,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
88
src/components/misc/keyboard-shortcuts/shortcuts.js
Normal file
88
src/components/misc/keyboard-shortcuts/shortcuts.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||
|
||||
const ctrl = isAppleDevice() ? '⌘' : 'ctrl'
|
||||
|
||||
export const KEYBOARD_SHORTCUTS = [
|
||||
{
|
||||
title: 'keyboardShortcuts.general',
|
||||
available: () => null,
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.toggleMenu',
|
||||
keys: [ctrl, 'e'],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.quickSearch',
|
||||
keys: [ctrl, 'k'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'list.kanban.title',
|
||||
available: (route) => route.name === 'list.kanban',
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.task.done',
|
||||
keys: [ctrl, 'click'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.title',
|
||||
available: (route) => route.name.startsWith('list.'),
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToListView',
|
||||
keys: ['g', 'l'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToGanttView',
|
||||
keys: ['g', 'g'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToTableView',
|
||||
keys: ['g', 't'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.list.switchToKanbanView',
|
||||
keys: ['g', 'k'],
|
||||
combination: 'then',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.title',
|
||||
available: (route) => [
|
||||
'task.detail',
|
||||
'task.list.detail',
|
||||
'task.gantt.detail',
|
||||
'task.kanban.detail',
|
||||
'task.detail',
|
||||
].includes(route.name),
|
||||
shortcuts: [
|
||||
{
|
||||
title: 'keyboardShortcuts.task.assign',
|
||||
keys: ['a'],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.labels',
|
||||
keys: ['l'],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.dueDate',
|
||||
keys: ['d'],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.attachment',
|
||||
keys: ['f'],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.related',
|
||||
keys: ['r'],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<span class="shortcuts">
|
||||
<component :is="is" class="shortcuts">
|
||||
<template v-for="(k, i) in keys" :key="i">
|
||||
<kbd>{{ k }}</kbd>
|
||||
<span v-if="i < keys.length - 1">+</span>
|
||||
<span v-if="i < keys.length - 1">{{ combination }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -15,6 +15,14 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
combination: {
|
||||
type: String,
|
||||
default: '+',
|
||||
},
|
||||
is: {
|
||||
type: String,
|
||||
default: 'div',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -12,8 +12,7 @@
|
|||
class="modal-container"
|
||||
:class="{'has-overflow': overflow}"
|
||||
@click.self.prevent.stop="$emit('close')"
|
||||
@shortkey="$emit('close')"
|
||||
v-shortkey="['esc']"
|
||||
v-shortcut="'Escape'"
|
||||
>
|
||||
<div
|
||||
class="modal-content"
|
||||
|
|
17
src/directives/shortcut.ts
Normal file
17
src/directives/shortcut.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import {Directive} from 'vue'
|
||||
import {install, uninstall} from '@github/hotkey'
|
||||
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||
|
||||
const directive: Directive = {
|
||||
mounted(el, {value}) {
|
||||
if (isAppleDevice() && value.includes('Control')) {
|
||||
value = value.replace('Control', 'Meta')
|
||||
}
|
||||
install(el, value)
|
||||
},
|
||||
beforeUnmount(el) {
|
||||
uninstall(el)
|
||||
},
|
||||
}
|
||||
|
||||
export default directive
|
10
src/helpers/isAppleDevice.ts
Normal file
10
src/helpers/isAppleDevice.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const isAppleDevice = (): Boolean => {
|
||||
return navigator.userAgent.includes('Mac') || [
|
||||
'iPad Simulator',
|
||||
'iPhone Simulator',
|
||||
'iPod Simulator',
|
||||
'iPad',
|
||||
'iPhone',
|
||||
'iPod',
|
||||
].includes(navigator.platform)
|
||||
}
|
|
@ -757,10 +757,12 @@
|
|||
},
|
||||
"keyboardShortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"general": "General",
|
||||
"allPages": "These shortcuts work on all pages.",
|
||||
"currentPageOnly": "These shortcuts work only on the current page.",
|
||||
"toggleMenu": "Toggle The Menu",
|
||||
"quickSearch": "Open the search/quick action bar",
|
||||
"then": "then",
|
||||
"task": {
|
||||
"title": "Task Page",
|
||||
"done": "Mark a task as done",
|
||||
|
@ -769,6 +771,13 @@
|
|||
"dueDate": "Change the due date of this task",
|
||||
"attachment": "Add an attachment to this task",
|
||||
"related": "Modify related tasks of this task"
|
||||
},
|
||||
"list": {
|
||||
"title": "List Views",
|
||||
"switchToListView": "Switch to list view",
|
||||
"switchToGanttView": "Switch to gantt view",
|
||||
"switchToKanbanView": "Switch to kanban view",
|
||||
"switchToTableView": "Switch to table view"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
|
|
|
@ -31,8 +31,6 @@ import Notifications from '@kyvg/vue3-notification'
|
|||
// PWA
|
||||
import './registerServiceWorker'
|
||||
|
||||
// Shortcuts
|
||||
import shortkey from '@/plugins/shortkey'
|
||||
// Vuex
|
||||
import {store} from './store'
|
||||
// i18n
|
||||
|
@ -55,14 +53,14 @@ const app = createApp(App)
|
|||
|
||||
app.use(Notifications)
|
||||
|
||||
app.use(shortkey, {prevent: ['input', 'textarea', '.input', '[contenteditable]']})
|
||||
|
||||
// directives
|
||||
import focus from './directives/focus'
|
||||
import tooltip from './directives/tooltip'
|
||||
import shortcut from '@/directives/shortcut'
|
||||
|
||||
app.directive('focus', focus)
|
||||
app.directive('tooltip', tooltip)
|
||||
app.directive('shortcut', shortcut)
|
||||
|
||||
// global components
|
||||
import FontAwesomeIcon from './icons'
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
const MODIFIER_KEYS = ['shift', 'ctrl', 'meta', 'alt']
|
||||
|
||||
const SHORT_CUT_INDEX = [
|
||||
{ key: 'ArrowUp', value: 'arrowup' },
|
||||
{ key: 'ArrowLeft', value: 'arrowlef' },
|
||||
{ key: 'ArrowRight', value: 'arrowright' },
|
||||
{ key: 'ArrowDown', value: 'arrowdown' },
|
||||
{ key: 'AltGraph', value: 'altgraph' },
|
||||
{ key: 'Escape', value: 'esc' },
|
||||
{ key: 'Enter', value: 'enter' },
|
||||
{ key: 'Tab', value: 'tab' },
|
||||
{ key: ' ', value: 'space' },
|
||||
{ key: 'PageUp', value: 'pagup' },
|
||||
{ key: 'PageDown', value: 'pagedow' },
|
||||
{ key: 'Home', value: 'home' },
|
||||
{ key: 'End', value: 'end' },
|
||||
{ key: 'Delete', value: 'del' },
|
||||
{ key: 'Backspace', value: 'bacspace' },
|
||||
{ key: 'Insert', value: 'insert' },
|
||||
{ key: 'NumLock', value: 'numlock' },
|
||||
{ key: 'CapsLock', value: 'capslock' },
|
||||
{ key: 'Pause', value: 'pause' },
|
||||
{ key: 'ContextMenu', value: 'cotextmenu' },
|
||||
{ key: 'ScrollLock', value: 'scrolllock' },
|
||||
{ key: 'BrowserHome', value: 'browserhome' },
|
||||
{ key: 'MediaSelect', value: 'mediaselect' },
|
||||
]
|
||||
|
||||
export function encodeKey(pKey) {
|
||||
const shortKey = {}
|
||||
|
||||
MODIFIER_KEYS.forEach((key) => {
|
||||
shortKey[`${key}Key`] = pKey.includes(key)
|
||||
})
|
||||
|
||||
let indexedKeys = createShortcutIndex(shortKey)
|
||||
const vKey = pKey.filter(
|
||||
(item) => !MODIFIER_KEYS.includes(item),
|
||||
)
|
||||
indexedKeys += vKey.join('')
|
||||
return indexedKeys
|
||||
}
|
||||
|
||||
function createShortcutIndex(pKey) {
|
||||
let k = ''
|
||||
|
||||
MODIFIER_KEYS.forEach((key) => {
|
||||
if (pKey.key === capitalizeFirstLetter(key) || pKey[`${key}Key`]) {
|
||||
k += key
|
||||
}
|
||||
})
|
||||
|
||||
SHORT_CUT_INDEX.forEach(({ key, value }) => {
|
||||
if (pKey.key === key) {
|
||||
k += value
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
(pKey.key && pKey.key !== ' ' && pKey.key.length === 1) ||
|
||||
/F\d{1,2}|\//g.test(pKey.key)
|
||||
) {
|
||||
k += pKey.key.toLowerCase()
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
export { createShortcutIndex as decodeKey }
|
||||
|
||||
export function parseValue(value) {
|
||||
value = typeof value === 'string' ? JSON.parse(value.replace(/'/gi, '"')) : value
|
||||
|
||||
return value instanceof Array ? { '': value } : value
|
||||
}
|
|
@ -1,186 +0,0 @@
|
|||
import { parseValue, decodeKey, encodeKey } from './helpers'
|
||||
|
||||
let mapFunctions = {}
|
||||
let objAvoided = []
|
||||
let elementAvoided = []
|
||||
let keyPressed = false
|
||||
|
||||
function dispatchShortkeyEvent(pKey) {
|
||||
const e = new CustomEvent('shortkey', { bubbles: false })
|
||||
|
||||
if (mapFunctions[pKey].key) {
|
||||
e.srcKey = mapFunctions[pKey].key
|
||||
}
|
||||
|
||||
const elm = mapFunctions[pKey].el
|
||||
|
||||
if (!mapFunctions[pKey].propagte) {
|
||||
elm[elm.length - 1].dispatchEvent(e)
|
||||
} else {
|
||||
elm.forEach((elmItem) => elmItem.dispatchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
function keyDown(pKey) {
|
||||
if (
|
||||
(!mapFunctions[pKey].once && !mapFunctions[pKey].push) ||
|
||||
(mapFunctions[pKey].push && !keyPressed)
|
||||
) {
|
||||
dispatchShortkeyEvent(pKey)
|
||||
}
|
||||
}
|
||||
|
||||
function fillMappingFunctions(
|
||||
mappingFunctions,
|
||||
{ b, push, once, focus, propagte, el },
|
||||
) {
|
||||
for (let key in b) {
|
||||
const k = encodeKey(b[key])
|
||||
const propagated = mappingFunctions[k] && mappingFunctions[k].propagte
|
||||
const elm =
|
||||
mappingFunctions[k] && mappingFunctions[k].el
|
||||
? mappingFunctions[k].el
|
||||
: []
|
||||
|
||||
elm.push(el)
|
||||
|
||||
mappingFunctions[k] = {
|
||||
push,
|
||||
once,
|
||||
focus,
|
||||
key,
|
||||
propagte: propagated || propagte,
|
||||
el: elm,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bindValue(value, el, binding, vnode) {
|
||||
const { modifiers } = binding
|
||||
const push = !!modifiers.push
|
||||
const avoid = !!modifiers.avoid
|
||||
const focus = !modifiers.focus
|
||||
const once = !!modifiers.once
|
||||
const propagte = !!modifiers.propagte
|
||||
|
||||
if (avoid) {
|
||||
objAvoided = objAvoided.filter((itm) => !itm === el)
|
||||
objAvoided.push(el)
|
||||
} else {
|
||||
fillMappingFunctions(mapFunctions, {
|
||||
b: value,
|
||||
push,
|
||||
once,
|
||||
focus,
|
||||
propagte,
|
||||
el: vnode.el,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function unbindValue(value, el) {
|
||||
for (let key in value) {
|
||||
const k = encodeKey(value[key])
|
||||
const idxElm = mapFunctions[k].el.indexOf(el)
|
||||
|
||||
if (mapFunctions[k].el.length > 1 && idxElm > -1) {
|
||||
mapFunctions[k].el.splice(idxElm, 1)
|
||||
} else {
|
||||
delete mapFunctions[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function availableElement(decodedKey) {
|
||||
const objectIsAvoided = !!objAvoided.find(
|
||||
(r) => r === document.activeElement,
|
||||
)
|
||||
const filterAvoided = !!elementAvoided.find(
|
||||
(selector) =>
|
||||
document.activeElement && document.activeElement.matches(selector),
|
||||
)
|
||||
return !!mapFunctions[decodedKey] && !(objectIsAvoided || filterAvoided)
|
||||
}
|
||||
|
||||
function keyDownListener(pKey) {
|
||||
const decodedKey = decodeKey(pKey)
|
||||
|
||||
// Check avoidable elements
|
||||
if (!availableElement(decodedKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!mapFunctions[decodedKey].propagte) {
|
||||
pKey.preventDefault()
|
||||
pKey.stopPropagation()
|
||||
}
|
||||
|
||||
if (mapFunctions[decodedKey].focus) {
|
||||
keyDown(decodedKey)
|
||||
keyPressed = true
|
||||
} else if (!keyPressed) {
|
||||
const elm = mapFunctions[decodedKey].el
|
||||
elm[elm.length - 1].focus()
|
||||
keyPressed = true
|
||||
}
|
||||
}
|
||||
|
||||
function keyUpListener(pKey) {
|
||||
const decodedKey = decodeKey(pKey)
|
||||
|
||||
if (!availableElement(decodedKey)) {
|
||||
keyPressed = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!mapFunctions[decodedKey].propagte) {
|
||||
pKey.preventDefault()
|
||||
pKey.stopPropagation()
|
||||
}
|
||||
|
||||
if (mapFunctions[decodedKey].once || mapFunctions[decodedKey].push) {
|
||||
dispatchShortkeyEvent(decodedKey)
|
||||
}
|
||||
|
||||
keyPressed = false
|
||||
}
|
||||
|
||||
// register key presses that happen before mounting of directive
|
||||
// if (process?.env?.NODE_ENV !== 'test') {
|
||||
// (() => {
|
||||
document.addEventListener('keydown', keyDownListener, true)
|
||||
document.addEventListener('keyup', keyUpListener, true)
|
||||
// })()
|
||||
// }
|
||||
|
||||
function install(app, options) {
|
||||
elementAvoided = [...(options && options.prevent ? options.prevent : [])]
|
||||
|
||||
app.directive('shortkey', {
|
||||
beforeMount(el, binding, vnode) {
|
||||
// Mapping the commands
|
||||
const value = parseValue(binding.value)
|
||||
bindValue(value, el, binding, vnode)
|
||||
},
|
||||
|
||||
updated(el, binding, vnode) {
|
||||
const oldValue = parseValue(binding.oldValue)
|
||||
unbindValue(oldValue, el)
|
||||
|
||||
const newValue = parseValue(binding.value)
|
||||
bindValue(newValue, el, binding, vnode)
|
||||
},
|
||||
|
||||
unmounted(el, binding) {
|
||||
const value = parseValue(binding.value)
|
||||
unbindValue(value, el)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
install,
|
||||
encodeKey,
|
||||
decodeKey,
|
||||
keyDown,
|
||||
}
|
|
@ -6,21 +6,29 @@
|
|||
<div class="switch-view-container">
|
||||
<div class="switch-view">
|
||||
<router-link
|
||||
v-shortcut="'g l'"
|
||||
:title="$t('keyboardShortcuts.list.switchToListView')"
|
||||
:class="{'is-active': $route.name.includes('list.list')}"
|
||||
:to="{ name: 'list.list', params: { listId: listId } }">
|
||||
{{ $t('list.list.title') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-shortcut="'g g'"
|
||||
:title="$t('keyboardShortcuts.list.switchToGanttView')"
|
||||
:class="{'is-active': $route.name.includes('list.gantt')}"
|
||||
:to="{ name: 'list.gantt', params: { listId: listId } }">
|
||||
{{ $t('list.gantt.title') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-shortcut="'g t'"
|
||||
:title="$t('keyboardShortcuts.list.switchToTableView')"
|
||||
:class="{'is-active': $route.name.includes('list.table')}"
|
||||
:to="{ name: 'list.table', params: { listId: listId } }">
|
||||
{{ $t('list.table.title') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-shortcut="'g k'"
|
||||
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
|
||||
:class="{'is-active': $route.name.includes('list.kanban')}"
|
||||
:to="{ name: 'list.kanban', params: { listId: listId } }">
|
||||
{{ $t('list.kanban.title') }}
|
||||
|
|
|
@ -270,18 +270,16 @@
|
|||
/>
|
||||
<x-button
|
||||
@click="setFieldActive('assignees')"
|
||||
@shortkey="setFieldActive('assignees')"
|
||||
type="secondary"
|
||||
v-shortkey="['a']">
|
||||
v-shortcut="'a'">
|
||||
<span class="icon is-small"><icon icon="users"/></span>
|
||||
{{ $t('task.detail.actions.assign') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('labels')"
|
||||
@shortkey="setFieldActive('labels')"
|
||||
type="secondary"
|
||||
v-shortkey="['l']"
|
||||
icon="tags"
|
||||
v-shortcut="'l'"
|
||||
>
|
||||
{{ $t('task.detail.actions.label') }}
|
||||
</x-button>
|
||||
|
@ -294,10 +292,9 @@
|
|||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('dueDate')"
|
||||
@shortkey="setFieldActive('dueDate')"
|
||||
type="secondary"
|
||||
v-shortkey="['d']"
|
||||
icon="calendar"
|
||||
v-shortcut="'d'"
|
||||
>
|
||||
{{ $t('task.detail.actions.dueDate') }}
|
||||
</x-button>
|
||||
|
@ -338,19 +335,17 @@
|
|||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('attachments')"
|
||||
@shortkey="setFieldActive('attachments')"
|
||||
type="secondary"
|
||||
v-shortkey="['f']"
|
||||
icon="paperclip"
|
||||
v-shortcut="'f'"
|
||||
>
|
||||
{{ $t('task.detail.actions.attachments') }}
|
||||
</x-button>
|
||||
<x-button
|
||||
@click="setFieldActive('relatedTasks')"
|
||||
@shortkey="setFieldActive('relatedTasks')"
|
||||
type="secondary"
|
||||
v-shortkey="['r']"
|
||||
icon="sitemap"
|
||||
v-shortcut="'r'"
|
||||
>
|
||||
{{ $t('task.detail.actions.relatedTasks') }}
|
||||
</x-button>
|
||||
|
|
|
@ -1849,6 +1849,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.0-5.tgz#6251e6917198362fa56510eb256cfb6aa6d30a32"
|
||||
integrity sha512-aNmBT4bOecrFsZTog1l6AJDQHPP3ocXV+WQ3Ogy8WZCqstB/ahfhH4CPu5i4N9Hw0MBKXqE+LX+NbUxcj8cVTw==
|
||||
|
||||
"@github/hotkey@^1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@github/hotkey/-/hotkey-1.6.0.tgz#64da82a18ac11d24f9d5d61575a0a58ba101b2ab"
|
||||
integrity sha512-pm/xBWrn0yyD2GFqPUBH4ne7mdpdrnmdHxwKV0hN/jnSKj01RTPxau65SAvBvWD1Pf2VRv/OEE4H9ECORBHGdg==
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
|
||||
|
|
Loading…
Reference in a new issue