mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-04-24 22:25:15 +00:00
feat: add shortcut to quickly copy task identifier, title and url to clipboard (#2028)
This adds the following shortcuts: - `.` to copy the task identifier - `..` to copy the task identifier and title - `...` to copy the task identifier, title, and url - `Control + .` to copy the task url
This commit is contained in:
@@ -209,6 +209,24 @@ export const KEYBOARD_SHORTCUTS: ShortcutGroup[] = [
|
||||
title: 'keyboardShortcuts.task.save',
|
||||
keys: [ctrl, 's'],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.copyIdentifier',
|
||||
keys: ['.'],
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.copyIdentifierAndTitle',
|
||||
keys: ['.', '.'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.copyIdentifierTitleAndUrl',
|
||||
keys: ['.', '.', '.'],
|
||||
combination: 'then',
|
||||
},
|
||||
{
|
||||
title: 'keyboardShortcuts.task.copyUrl',
|
||||
keys: [ctrl, '.'],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
100
frontend/src/composables/useTaskDetailShortcuts.ts
Normal file
100
frontend/src/composables/useTaskDetailShortcuts.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import {ref, onMounted, onBeforeUnmount} from 'vue'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
|
||||
import {getTaskIdentifier} from '@/models/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
interface UseTaskDetailShortcutsOptions {
|
||||
task: () => ITask
|
||||
taskTitle: () => string
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
async function copySavely(value: string) {
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
} catch(e) {
|
||||
console.error('could not write to clipboard', e)
|
||||
}
|
||||
}
|
||||
|
||||
export function useTaskDetailShortcuts({
|
||||
task,
|
||||
taskTitle,
|
||||
onSave,
|
||||
}: UseTaskDetailShortcutsOptions) {
|
||||
const dotKeyPressedTimes = ref(0)
|
||||
const dotKeyCopyValue = ref('')
|
||||
let dotKeyPressedTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function resetDotKeyPressed() {
|
||||
dotKeyPressedTimes.value = 0
|
||||
dotKeyCopyValue.value = ''
|
||||
if (dotKeyPressedTimeout !== null) {
|
||||
clearTimeout(dotKeyPressedTimeout)
|
||||
dotKeyPressedTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
|
||||
async function handleTaskHotkey(event: KeyboardEvent) {
|
||||
const hotkeyString = eventToHotkeyString(event)
|
||||
if (!hotkeyString) return
|
||||
|
||||
if (hotkeyString === 'Control+s' || hotkeyString === 'Meta+s') {
|
||||
event.preventDefault()
|
||||
onSave()
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
if (
|
||||
target.tagName.toLowerCase() === 'input' ||
|
||||
target.tagName.toLowerCase() === 'textarea' ||
|
||||
target.contentEditable === 'true'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hotkeyString === 'Control+.') {
|
||||
await copySavely(window.location.href)
|
||||
return
|
||||
}
|
||||
|
||||
if (hotkeyString === '.') {
|
||||
dotKeyPressedTimes.value++
|
||||
if (dotKeyPressedTimeout !== null) {
|
||||
clearTimeout(dotKeyPressedTimeout)
|
||||
}
|
||||
dotKeyPressedTimeout = setTimeout(async () => {
|
||||
await copySavely(dotKeyCopyValue.value)
|
||||
resetDotKeyPressed()
|
||||
}, 300)
|
||||
|
||||
switch (dotKeyPressedTimes.value) {
|
||||
case 1:
|
||||
dotKeyCopyValue.value = getTaskIdentifier(task())
|
||||
break
|
||||
case 2:
|
||||
dotKeyCopyValue.value += ' - ' + taskTitle()
|
||||
break
|
||||
case 3:
|
||||
dotKeyCopyValue.value += ' - ' + window.location.href
|
||||
break
|
||||
default:
|
||||
resetDotKeyPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleTaskHotkey)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', handleTaskHotkey)
|
||||
resetDotKeyPressed()
|
||||
})
|
||||
}
|
||||
@@ -1117,7 +1117,11 @@
|
||||
"priority": "Change the priority of this task",
|
||||
"favorite": "Mark this task as favorite / unfavorite",
|
||||
"openProject": "Open the project of this task",
|
||||
"save": "Save the current task"
|
||||
"save": "Save the current task",
|
||||
"copyIdentifier": "Copy task identifier to clipboard",
|
||||
"copyIdentifierAndTitle": "Copy task identifier and title to clipboard",
|
||||
"copyIdentifierTitleAndUrl": "Copy task identifier, title and URL to clipboard",
|
||||
"copyUrl": "Copy task URL to clipboard"
|
||||
},
|
||||
"project": {
|
||||
"title": "Project Views",
|
||||
|
||||
@@ -614,13 +614,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, reactive, shallowReactive, computed, watch, nextTick, onMounted, onBeforeUnmount} from 'vue'
|
||||
import {ref, reactive, shallowReactive, computed, watch, nextTick, onMounted} from 'vue'
|
||||
import {useRouter, useRoute, type RouteLocation, onBeforeRouteLeave} from 'vue-router'
|
||||
import {storeToRefs} from 'pinia'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {unrefElement, useDebounceFn, useElementSize, useIntersectionObserver, useMediaQuery, useMutationObserver} from '@vueuse/core'
|
||||
import {klona} from 'klona/lite'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel from '@/models/task'
|
||||
@@ -670,6 +669,7 @@ import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useTaskDetailShortcuts} from '@/composables/useTaskDetailShortcuts'
|
||||
|
||||
import {success} from '@/message'
|
||||
import type {Action as MessageAction} from '@/message'
|
||||
@@ -700,16 +700,6 @@ const taskNotFound = ref(false)
|
||||
const taskTitle = computed(() => task.value.title)
|
||||
useTitle(taskTitle)
|
||||
|
||||
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
|
||||
function saveTaskViaHotkey(event) {
|
||||
const hotkeyString = eventToHotkeyString(event)
|
||||
if (!hotkeyString) return
|
||||
if (hotkeyString !== 'Control+s' && hotkeyString !== 'Meta+s') return
|
||||
event.preventDefault()
|
||||
|
||||
saveTask()
|
||||
}
|
||||
|
||||
const lastProject = computed(() => {
|
||||
const backRoute = router.options.history.state?.back
|
||||
if (!backRoute || typeof backRoute !== 'string') {
|
||||
@@ -732,14 +722,6 @@ const lastProjectOrTaskProject = computed(() => lastProject.value ?? project.val
|
||||
// Use Alt+r on other platforms
|
||||
const reminderShortcut = computed(() => isAppleDevice() ? 'Shift+R' : 'Alt+r')
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', saveTaskViaHotkey)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', saveTaskViaHotkey)
|
||||
})
|
||||
|
||||
onBeforeRouteLeave(async () => {
|
||||
if (taskNotFound.value) {
|
||||
return
|
||||
@@ -1054,6 +1036,12 @@ async function saveTask(
|
||||
success({message: t('task.detail.updateSuccess')}, actions)
|
||||
}
|
||||
|
||||
useTaskDetailShortcuts({
|
||||
task: () => task.value,
|
||||
taskTitle: () => taskTitle.value,
|
||||
onSave: saveTask,
|
||||
})
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
|
||||
async function deleteTask() {
|
||||
|
||||
Reference in New Issue
Block a user