diff --git a/package.json b/package.json
index 5453bccd..933d6606 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/home/MenuButton.vue b/src/components/home/MenuButton.vue
index ec5caeea..14a2e469 100644
--- a/src/components/home/MenuButton.vue
+++ b/src/components/home/MenuButton.vue
@@ -1,16 +1,17 @@
-
\ No newline at end of file
diff --git a/src/components/misc/keyboard-shortcuts/index.vue b/src/components/misc/keyboard-shortcuts/index.vue
new file mode 100644
index 00000000..c46d209e
--- /dev/null
+++ b/src/components/misc/keyboard-shortcuts/index.vue
@@ -0,0 +1,54 @@
+
+
+
+
+ {{ $t(s.title) }}
+
+
+
+ {{
+ s.available($route) ? $t('keyboardShortcuts.currentPageOnly') : $t('keyboardShortcuts.allPages')
+ }}
+
+
+
+
+
+ - {{ $t(sc.title) }}
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/misc/keyboard-shortcuts/shortcuts.js b/src/components/misc/keyboard-shortcuts/shortcuts.js
new file mode 100644
index 00000000..bcc5014b
--- /dev/null
+++ b/src/components/misc/keyboard-shortcuts/shortcuts.js
@@ -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'],
+ },
+ ],
+ },
+]
diff --git a/src/components/misc/shortcut.vue b/src/components/misc/shortcut.vue
index 5d155db2..8c1809ea 100644
--- a/src/components/misc/shortcut.vue
+++ b/src/components/misc/shortcut.vue
@@ -1,10 +1,10 @@
-
+
{{ k }}
- +
+ {{ combination }}
-
+
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
index 0f05b219..76fcac45 100644
--- a/src/components/modal/modal.vue
+++ b/src/components/modal/modal.vue
@@ -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'"
>
{
+ return navigator.userAgent.includes('Mac') || [
+ 'iPad Simulator',
+ 'iPhone Simulator',
+ 'iPod Simulator',
+ 'iPad',
+ 'iPhone',
+ 'iPod',
+ ].includes(navigator.platform)
+}
diff --git a/src/i18n/lang/en.json b/src/i18n/lang/en.json
index 11319b16..9889189b 100644
--- a/src/i18n/lang/en.json
+++ b/src/i18n/lang/en.json
@@ -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": {
diff --git a/src/main.ts b/src/main.ts
index 62977e42..ec655d11 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -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'
diff --git a/src/plugins/shortkey/helpers.js b/src/plugins/shortkey/helpers.js
deleted file mode 100644
index 2baf329b..00000000
--- a/src/plugins/shortkey/helpers.js
+++ /dev/null
@@ -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
-}
diff --git a/src/plugins/shortkey/index.js b/src/plugins/shortkey/index.js
deleted file mode 100644
index 8755fd92..00000000
--- a/src/plugins/shortkey/index.js
+++ /dev/null
@@ -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,
-}
diff --git a/src/views/list/ShowList.vue b/src/views/list/ShowList.vue
index cca9b3d5..87306923 100644
--- a/src/views/list/ShowList.vue
+++ b/src/views/list/ShowList.vue
@@ -6,21 +6,29 @@
{{ $t('list.list.title') }}
{{ $t('list.gantt.title') }}
{{ $t('list.table.title') }}
{{ $t('list.kanban.title') }}
diff --git a/src/views/tasks/TaskDetailView.vue b/src/views/tasks/TaskDetailView.vue
index fc64d819..30f79e78 100644
--- a/src/views/tasks/TaskDetailView.vue
+++ b/src/views/tasks/TaskDetailView.vue
@@ -270,18 +270,16 @@
/>
+ v-shortcut="'a'">
{{ $t('task.detail.actions.assign') }}
{{ $t('task.detail.actions.label') }}
@@ -294,10 +292,9 @@
{{ $t('task.detail.actions.dueDate') }}
@@ -338,19 +335,17 @@
{{ $t('task.detail.actions.attachments') }}
{{ $t('task.detail.actions.relatedTasks') }}
diff --git a/yarn.lock b/yarn.lock
index f224420e..6e27243c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"