fix: add ShortcutTooltip component and integrate it for keyboard shortcuts in CalendarEventPanel and Calendar pages
This commit is contained in:
parent
f2ce3165dd
commit
346643bc6d
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@ -254,6 +254,7 @@ declare module 'vue' {
|
|||||||
SettingsIcon: typeof import('./src/components/Icons/SettingsIcon.vue')['default']
|
SettingsIcon: typeof import('./src/components/Icons/SettingsIcon.vue')['default']
|
||||||
SettingsIcon2: typeof import('./src/components/Icons/SettingsIcon2.vue')['default']
|
SettingsIcon2: typeof import('./src/components/Icons/SettingsIcon2.vue')['default']
|
||||||
SettingsPage: typeof import('./src/components/Settings/SettingsPage.vue')['default']
|
SettingsPage: typeof import('./src/components/Settings/SettingsPage.vue')['default']
|
||||||
|
ShortcutTooltip: typeof import('./src/components/ShortcutTooltip.vue')['default']
|
||||||
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
|
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
|
||||||
SidePanelLayout: typeof import('./src/components/SidePanelLayout.vue')['default']
|
SidePanelLayout: typeof import('./src/components/SidePanelLayout.vue')['default']
|
||||||
SidePanelLayoutEditor: typeof import('./src/components/SidePanelLayoutEditor.vue')['default']
|
SidePanelLayoutEditor: typeof import('./src/components/SidePanelLayoutEditor.vue')['default']
|
||||||
|
|||||||
@ -13,33 +13,31 @@
|
|||||||
{{ __(title) }}
|
{{ __(title) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-x-1">
|
<div class="flex items-center gap-x-1">
|
||||||
<Button
|
<ShortcutTooltip
|
||||||
v-if="mode == 'details'"
|
v-if="mode == 'details'"
|
||||||
:icon="EditIcon"
|
:label="__('Edit event')"
|
||||||
variant="ghost"
|
combo="Enter"
|
||||||
:tooltip="__('Edit event')"
|
>
|
||||||
@click="editDetails"
|
<Button :icon="EditIcon" variant="ghost" @click="editDetails" />
|
||||||
/>
|
</ShortcutTooltip>
|
||||||
<Button
|
<ShortcutTooltip
|
||||||
v-if="mode === 'edit' || mode === 'details'"
|
v-if="mode === 'edit' || mode === 'details'"
|
||||||
icon="trash-2"
|
:label="__('Delete event')"
|
||||||
variant="ghost"
|
combo="Delete"
|
||||||
:tooltip="__('Delete event')"
|
:alt-combos="['Backspace']"
|
||||||
@click="deleteEvent"
|
>
|
||||||
/>
|
<Button icon="trash-2" variant="ghost" @click="deleteEvent" />
|
||||||
<Button
|
</ShortcutTooltip>
|
||||||
|
<ShortcutTooltip
|
||||||
v-if="mode === 'edit' || mode === 'details'"
|
v-if="mode === 'edit' || mode === 'details'"
|
||||||
icon="copy"
|
:label="__('Duplicate event')"
|
||||||
variant="ghost"
|
combo="Mod+D"
|
||||||
:tooltip="__('Duplicate event')"
|
>
|
||||||
@click="duplicateEvent"
|
<Button icon="copy" variant="ghost" @click="duplicateEvent" />
|
||||||
/>
|
</ShortcutTooltip>
|
||||||
<Button
|
<ShortcutTooltip :label="__('Close panel')" combo="Esc">
|
||||||
icon="x"
|
<Button icon="x" variant="ghost" @click="close" />
|
||||||
variant="ghost"
|
</ShortcutTooltip>
|
||||||
:tooltip="__('Close panel')"
|
|
||||||
@click="close"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -377,6 +375,7 @@ import {
|
|||||||
CalendarActiveEvent as activeEvent,
|
CalendarActiveEvent as activeEvent,
|
||||||
createDocumentResource,
|
createDocumentResource,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
|
import ShortcutTooltip from '@/components/ShortcutTooltip.vue'
|
||||||
import { ref, computed, watch, h } from 'vue'
|
import { ref, computed, watch, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
|||||||
104
frontend/src/components/ShortcutTooltip.vue
Normal file
104
frontend/src/components/ShortcutTooltip.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<Tooltip v-if="!disabled">
|
||||||
|
<template #body>
|
||||||
|
<div
|
||||||
|
class="rounded bg-surface-gray-7 py-1.5 px-2 text-xs text-ink-white shadow-xl"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<!-- Primary combos (one or many) -->
|
||||||
|
<template
|
||||||
|
v-for="(combo, idx) in primaryCombosDisplay"
|
||||||
|
:key="'prim-' + idx + combo"
|
||||||
|
>
|
||||||
|
<KeyboardShortcut
|
||||||
|
bg
|
||||||
|
class="!bg-surface-gray-5 !text-ink-gray-2 px-1"
|
||||||
|
:combo="combo"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- Alternate combos / equivalents -->
|
||||||
|
<template
|
||||||
|
v-for="(alt, idx) in altCombosDisplay"
|
||||||
|
:key="'alt-' + idx + alt"
|
||||||
|
>
|
||||||
|
<KeyboardShortcut
|
||||||
|
bg
|
||||||
|
class="!bg-surface-gray-5 !text-ink-gray-2 px-1"
|
||||||
|
:combo="alt"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<slot />
|
||||||
|
</Tooltip>
|
||||||
|
<slot v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Tooltip, KeyboardShortcut } from 'frappe-ui'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label: string
|
||||||
|
combo?: string | string[]
|
||||||
|
altCombos?: string[]
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
combo: '',
|
||||||
|
altCombos: () => [],
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isMac = computed(() => {
|
||||||
|
if (typeof navigator === 'undefined') return false
|
||||||
|
const platform =
|
||||||
|
(navigator as any).userAgentData?.platform || navigator.platform || ''
|
||||||
|
if (/Mac|iPod|iPhone|iPad/i.test(platform)) return true
|
||||||
|
return /Mac OS X|Macintosh|iPhone|iPad|iPod/i.test(navigator.userAgent || '')
|
||||||
|
})
|
||||||
|
|
||||||
|
function normalizeCombo(raw: string): string {
|
||||||
|
if (!raw) return ''
|
||||||
|
if (/^mod\+/i.test(raw)) {
|
||||||
|
const rest = raw.split('+').slice(1).join('+')
|
||||||
|
return (isMac.value ? 'Cmd' : 'Ctrl') + '+' + rest
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeList(list: string | string[]): string[] {
|
||||||
|
const arr = Array.isArray(list) ? list : list ? [list] : []
|
||||||
|
return arr.map(normalizeCombo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedupe Backspace/Delete (prefer Backspace) on macOS
|
||||||
|
function dedupeMacDeleteVariants(primary: string[], alts: string[]) {
|
||||||
|
if (!isMac.value) return { primary, alts }
|
||||||
|
const all = [...primary, ...alts]
|
||||||
|
if (all.includes('Delete') && all.includes('Backspace')) {
|
||||||
|
return {
|
||||||
|
primary: primary.filter((k) => k !== 'Delete'),
|
||||||
|
alts: alts.filter((k) => k !== 'Delete'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { primary, alts }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base normalized lists
|
||||||
|
const normalizedPrimary = computed(() => normalizeList(props.combo))
|
||||||
|
const normalizedAlt = computed(() => props.altCombos.map(normalizeCombo))
|
||||||
|
|
||||||
|
// Apply dedupe once to both arrays to avoid circular dependency
|
||||||
|
const deduped = computed(() =>
|
||||||
|
dedupeMacDeleteVariants(normalizedPrimary.value, normalizedAlt.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const primaryCombosDisplay = computed(() => deduped.value.primary)
|
||||||
|
const altCombosDisplay = computed(() => deduped.value.alts)
|
||||||
|
|
||||||
|
defineOptions({ name: 'ShortcutTooltip' })
|
||||||
|
</script>
|
||||||
@ -4,14 +4,16 @@
|
|||||||
<ViewBreadcrumbs routeName="Calendar" />
|
<ViewBreadcrumbs routeName="Calendar" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-header>
|
<template #right-header>
|
||||||
<Button
|
<ShortcutTooltip :label="__('Create event')" combo="Mod+E">
|
||||||
variant="solid"
|
<Button
|
||||||
:label="__('Create')"
|
variant="solid"
|
||||||
:disabled="isCreateDisabled"
|
:label="__('Create')"
|
||||||
@click="newEvent"
|
:disabled="isCreateDisabled"
|
||||||
>
|
@click="newEvent"
|
||||||
<template #prefix><FeatherIcon name="plus" class="h-4" /></template>
|
>
|
||||||
</Button>
|
<template #prefix><FeatherIcon name="plus" class="h-4" /></template>
|
||||||
|
</Button>
|
||||||
|
</ShortcutTooltip>
|
||||||
</template>
|
</template>
|
||||||
</LayoutHeader>
|
</LayoutHeader>
|
||||||
<div class="flex h-screen overflow-hidden">
|
<div class="flex h-screen overflow-hidden">
|
||||||
@ -129,6 +131,7 @@
|
|||||||
import CalendarEventPanel from '@/components/Calendar/CalendarEventPanel.vue'
|
import CalendarEventPanel from '@/components/Calendar/CalendarEventPanel.vue'
|
||||||
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
|
import ShortcutTooltip from '@/components/ShortcutTooltip.vue'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user