2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 23:30:56 +00:00

refactor: use App level shortcut

This commit is contained in:
18alantom 2022-12-08 15:19:22 +05:30
parent 0bc22d847b
commit a140e82321
4 changed files with 129 additions and 59 deletions

View File

@ -52,20 +52,27 @@ import { checkForUpdates } from './utils/ipcCalls';
import { updateConfigFiles } from './utils/misc'; import { updateConfigFiles } from './utils/misc';
import { Search } from './utils/search'; import { Search } from './utils/search';
import { routeTo } from './utils/ui'; import { routeTo } from './utils/ui';
import { Shortcuts, useKeys } from './utils/vueUtils';
export default { export default {
name: 'App', name: 'App',
setup() {
return { keys: useKeys() };
},
data() { data() {
return { return {
activeScreen: null, activeScreen: null,
dbPath: '', dbPath: '',
companyName: '', companyName: '',
searcher: null, searcher: null,
shortcuts: null,
}; };
}, },
provide() { provide() {
return { return {
searcher: computed(() => this.searcher), searcher: computed(() => this.searcher),
shortcuts: computed(() => this.shortcuts),
keys: computed(() => this.keys),
}; };
}, },
components: { components: {
@ -75,6 +82,7 @@ export default {
WindowsTitleBar, WindowsTitleBar,
}, },
async mounted() { async mounted() {
this.shortcuts = new Shortcuts(this.keys);
const lastSelectedFilePath = fyo.config.get( const lastSelectedFilePath = fyo.config.get(
ConfigKeys.LastSelectedFilePath, ConfigKeys.LastSelectedFilePath,
null null

View File

@ -11,7 +11,11 @@
</div> </div>
<!-- Search Modal --> <!-- Search Modal -->
<Modal :open-modal="openModal" @closemodal="close" :set-close-listener="false"> <Modal
:open-modal="openModal"
@closemodal="close"
:set-close-listener="false"
>
<!-- Search Input --> <!-- Search Input -->
<div class="p-1"> <div class="p-1">
<input <input
@ -191,18 +195,13 @@ import { getBgTextColorClass } from 'src/utils/colors';
import { openLink } from 'src/utils/ipcCalls'; import { openLink } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { getGroupLabelMap, searchGroups } from 'src/utils/search'; import { getGroupLabelMap, searchGroups } from 'src/utils/search';
import { useKeys } from 'src/utils/vueUtils'; import { getModKeyCode } from 'src/utils/vueUtils';
import { getIsNullOrUndef } from 'utils/';
import { safeParseInt } from 'utils/index'; import { safeParseInt } from 'utils/index';
import { nextTick, watch } from 'vue'; import { nextTick, watch } from 'vue';
import Button from './Button.vue'; import Button from './Button.vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
export default { export default {
setup() {
const keys = useKeys();
return { keys };
},
data() { data() {
return { return {
idx: 0, idx: 0,
@ -214,69 +213,62 @@ export default {
allowedLimits: [50, 100, 500, -1], allowedLimits: [50, 100, 500, -1],
}; };
}, },
inject: ['searcher'], inject: ['searcher', 'shortcuts'],
components: { Modal, Button }, components: { Modal, Button },
async mounted() { async mounted() {
if (fyo.store.isDevelopment) { if (fyo.store.isDevelopment) {
window.search = this; window.search = this;
} }
watch(this.keys, (keys) => {
if (
keys.size === 2 &&
keys.has('KeyK') &&
(keys.has('MetaLeft') || keys.has('ControlLeft'))
) {
this.open();
}
if (!this.openModal) {
return;
}
if (keys.size === 1 && keys.has('Escape')) {
this.close();
}
const input = this.$refs.input;
if (!getIsNullOrUndef(input) && document.activeElement !== input) {
input.focus();
}
this.setFilter(keys);
});
this.openModal = false; this.openModal = false;
}, },
activated() { activated() {
this.setShortcuts();
this.openModal = false; this.openModal = false;
}, },
deactivated() {
this.deleteShortcuts();
},
methods: { methods: {
openDocs() { openDocs() {
openLink('https://docs.frappebooks.com/' + docsPathMap.Search); openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
}, },
setFilter(keys) { getShortcuts() {
if (!keys.has('MetaLeft') && !keys.has('ControlLeft')) { const modKey = getModKeyCode(this.platform);
return; const ifOpen = (cb) => () => this.openModal && cb();
const ifClose = (cb) => () => !this.openModal && cb();
const shortcuts = [
{ shortcut: ['KeyK', modKey], callback: ifClose(() => this.open()) },
{ shortcut: ['Escape'], callback: ifOpen(() => this.close()) },
];
for (const i in searchGroups) {
shortcuts.push({
shortcut: [modKey, `Digit${Number(i) + 1}`],
callback: ifOpen(() => {
const group = searchGroups[i];
const value = this.searcher.filters.groupFilters[group];
if (typeof value !== 'boolean') {
return;
}
this.searcher.set(group, !value);
}),
});
} }
if (!keys.size === 2) { return shortcuts;
return; },
setShortcuts() {
for (const { shortcut, callback } of this.getShortcuts()) {
this.shortcuts.set(shortcut, callback);
} }
},
const matches = [...keys].join(',').match(/Digit(\d+)/); deleteShortcuts() {
if (!matches) { for (const { shortcut } of this.getShortcuts()) {
return; this.shortcuts.delete(shortcut);
} }
const digit = matches[1];
const index = safeParseInt(digit) - 1;
const group = searchGroups[index];
const value = this.searcher.filters.groupFilters[group];
if (!group || typeof value !== 'boolean') {
return;
}
this.searcher.set(group, !value);
}, },
modKey(key) { modKey(key) {
key = key.toUpperCase(); key = key.toUpperCase();

View File

@ -5,6 +5,6 @@ declare module 'vue' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
t: (...args: TranslationLiteral[]) => string; t: (...args: TranslationLiteral[]) => string;
fyo: Fyo; fyo: Fyo;
platform: string; platform: 'Windows' | 'Linux' | 'Mac';
} }
} }

View File

@ -1,19 +1,81 @@
import { onMounted, onUnmounted, Ref, ref } from 'vue'; import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
export function useKeys(callback?: (keys: Set<string>) => void) { interface Keys {
const keys: Ref<Set<string>> = ref(new Set()); pressed: Set<string>;
alt: boolean;
ctrl: boolean;
meta: boolean;
shift: boolean;
repeat: boolean;
}
export class Shortcuts {
keys: Ref<Keys>;
shortcuts: Map<string, Function>;
constructor(keys?: Ref<Keys>) {
this.keys = keys ?? useKeys();
this.shortcuts = new Map();
watch(this.keys, (keys) => {
this.#trigger(keys);
});
}
#trigger(keys: Keys) {
const key = Array.from(keys.pressed).sort().join('+');
this.shortcuts.get(key)?.();
}
has(shortcut: string[]) {
const key = shortcut.sort().join('+');
return this.shortcuts.has(key);
}
set(shortcut: string[], callback: Function, removeIfSet: boolean = true) {
const key = shortcut.sort().join('+');
if (removeIfSet) {
this.shortcuts.delete(key);
}
if (this.shortcuts.has(key)) {
throw new Error(`Shortcut ${key} already exists.`);
}
this.shortcuts.set(key, callback);
}
delete(shortcut: string[]) {
const key = shortcut.sort().join('+');
this.shortcuts.delete(key);
}
}
export function useKeys() {
const keys: Ref<Keys> = ref({
pressed: new Set<string>(),
alt: false,
ctrl: false,
meta: false,
shift: false,
repeat: false,
});
const keydownListener = (e: KeyboardEvent) => { const keydownListener = (e: KeyboardEvent) => {
keys.value.add(e.code); keys.value.pressed.add(e.code);
callback?.(keys.value); keys.value.alt = e.altKey;
keys.value.ctrl = e.ctrlKey;
keys.value.meta = e.metaKey;
keys.value.shift = e.shiftKey;
keys.value.repeat = e.repeat;
}; };
const keyupListener = (e: KeyboardEvent) => { const keyupListener = (e: KeyboardEvent) => {
keys.value.delete(e.code); keys.value.pressed.delete(e.code);
// Key up won't trigger on macOS for other keys. // Key up won't trigger on macOS for other keys.
if (e.code === 'MetaLeft') { if (e.code === 'MetaLeft') {
keys.value.clear(); keys.value.pressed.clear();
} }
}; };
@ -47,3 +109,11 @@ export function useMouseLocation() {
return loc; return loc;
} }
export function getModKeyCode(platform: 'Windows' | 'Linux' | 'Mac') {
if (platform === 'Mac') {
return 'MetaLeft';
}
return 'CtrlLeft';
}