|
| 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> |
0 commit comments