Skip to content

Commit 103e113

Browse files
authored
improve keybinding service (#710)
- **swap columns in sessionConfiguration** - **add mousetrapToolTip template wrapper component** - **add highlight features to wrapped element** - **improve highlight behaviours** - **add left, right position options** - **expand `clickable` element search** - **add navigation shortcuts** - **update SkMoustTrap service interface: add, remove** - **Mousetrap: remove destructive `bind` endpoint...** - **skip hotkey execution if user is in an input area** - **remove footer (future consideration)**
2 parents f3b03d9 + 9adbd29 commit 103e113

File tree

12 files changed

+631
-86
lines changed

12 files changed

+631
-86
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export interface SkMouseTrapToolTipProps {
2+
/**
3+
* The keyboard shortcut(s) to bind to this tooltip.
4+
* Can be a single string like "ctrl+s" or an array of strings.
5+
*/
6+
hotkey: string | string[];
7+
8+
/**
9+
* Descriptive name for the keyboard shortcut.
10+
* Will be displayed in the shortcut dialog.
11+
*/
12+
command: string;
13+
14+
/**
15+
* Whether the shortcut is currently disabled.
16+
* @default false
17+
*/
18+
disabled?: boolean;
19+
20+
/**
21+
* The position of the tooltip relative to the wrapped element.
22+
* @default 'top'
23+
*/
24+
position?: 'top' | 'bottom' | 'left' | 'right';
25+
26+
/**
27+
* Whether to show the tooltip when the Ctrl key is pressed.
28+
* @default true
29+
*/
30+
showTooltip?: boolean;
31+
32+
/**
33+
* The visual effect to apply to the wrapped element when Ctrl is pressed.
34+
* @default 'glow'
35+
*/
36+
highlightEffect?: 'glow' | 'scale' | 'border' | 'none';
37+
}
38+
39+
export interface SkMouseTrapToolTipEmits {
40+
(event: 'hotkey-triggered', hotkey: string | string[]): void;
41+
}
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
<template>
2+
<div
3+
class="sk-mousetrap-tooltip-wrapper"
4+
ref="wrapperElement"
5+
:class="[
6+
isControlKeyPressed && !disabled && highlightEffect !== 'none' ? `sk-mousetrap-highlight-${highlightEffect}` : '',
7+
]"
8+
>
9+
<slot></slot>
10+
<transition name="fade">
11+
<div
12+
v-if="showTooltip && isControlKeyPressed && !disabled"
13+
class="sk-mousetrap-tooltip"
14+
:class="{
15+
'sk-mt-tooltip-top': position === 'top',
16+
'sk-mt-tooltip-bottom': position === 'bottom',
17+
'sk-mt-tooltip-left': position === 'left',
18+
'sk-mt-tooltip-right': position === 'right',
19+
}"
20+
>
21+
{{ formattedHotkey }}
22+
</div>
23+
</transition>
24+
</div>
25+
</template>
26+
27+
<script lang="ts">
28+
import { defineComponent, PropType, ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
29+
import { SkldrMouseTrap, HotKeyMetaData } from '../utils/SkldrMouseTrap';
30+
31+
export default defineComponent({
32+
name: 'SkMouseTrapToolTip',
33+
34+
props: {
35+
hotkey: {
36+
type: [String, Array] as PropType<string | string[]>,
37+
required: true,
38+
},
39+
command: {
40+
type: String,
41+
required: true,
42+
},
43+
disabled: {
44+
type: Boolean,
45+
default: false,
46+
},
47+
position: {
48+
type: String as PropType<'top' | 'bottom' | 'left' | 'right'>,
49+
default: 'top',
50+
},
51+
showTooltip: {
52+
type: Boolean,
53+
default: true,
54+
},
55+
highlightEffect: {
56+
type: String as PropType<'glow' | 'scale' | 'border' | 'none'>,
57+
default: 'glow',
58+
},
59+
},
60+
61+
emits: ['hotkey-triggered'],
62+
63+
setup(props, { emit }) {
64+
const wrapperElement = ref<HTMLElement | null>(null);
65+
const isControlKeyPressed = ref(false);
66+
const hotkeyId = ref(`hotkey-${Math.random().toString(36).substring(2, 15)}`);
67+
68+
// Format hotkey for display
69+
const formattedHotkey = computed(() => {
70+
const hotkey = Array.isArray(props.hotkey) ? props.hotkey[0] : props.hotkey;
71+
// Check if this is a sequence (has spaces) or a combination (has +)
72+
if (hotkey.includes(' ')) {
73+
// For sequences like "g h", display as "g, h"
74+
return hotkey
75+
.toLowerCase()
76+
.split(' ')
77+
.map((part) => part.charAt(0) + part.slice(1))
78+
.join(', ');
79+
} else {
80+
// For combinations like "ctrl+s", display as "Ctrl + S"
81+
return hotkey
82+
.toLowerCase()
83+
.split('+')
84+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
85+
.join(' + ');
86+
}
87+
});
88+
89+
// Apply highlight effect to the actual button/control when Ctrl is pressed
90+
watch(
91+
() => isControlKeyPressed.value,
92+
(pressed) => {
93+
if (!wrapperElement.value || props.disabled) return;
94+
95+
const clickableElement = wrapperElement.value.querySelector(
96+
'button, a, input[type="button"], [role="button"]'
97+
) as HTMLElement;
98+
99+
if (clickableElement) {
100+
clickableElement.style.transition = 'all 250ms ease';
101+
102+
if (pressed && props.highlightEffect !== 'none') {
103+
// Add slight brightness increase to the inner element
104+
clickableElement.style.filter = 'brightness(1.1)';
105+
} else {
106+
clickableElement.style.filter = '';
107+
}
108+
}
109+
}
110+
);
111+
112+
// Handle Ctrl key detection
113+
const handleKeyDown = (e: KeyboardEvent) => {
114+
if (e.key === 'Control') {
115+
isControlKeyPressed.value = true;
116+
}
117+
};
118+
119+
const handleKeyUp = (e: KeyboardEvent) => {
120+
if (e.key === 'Control') {
121+
isControlKeyPressed.value = false;
122+
}
123+
};
124+
125+
// Handle clicking the element when hotkey is pressed
126+
const handleHotkeyPress = () => {
127+
if (props.disabled || !wrapperElement.value) return;
128+
129+
// Try finding a clickable element within our wrapper
130+
let clickableElement = wrapperElement.value.querySelector(
131+
'button, a, input[type="button"], [role="button"]'
132+
) as HTMLElement;
133+
134+
// If no standard clickable element found, try to find navigation or list items
135+
if (!clickableElement) {
136+
clickableElement = wrapperElement.value.querySelector(
137+
'v-list-item, .v-list-item, router-link, .router-link, a, [to]'
138+
) as HTMLElement;
139+
}
140+
141+
// If still no element found, try the wrapper itself - it might be clickable
142+
if (
143+
!clickableElement &&
144+
(wrapperElement.value.hasAttribute('to') ||
145+
wrapperElement.value.tagName === 'A' ||
146+
wrapperElement.value.classList.contains('v-list-item'))
147+
) {
148+
clickableElement = wrapperElement.value;
149+
}
150+
151+
// Get closest parent list item or router link if we found a title/content element
152+
if (!clickableElement) {
153+
const closestClickableParent = wrapperElement.value.closest('v-list-item, .v-list-item, a, [to]');
154+
if (closestClickableParent) {
155+
clickableElement = closestClickableParent as HTMLElement;
156+
}
157+
}
158+
159+
if (clickableElement) {
160+
if (clickableElement.hasAttribute('to')) {
161+
// Handle router-link style navigation
162+
const routePath = clickableElement.getAttribute('to');
163+
if (routePath && window.location.pathname !== routePath) {
164+
// Use parent router if available (Vue component)
165+
const router = (window as any).$nuxt?.$router || (window as any).$router;
166+
if (router && typeof router.push === 'function') {
167+
router.push(routePath);
168+
} else {
169+
// Fallback to regular navigation
170+
window.location.pathname = routePath;
171+
}
172+
}
173+
emit('hotkey-triggered', props.hotkey);
174+
} else {
175+
// Regular click for standard elements
176+
clickableElement.click();
177+
emit('hotkey-triggered', props.hotkey);
178+
}
179+
} else {
180+
// If no clickable element found, emit the event for parent handling
181+
console.log('No clickable element found for hotkey', props.hotkey);
182+
emit('hotkey-triggered', props.hotkey);
183+
}
184+
};
185+
186+
// Register/unregister the hotkey binding
187+
const registerHotkey = () => {
188+
if (!props.disabled) {
189+
SkldrMouseTrap.addBinding({
190+
hotkey: props.hotkey,
191+
command: props.command,
192+
callback: handleHotkeyPress,
193+
});
194+
}
195+
};
196+
197+
const unregisterHotkey = () => {
198+
if (!props.disabled) {
199+
SkldrMouseTrap.removeBinding(props.hotkey);
200+
}
201+
};
202+
203+
// Watch for changes to the disabled prop
204+
watch(
205+
() => props.disabled,
206+
(newValue) => {
207+
if (newValue) {
208+
unregisterHotkey();
209+
} else {
210+
registerHotkey();
211+
}
212+
}
213+
);
214+
215+
onMounted(() => {
216+
// Register global keyboard listeners for the Ctrl key
217+
document.addEventListener('keydown', handleKeyDown);
218+
document.addEventListener('keyup', handleKeyUp);
219+
220+
// Register the hotkey
221+
registerHotkey();
222+
});
223+
224+
onBeforeUnmount(() => {
225+
// Clean up event listeners
226+
document.removeEventListener('keydown', handleKeyDown);
227+
document.removeEventListener('keyup', handleKeyUp);
228+
229+
// Unregister the hotkey
230+
unregisterHotkey();
231+
});
232+
233+
return {
234+
wrapperElement,
235+
isControlKeyPressed,
236+
formattedHotkey,
237+
};
238+
},
239+
});
240+
</script>
241+
242+
<style scoped>
243+
.sk-mousetrap-tooltip-wrapper {
244+
display: inline-block;
245+
position: relative;
246+
}
247+
248+
.sk-mousetrap-tooltip {
249+
position: absolute;
250+
background-color: rgba(0, 0, 0, 0.8);
251+
color: white;
252+
padding: 2px 6px;
253+
border-radius: 4px;
254+
font-size: 12px;
255+
white-space: nowrap;
256+
pointer-events: none;
257+
z-index: 9999;
258+
}
259+
260+
.sk-mt-tooltip-top {
261+
bottom: 100%;
262+
margin-bottom: 5px;
263+
left: 50%;
264+
transform: translateX(-50%);
265+
}
266+
267+
.sk-mt-tooltip-bottom {
268+
top: 100%;
269+
margin-top: 5px;
270+
left: 50%;
271+
transform: translateX(-50%);
272+
}
273+
274+
.sk-mt-tooltip-left {
275+
right: 100%;
276+
margin-right: 5px;
277+
top: 50%;
278+
transform: translateY(-50%);
279+
}
280+
281+
.sk-mt-tooltip-right {
282+
left: 100%;
283+
margin-left: 5px;
284+
top: 50%;
285+
transform: translateY(-50%);
286+
}
287+
288+
/* Highlight effects when Ctrl is pressed */
289+
.sk-mousetrap-highlight-glow {
290+
box-shadow: 0 0 8px 2px rgba(25, 118, 210, 0.6);
291+
transition: box-shadow 250ms ease;
292+
}
293+
294+
.sk-mousetrap-highlight-scale {
295+
transform: scale(1.03);
296+
transition: transform 250ms ease;
297+
}
298+
299+
.sk-mousetrap-highlight-border {
300+
outline: 2px solid rgba(25, 118, 210, 0.8);
301+
outline-offset: 2px;
302+
border-radius: 4px;
303+
transition: outline 250ms ease, outline-offset 250ms ease;
304+
}
305+
306+
/* Fade transition */
307+
.fade-enter-active,
308+
.fade-leave-active {
309+
transition: opacity 250ms ease;
310+
}
311+
312+
.fade-enter-from,
313+
.fade-leave-to {
314+
opacity: 0;
315+
}
316+
</style>

packages/common-ui/src/components/cardRendering/AudioAutoPlayer.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,19 +91,24 @@ onMounted(() => {
9191
});
9292
}
9393
94-
SkldrMouseTrap.bind([
94+
// Define hotkeys
95+
const hotkeys = [
9596
{
9697
hotkey: 'up',
9798
callback: play,
9899
command: 'Replay Audio',
99100
},
100-
]);
101+
];
102+
103+
// Add bindings
104+
SkldrMouseTrap.addBinding(hotkeys);
101105
102106
play();
103107
});
104108
105109
onBeforeUnmount(() => {
106-
SkldrMouseTrap.reset();
110+
// Clean up hotkey bindings
111+
SkldrMouseTrap.removeBinding('up');
107112
stop();
108113
});
109114
</script>

0 commit comments

Comments
 (0)