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:
@ -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"
|
||||
|
Reference in New Issue
Block a user