feat: singlecharacter shortcuts

This commit is contained in:
Andrew Bastin
2021-08-07 18:42:15 +05:30
parent 3d963a7719
commit 93dfed74f8
4 changed files with 94 additions and 38 deletions

View File

@@ -7,7 +7,7 @@
:title="LEFT_SIDEBAR ? $t('hide.sidebar') : $t('show.sidebar')" :title="LEFT_SIDEBAR ? $t('hide.sidebar') : $t('show.sidebar')"
icon="menu_open" icon="menu_open"
:class="{ 'transform rotate-180': !LEFT_SIDEBAR }" :class="{ 'transform rotate-180': !LEFT_SIDEBAR }"
@click.native="toggleSetting('LEFT_SIDEBAR')" @click.native="LEFT_SIDEBAR = !LEFT_SIDEBAR"
/> />
<ButtonSecondary <ButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -18,7 +18,7 @@
:class="{ :class="{
'!text-accent focus:text-accent hover:text-accent': ZEN_MODE, '!text-accent focus:text-accent hover:text-accent': ZEN_MODE,
}" }"
@click.native="toggleSetting('ZEN_MODE')" @click.native="ZEN_MODE = !ZEN_MODE"
/> />
</div> </div>
<div class="flex"> <div class="flex">
@@ -98,7 +98,7 @@
icon="menu_open" icon="menu_open"
class="transform rotate-180" class="transform rotate-180"
:class="{ 'rotate-0': !RIGHT_SIDEBAR }" :class="{ 'rotate-0': !RIGHT_SIDEBAR }"
@click.native="toggleSetting('RIGHT_SIDEBAR')" @click.native="RIGHT_SIDEBAR = !RIGHT_SIDEBAR"
/> />
</div> </div>
</div> </div>
@@ -107,47 +107,34 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import { defineComponent, ref } from "@nuxtjs/composition-api"
import { import { defineActionHandler } from "~/helpers/actions"
defaultSettings, import { useSetting } from "~/newstore/settings"
getSettingSubject,
applySetting,
toggleSetting,
} from "~/newstore/settings"
import type { KeysMatching } from "~/types/ts-utils"
type SettingsType = typeof defaultSettings
export default defineComponent({ export default defineComponent({
data() { setup() {
const showShortcuts = ref(false)
defineActionHandler("flyouts.keybinds.toggle", () => {
showShortcuts.value = !showShortcuts.value
})
return { return {
LEFT_SIDEBAR: null, LEFT_SIDEBAR: useSetting("LEFT_SIDEBAR"),
RIGHT_SIDEBAR: null, RIGHT_SIDEBAR: useSetting("RIGHT_SIDEBAR"),
ZEN_MODE: null, ZEN_MODE: useSetting("ZEN_MODE"),
showShortcuts: false,
navigatorShare: navigator.share, navigatorShare: !!navigator.share,
}
}, showShortcuts,
subscriptions() {
return {
LEFT_SIDEBAR: getSettingSubject("LEFT_SIDEBAR"),
RIGHT_SIDEBAR: getSettingSubject("RIGHT_SIDEBAR"),
ZEN_MODE: getSettingSubject("ZEN_MODE"),
} }
}, },
watch: { watch: {
ZEN_MODE(ZEN_MODE) { ZEN_MODE() {
this.applySetting("LEFT_SIDEBAR", !ZEN_MODE) this.LEFT_SIDEBAR = !this.ZEN_MODE
// this.applySetting("RIGHT_SIDEBAR", !ZEN_MODE)
}, },
}, },
methods: { methods: {
toggleSetting<K extends KeysMatching<SettingsType, boolean>>(key: K) {
toggleSetting(key)
},
applySetting<K extends keyof SettingsType>(key: K, value: SettingsType[K]) {
applySetting(key, value)
},
nativeShare() { nativeShare() {
if (navigator.share) { if (navigator.share) {
navigator navigator

View File

@@ -16,6 +16,7 @@ export type HoppAction =
| "request.method.post" // Select POST Method | "request.method.post" // Select POST Method
| "request.method.put" // Select PUT Method | "request.method.put" // Select PUT Method
| "request.method.delete" // Select DELETE Method | "request.method.delete" // Select DELETE Method
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
type BoundActionList = { type BoundActionList = {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars

View File

@@ -1,6 +1,7 @@
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api" import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
import { HoppAction, invokeAction } from "./actions" import { HoppAction, invokeAction } from "./actions"
import { isAppleDevice } from "./platformutils" import { isAppleDevice } from "./platformutils"
import { isDOMElement, isTypableElement } from "./utils/dom"
/** /**
* This variable keeps track whether keybindings are being accepted * This variable keeps track whether keybindings are being accepted
@@ -9,6 +10,13 @@ import { isAppleDevice } from "./platformutils"
*/ */
let keybindingsEnabled = true let keybindingsEnabled = true
/**
* This variable keeps track whether the currently focused element on the document
* is something that accepts a keyboard input
* (this is to prevent single character shortcuts from firing while typing)
*/
let focusNotTypable = true
/** /**
* Alt is also regarded as macOS OPTION (⌥) key * Alt is also regarded as macOS OPTION (⌥) key
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!) * Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
@@ -19,10 +27,14 @@ type ModifierKeys = "ctrl" | "alt"
type Key = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' type Key = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k'
| 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x'
| 'y' | 'z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'y' | 'z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
| "up" | "down" | "left" | "right" | "up" | "down" | "left" | "right" | "?"
/* eslint-enable */ /* eslint-enable */
type ShortcutKey = `${ModifierKeys}-${Key}` type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
// Singular keybindings (these will be disabled when an input-ish area has been focused)
type SingleCharacterShortcutKey = `${Key}`
type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
export const bindings: { export const bindings: {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@@ -39,6 +51,7 @@ export const bindings: {
"alt-p": "request.method.post", "alt-p": "request.method.post",
"alt-u": "request.method.put", "alt-u": "request.method.put",
"alt-x": "request.method.delete", "alt-x": "request.method.delete",
"?": "flyouts.keybinds.toggle",
} }
/** /**
@@ -49,13 +62,35 @@ export const bindings: {
export function hookKeybindingsListener() { export function hookKeybindingsListener() {
onMounted(() => { onMounted(() => {
document.addEventListener("keydown", handleKeyDown) document.addEventListener("keydown", handleKeyDown)
document.addEventListener("focusin", handleFocusUpdate)
document.addEventListener("focusout", handleFocusUpdate)
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener("keydown", handleKeyDown) document.removeEventListener("keydown", handleKeyDown)
document.removeEventListener("focusin", handleFocusUpdate)
document.removeEventListener("focusout", handleFocusUpdate)
}) })
} }
function handleFocusUpdate(ev: FocusEvent) {
const target = ev.target
if (isDOMElement(target) && isTypableElement(target)) {
if (focusNotTypable) {
console.log(
"Single Char keybindings are disabled because typable element is having focus"
)
focusNotTypable = false
}
} else if (!focusNotTypable) {
console.log(
"Single Char keybindings are restored because typable element is no longer focused"
)
focusNotTypable = true
}
}
function handleKeyDown(ev: KeyboardEvent) { function handleKeyDown(ev: KeyboardEvent) {
// Do not check keybinds if the mode is disabled // Do not check keybinds if the mode is disabled
if (!keybindingsEnabled) return if (!keybindingsEnabled) return
@@ -73,7 +108,18 @@ function handleKeyDown(ev: KeyboardEvent) {
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null { function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
// All our keybinds need to have one modifier pressed atleast // All our keybinds need to have one modifier pressed atleast
const modifierKey = getActiveModifier(ev) const modifierKey = getActiveModifier(ev)
if (!modifierKey) return null
const target = ev.target
console.log(target)
debugger
if (!modifierKey && !(isDOMElement(target) && isTypableElement(target))) {
// Check if we are having singulars instead
const key = getPressedKey(ev)
if (!key) return null
else return `${key}` as ShortcutKey
}
const key = getPressedKey(ev) const key = getPressedKey(ev)
if (!key) return null if (!key) return null
@@ -97,6 +143,9 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
// Check if number keys // Check if number keys
if (val.length === 1 && !isNaN(val as any)) return val as Key if (val.length === 1 && !isNaN(val as any)) return val as Key
// Check if question mark
if (val === "?") return "?"
// If no other cases match, this is not a valid key // If no other cases match, this is not a valid key
return null return null
} }

19
helpers/utils/dom.ts Normal file
View File

@@ -0,0 +1,19 @@
export function isDOMElement(el: any): el is HTMLElement {
return !!el && (el instanceof Element || el instanceof HTMLElement)
}
export function isTypableElement(el: HTMLElement): boolean {
// If content editable, then it is editable
if (el.isContentEditable) return true
// If element is an input and the input is enabled, then it is typable
if (el.tagName === "INPUT") {
return !(el as HTMLInputElement).disabled
}
// If element is a textarea and the input is enabled, then it is typable
if (el.tagName === "TEXTAREA") {
return !(el as HTMLTextAreaElement).disabled
}
return false
}