Add functionality to add and delete user key bindings

This commit is contained in:
Jonatan Heyman 2025-04-17 10:49:04 +02:00
parent 08e0e9f7c3
commit c64033359b
10 changed files with 414 additions and 58 deletions

82
package-lock.json generated
View File

@ -55,6 +55,7 @@
"fs-jetpack": "^5.1.0",
"lezer-elixir": "^1.1.2",
"prettier": "^3.3.2",
"primevue": "^4.3.3",
"rollup-plugin-license": "^3.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4",
@ -1382,6 +1383,70 @@
"node": ">=18"
}
},
"node_modules/@primeuix/styled": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.5.1.tgz",
"integrity": "sha512-5Ftw/KSauDPClQ8F2qCyCUF7cIUEY4yLNikf0rKV7Vsb8zGYNK0dahQe7CChaR6M2Kn+NA2DSBSk76ZXqj6Uog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@primeuix/utils": "^0.5.3"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primeuix/styles": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-1.0.3.tgz",
"integrity": "sha512-yHj/Q+fosJ1736Ty5lRbpqhKa9piou+xZPPppNHUDshq0+XhrFwDGggvPGmDAJyUIM+ChM/Nj8lPY/AwTNXAkg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.5.1"
}
},
"node_modules/@primeuix/utils": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.5.3.tgz",
"integrity": "sha512-7SGh7734wcF1/uK6RzO6Z6CBjGQ97GDHfpyl2F1G/c7R0z9hkT/V72ypDo82AWcCS7Ta07oIjDpOCTkSVZuEGQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@primevue/core": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.3.3.tgz",
"integrity": "sha512-kSkN5oourG7eueoFPIqiNX3oDT/f0I5IRK3uOY/ytz+VzTZp5yuaCN0Nt42ZQpVXjDxMxDvUhIdaXVrjr58NhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.5.0",
"@primeuix/utils": "^0.5.1"
},
"engines": {
"node": ">=12.11.0"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@primevue/icons": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.3.3.tgz",
"integrity": "sha512-ouQaxHyeFB6MSfEGGbjaK5Qv9efS1xZGetZoU5jcPm090MSYLFtroP1CuK3lZZAQals06TZ6T6qcoNukSHpK5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@primeuix/utils": "^0.5.1",
"@primevue/core": "4.3.3"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/@replit/codemirror-lang-csharp": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.2.0.tgz",
@ -5329,6 +5394,23 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/primevue": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.3.3.tgz",
"integrity": "sha512-nooYVoEz5CdP3EhUkD6c3qTdRmpLHZh75fBynkUkl46K8y5rksHTjdSISiDijwTA5STQIOkyqLb+RM+HQ6nC1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@primeuix/styled": "^0.5.0",
"@primeuix/styles": "^1.0.0",
"@primeuix/utils": "^0.5.1",
"@primevue/core": "4.3.3",
"@primevue/icons": "4.3.3"
},
"engines": {
"node": ">=12.11.0"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",

View File

@ -70,6 +70,7 @@
"fs-jetpack": "^5.1.0",
"lezer-elixir": "^1.1.2",
"prettier": "^3.3.2",
"primevue": "^4.3.3",
"rollup-plugin-license": "^3.0.1",
"sass": "^1.57.1",
"typescript": "^4.9.4",

View File

@ -0,0 +1,195 @@
<script>
import fuzzysort from 'fuzzysort'
import AutoComplete from 'primevue/autocomplete'
import { HEYNOTE_COMMANDS } from '@/src/editor/commands'
export default {
name: "AddKeyBind",
components: {
AutoComplete,
},
data() {
return {
key: "",
command: "",
commandSuggestions: [],
}
},
computed: {
commands() {
return Object.entries(HEYNOTE_COMMANDS).map(([key, cmd]) => {
const description = cmd.category + ": " + cmd.description
return {
name: key,
category: cmd.category,
description: description,
key: cmd.description,
label: description,
}
})
},
},
mounted() {
window.addEventListener("keydown", this.onKeyDown)
},
beforeUnmount() {
window.removeEventListener("keydown", this.onKeyDown)
},
methods: {
onKeyDown(event) {
if (event.key === "Escape") {
this.$emit("close")
}
},
onCommandSearch(event) {
if (event.query === "") {
this.commandSuggestions = [...this.commands]
} else {
const searchResults = fuzzysort.go(event.query, this.commands, {
keys: ["description", "name"],
})
this.commandSuggestions = searchResults.map((result) => {
const obj = {...result.obj}
const nameHighlight = result[0].highlight("<b>", "</b>")
obj.label = nameHighlight !== "" ? nameHighlight : obj.description
return obj
})
}
},
onSave() {
this.$emit("save", {
key: this.key,
command: this.command.name,
})
}
},
}
</script>
<template>
<div class="container">
<div class="dialog">
<div class="dialog-content">
<h3>Add key binding</h3>
<div class="form">
<div class="field">
<label>Key</label>
<input
type="text"
v-model="key"
class="keys"
>
</div>
<div class="field">
<label>Command</label>
<AutoComplete
dropdown
forceSelection
v-model="command"
:suggestions="commandSuggestions"
optionLabel="key"
:delay="0"
@complete="onCommandSearch"
emptySearchMessage="No commands found"
class="command-autocomplete"
>
<template #option="slotProps">
<div class="command-option">
<span v-html="slotProps.option.label" />
</div>
</template>
</AutoComplete>
</div>
</div>
</div>
<div class="footer">
<button
@click="onSave"
class="save"
>Save</button>
<button
@click="$emit('close')"
class="cancel"
>Cancel</button>
</div>
</div>
</div>
</template>
<style lang="sass" scoped>
.container
position: absolute
top: 0
left: 0
right: 0
bottom: 0
background: rgba(255,255,255, 0.7)
+dark-mode
background: rgba(51,51,51, 0.7)
.dialog
width: 400px
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
display: flex
flex-direction: column
background: #fff
border-radius: 5px
box-shadow: 0 5px 30px rgba(0,0,0, 0.3)
border: 2px solid #c0c0c0
+dark-mode
background: #333
box-shadow: 0 5px 30px rgba(0,0,0, 0.7)
border: 2px solid #555
.dialog-content
flex-grow: 1
padding: 20px
h3
font-size: 14px
font-weight: 600
text-align: center
margin: 0
margin-bottom: 30px
.form
//display: flex
.field
//width: 50%
margin-bottom: 20px
label
display: block
margin-bottom: 8px
input.keys
width: 100%
padding: 4px
border-radius: 2px
border: 1px solid #ccc
&:focus
border: 1px solid rgba(0,0,0, 0)
outline: 2px solid var(--highlight-color)
//border: 1px solid var(--highlight-color)
+dark-mode
background: #202020
border: 1px solid #5a5a5a
&:focus
border: 1px solid rgba(0,0,0, 0)
.command-autocomplete
width: 100%
.footer
padding: 10px
background: #f1f1f1
border-radius: 0 0 5px 5px
text-align: right
+dark-mode
background: #2c2c2c
.cancel
float: left
</style>

View File

@ -42,9 +42,11 @@
<span class="command-name">{{ commandLabel }}</span>
</td>
<td class="actions">
<template v-if="!isDefault">
<button class="delete">Delete</button>
</template>
<button
v-if="!isDefault"
@click="$emit('delete')"
class="delete"
>Delete</button>
</td>
<td v-if="!isDefault" class="drag-handle"></td>
@ -80,14 +82,13 @@
background-color: rgba(0,0,0, 0.05)
+dark-mode
background-color: rgba(0,0,0, 0.25)
button
button.delete
padding: 0 10px
height: 22px
font-size: 12px
background: none
border: none
border-radius: 2px
margin-right: 2px
cursor: pointer
background: #ddd
&:hover
@ -96,13 +97,4 @@
background: #555
&:hover
background: #666
//&.delete
// background: #e95050
// &:hover
// background: #ce4848
// +dark-mode
// &.delete
// background: #ae1e1e
// &:hover
// background: #bf2222
</style>

View File

@ -5,6 +5,7 @@
import { DEFAULT_KEYMAP, EMACS_KEYMAP } from "@/src/editor/keymap"
import { useSettingsStore } from "@/src/stores/settings-store"
import KeyBindRow from "./KeyBindRow.vue"
import AddKeyBind from "./AddKeyBind.vue"
export default {
props: [
@ -12,43 +13,26 @@
"modelValue",
],
components: {
KeyBindRow,
draggable,
KeyBindRow,
AddKeyBind,
},
data() {
return {
keymap: this.modelValue,
//fixedKeymap: [],
addKeyBinding: false,
}
},
mounted() {
/*const defaultKeymap = this.settings.keymap === "emacs" ? EMACS_KEYMAP : DEFAULT_KEYMAP
this.fixedKeymap = defaultKeymap.map((km) => {
return {
key: km.key,
command: km.command,
isDefault: true,
}
})*/
/*
const keymap = this.settings.keymap === "emacs" ? EMACS_KEYMAP : DEFAULT_KEYMAP
this.testKeymap = [
...this.userKeys,
{"key": "Mod-Enter", command: "yay"},
{"key": "Mod-Enter n", command: "nay"},
{"key": "Ctrl-o", command: null},
]
this.fixedKeymap = keymap.map((km) => {
return {
key: km.key,
command: km.command,
isDefault: true,
isOverridden: km.key in this.userKeys,
}
})*/
},
watch: {
addKeyBinding(newValue) {
this.$emit("addKeyBindingDialogVisible", newValue)
},
},
computed: {
@ -76,7 +60,24 @@
methods: {
onDragEnd(event) {
console.log("onDragEnd", this.testKeymap)
this.$emit("update:modelValue", this.keymap)
},
onSaveKeyBinding(event) {
this.keymap = [
{
key: event.key,
command: event.command,
},
...this.keymap,
]
//console.log("keymap", this.keymap)
this.$emit("update:modelValue", this.keymap)
this.addKeyBinding = false
},
deleteKeyBinding(index) {
this.keymap = this.keymap.toSpliced(index, 1)
this.$emit("update:modelValue", this.keymap)
},
},
@ -85,8 +86,25 @@
<template>
<div class="container">
<div class="header" :inert="addKeyBinding">
<h2>Keyboard Bindings</h2>
<table>
<!--<p>User key bindings can be reordered. Bindings that appear first take precedence</p>-->
<div class="button-container">
<button
class="add-keybinding"
@click="addKeyBinding = !addKeyBinding"
>Add Keybinding</button>
</div>
</div>
<AddKeyBind
v-if="addKeyBinding"
@close="addKeyBinding = false"
@save="onSaveKeyBinding"
/>
<table :inert="addKeyBinding">
<thead>
<tr>
<th>Source</th>
<th>Key</th>
@ -94,6 +112,7 @@
<th></th>
<th></th>
</tr>
</thead>
<draggable
v-model="keymap"
tag="tbody"
@ -104,11 +123,13 @@
@end="onDragEnd"
item-key="key"
>
<template #item="{element}">
<template #item="{element, index}">
<KeyBindRow
:keys="element.key"
:command="element.command"
:isDefault="element.isDefault"
:index="index"
@delete="deleteKeyBinding(index)"
source="User"
/>
</template>
@ -128,10 +149,20 @@
</template>
<style lang="sass" scoped>
.header
display: flex
margin-bottom: 12px
h2
flex-grow: 1
font-weight: 600
margin-bottom: 20px
font-size: 14px
margin: 0
.button-container
.add-keybinding
font-size: 12px
height: 26px
cursor: pointer
table
width: 100%

View File

@ -67,6 +67,9 @@
defaultFontSize: defaultFontSize,
appVersion: "",
theme: this.themeSetting,
// tracks if the add key binding dialog is visible (so that we can set inert on the save button)
addKeyBindingDialogVisible: false,
}
},
@ -94,7 +97,7 @@
methods: {
onKeyDown(event) {
if (event.key === "Escape") {
if (event.key === "Escape" && !this.addKeyBindingDialogVisible) {
this.$emit("closeSettings")
}
},
@ -376,6 +379,7 @@
<KeyboardBindings
:userKeys="keyBindings ? keyBindings : {}"
v-model="keyBindings"
@addKeyBindingDialogVisible="addKeyBindingDialogVisible = $event"
/>
</TabContent>
@ -417,7 +421,7 @@
</div>
</div>
<div class="bottom-bar">
<div class="bottom-bar" :inert="addKeyBindingDialogVisible">
<button
@click="$emit('closeSettings')"
class="close"
@ -493,6 +497,7 @@
flex-grow: 1
padding: 40px
overflow-y: auto
position: relative
select
height: 22px
margin: 4px 0
@ -559,4 +564,5 @@
background: #222
.close
height: 28px
cursor: pointer
</style>

View File

@ -1,3 +1,4 @@
@import "reset"
@import "font"
@import "base"
@import "autocomplete"

44
src/css/autocomplete.sass Normal file
View File

@ -0,0 +1,44 @@
@import "./src/css/include.sass"
.p-component
--p-inputtext-background: #fff
--p-inputtext-border-color: #ccc
--p-inputtext-border-radius: 3px
--p-inputtext-padding-y: 4px
--p-inputtext-padding-x: 4px
--p-inputtext-focus-border-color: var(--p-inputtext-border-color)
--p-inputtext-hover-border-color: var(--p-inputtext-border-color)
--p-inputtext-active-border-color: var(--p-inputtext-border-color)
--p-autocomplete-dropdown-border-color: var(--p-inputtext-border-color)
--p-autocomplete-dropdown-border-radius: var(--p-inputtext-border-radius)
--p-autocomplete-dropdown-hover-border-color: var(--p-inputtext-hover-border-color)
--p-autocomplete-dropdown-active-border-color: var(--p-inputtext-active-border-color)
--p-autocomplete-option-focus-background: #f1f1f1
+dark-mode
--p-inputtext-background: #202020
--p-inputtext-border-color: #444
--p-autocomplete-option-focus-background: #555
--p-autocomplete-option-focus-color: #fff
.p-inputtext.p-inputtext
font-size: 12px
.p-autocomplete-list-container
background: #fff
border: 1px solid #f1f1f1
padding: 6px 0
border-radius: 3px
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1)
+dark-mode
color: rgba(255,255,255, 0.8)
background: #333
border: 1px solid #2a2a2a
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3)
.p-autocomplete-list > li
padding: 6px 10px
b
font-weight: bold

View File

@ -2,6 +2,7 @@ import './css/application.sass'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config';
import App from './components/App.vue'
import { loadCurrencies } from './currency'
@ -13,6 +14,7 @@ const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.use(PrimeVue)
app.mount('#app').$nextTick(() => {
// hide loading screen
postMessage({ payload: 'removeLoading' }, '*')

View File

@ -2,6 +2,7 @@ import '../src/css/application.sass'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config';
import App from '../src/components/App.vue'
import { loadCurrencies } from '../src/currency'
@ -9,6 +10,7 @@ import { loadCurrencies } from '../src/currency'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.use(PrimeVue)
app.mount('#app')
//console.log("test:", app.hej.test)