2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +00:00

refactor: remove global shortcuts, move Shortcuts

- update injections in a few components
- use Shift+Backspace for back (prevent clashes with the \b)
- use string context for single instance components
This commit is contained in:
18alantom 2023-03-22 22:58:36 +05:30 committed by Alan
parent 3fd4cd16be
commit e1b502b138
13 changed files with 293 additions and 257 deletions

View File

@ -57,9 +57,9 @@ import { checkForUpdates } from './utils/ipcCalls';
import { updateConfigFiles } from './utils/misc';
import { updatePrintTemplates } from './utils/printTemplates';
import { Search } from './utils/search';
import { setGlobalShortcuts } from './utils/shortcuts';
import { Shortcuts } from './utils/shortcuts';
import { routeTo } from './utils/ui';
import { Shortcuts, useKeys } from './utils/vueUtils';
import { useKeys } from './utils/vueUtils';
enum Screen {
Desk = 'Desk',
@ -102,7 +102,7 @@ export default defineComponent({
WindowsTitleBar,
},
async mounted() {
setGlobalShortcuts(this.shortcuts as Shortcuts);
// setGlobalShortcuts(this.shortcuts as Shortcuts);
this.setInitialScreen();
},
watch: {

View File

@ -1,5 +1,6 @@
<template>
<a
ref="backlink"
class="
cursor-pointer
font-semibold
@ -16,6 +17,26 @@
</a>
</template>
<script lang="ts">
import { shortcutsKey } from 'src/utils/injectionKeys';
import { ref, inject } from 'vue';
import { defineComponent } from 'vue';
export default defineComponent({});
const COMPONENT_NAME = 'BackLink';
export default defineComponent({
setup() {
return {
backlink: ref<HTMLAnchorElement | null>(null),
shortcuts: inject(shortcutsKey),
};
},
activated() {
this.shortcuts?.shift.set(COMPONENT_NAME, ['Backspace'], () => {
this.backlink?.click();
});
},
deactivated() {
this.shortcuts?.delete(COMPONENT_NAME);
},
});
</script>

View File

@ -88,15 +88,21 @@
<script>
import { Report } from 'reports/Report';
import { isNumeric } from 'src/utils';
import { languageDirectionKey } from 'src/utils/injectionKeys';
import { defineComponent } from 'vue';
import Paginator from '../Paginator.vue';
import WithScroll from '../WithScroll.vue';
import { inject } from 'vue';
export default defineComponent({
props: {
report: Report,
},
inject: ['languageDirection'],
setup() {
return {
languageDirection: inject(languageDirectionKey),
};
},
data() {
return {
wconst: 8,

View File

@ -213,6 +213,8 @@ import { defineComponent, inject, nextTick } from 'vue';
import Button from './Button.vue';
import Modal from './Modal.vue';
const COMPONENT_NAME = 'SearchBar';
type SchemaFilters = { value: string; label: string; index: number }[];
export default defineComponent({
@ -247,7 +249,7 @@ export default defineComponent({
this.openModal = false;
},
deactivated() {
this.shortcuts!.delete(this);
this.shortcuts?.delete(COMPONENT_NAME);
},
methods: {
openDocs() {
@ -287,7 +289,7 @@ export default defineComponent({
},
setShortcuts() {
for (const { shortcut, callback } of this.getShortcuts()) {
this.shortcuts!.pmod.set(this, [shortcut], callback);
this.shortcuts!.pmod.set(COMPONENT_NAME, [shortcut], callback);
}
},
modKeyText(key: string): string {

View File

@ -20,7 +20,7 @@
v-for="(s, i) in g.shortcuts"
:key="g.label + ' ' + i"
class="grid gap-4 items-start"
style="grid-template-columns: 6rem auto"
style="grid-template-columns: 8rem auto"
>
<ShortcutKeys class="text-base" :keys="s.shortcut" />
<div class="whitespace-normal text-base">{{ s.description }}</div>
@ -69,7 +69,7 @@ export default defineComponent({
description: t`Open Quick Search`,
},
{
shortcut: [ShortcutKey.delete],
shortcut: [ShortcutKey.shift, ShortcutKey.delete],
description: t`Go back to the previous page`,
},
{

View File

@ -190,6 +190,8 @@ import Icon from './Icon.vue';
import Modal from './Modal.vue';
import ShortcutsHelper from './ShortcutsHelper.vue';
const COMPONENT_NAME = 'Sidebar';
export default defineComponent({
emits: ['change-db-file'],
setup() {
@ -231,15 +233,15 @@ export default defineComponent({
this.setActiveGroup();
});
this.shortcuts?.shift.set(this, ['KeyH'], () => {
this.shortcuts?.shift.set(COMPONENT_NAME, ['KeyH'], () => {
if (document.body === document.activeElement) {
this.toggleSidebar();
}
});
this.shortcuts?.set(this, ['F1'], () => this.openDocumentation());
this.shortcuts?.set(COMPONENT_NAME, ['F1'], () => this.openDocumentation());
},
unmounted() {
this.shortcuts?.delete(this);
this.shortcuts?.delete(COMPONENT_NAME);
},
methods: {
routeTo,

View File

@ -146,12 +146,14 @@ import { isCredit } from 'models/helpers';
import { ModelNameEnum } from 'models/types';
import PageHeader from 'src/components/PageHeader.vue';
import { fyo } from 'src/initFyo';
import { languageDirectionKey } from 'src/utils/injectionKeys';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef } from 'src/utils/refs';
import { openQuickEdit } from 'src/utils/ui';
import { getMapFromList, removeAtIndex } from 'utils/index';
import { nextTick } from 'vue';
import Button from '../components/Button.vue';
import { inject } from 'vue';
import { handleErrorWithDialog } from '../errorHandling';
export default {
@ -159,6 +161,11 @@ export default {
Button,
PageHeader,
},
setup() {
return {
languageDirection: inject(languageDirectionKey),
};
},
data() {
return {
isAllCollapsed: true,
@ -172,7 +179,6 @@ export default {
refetchTotals: false,
};
},
inject: ['languageDirection'],
async mounted() {
await this.setTotalDebitAndCredit();
fyo.doc.observer.on('sync:AccountingLedgerEntry', () => {

View File

@ -246,6 +246,8 @@ import TemplateBuilderHint from './TemplateBuilderHint.vue';
import TemplateEditor from './TemplateEditor.vue';
import { inject } from 'vue';
const COMPONENT_NAME = 'TemplateBuilder';
export default defineComponent({
props: { name: String },
components: {
@ -313,7 +315,7 @@ export default defineComponent({
},
deactivated(): void {
docsPathRef.value = '';
this.shortcuts?.delete(this);
this.shortcuts?.delete(COMPONENT_NAME);
},
methods: {
setShortcuts() {
@ -321,13 +323,13 @@ export default defineComponent({
return;
}
this.shortcuts.ctrl.set(this, ['Enter'], this.setTemplate);
this.shortcuts.ctrl.set(this, ['KeyE'], this.toggleEditMode);
this.shortcuts.ctrl.set(this, ['KeyH'], this.toggleShowHints);
this.shortcuts.ctrl.set(this, ['Equal'], () =>
this.shortcuts.ctrl.set(COMPONENT_NAME, ['Enter'], this.setTemplate);
this.shortcuts.ctrl.set(COMPONENT_NAME, ['KeyE'], this.toggleEditMode);
this.shortcuts.ctrl.set(COMPONENT_NAME, ['KeyH'], this.toggleShowHints);
this.shortcuts.ctrl.set(COMPONENT_NAME, ['Equal'], () =>
this.setScale(this.scale + 0.1)
);
this.shortcuts.ctrl.set(this, ['Minus'], () =>
this.shortcuts.ctrl.set(COMPONENT_NAME, ['Minus'], () =>
this.setScale(this.scale - 0.1)
);
},

View File

@ -1,6 +1,7 @@
import { InjectionKey, Ref } from 'vue';
import { Search } from './search';
import type { Shortcuts, useKeys } from './vueUtils';
import type { InjectionKey, Ref } from 'vue';
import type { Search } from './search';
import type { Shortcuts } from './shortcuts';
import type { useKeys } from './vueUtils';
export const languageDirectionKey = Symbol('languageDirection') as InjectionKey<
Ref<'ltr' | 'rtl'>

View File

@ -163,3 +163,7 @@ export function getCreateFiltersFromListViewFilters(filters: QueryFilter) {
return createFilters;
}
export function getIsMac() {
return navigator.userAgent.indexOf('Mac') !== -1;
}

View File

@ -1,17 +1,221 @@
import router from 'src/router';
import { Shortcuts } from './vueUtils';
import { Keys } from 'utils/types';
import { watch } from 'vue';
import { getIsMac } from './misc';
import { useKeys } from './vueUtils';
interface ModMap {
alt: boolean;
ctrl: boolean;
meta: boolean;
shift: boolean;
repeat: boolean;
}
type Mod = keyof ModMap;
type Context = unknown;
type ShortcutFunction = () => void;
type ShortcutConfig = {
callback: ShortcutFunction;
propagate: boolean;
};
type ShortcutMap = Map<Context, Map<string, ShortcutConfig>>;
const mods: Readonly<Mod[]> = ['alt', 'ctrl', 'meta', 'repeat', 'shift'];
export class Shortcuts {
keys: Keys;
isMac: boolean;
contextStack: Context[];
shortcuts: ShortcutMap;
modMap: Partial<Record<Mod, boolean>>;
constructor(keys?: Keys) {
this.contextStack = [];
this.modMap = {};
this.keys = keys ?? useKeys();
this.shortcuts = new Map();
this.isMac = getIsMac();
watch(this.keys, (keys) => {
this.#trigger(keys);
});
}
#trigger(keys: Keys) {
const key = this.getKey(Array.from(keys.pressed), keys);
for (const context of this.contextStack.reverse()) {
const obj = this.shortcuts.get(context)?.get(key);
if (!obj) {
continue;
}
obj.callback();
if (!obj.propagate) {
break;
}
}
}
export function setGlobalShortcuts(shortcuts: Shortcuts) {
/**
* PMod : if macOS then Meta () else Ctrl, both Left and Right
* Check if a context is present or if a shortcut
* is present in a context.
*
* Backspace : Go to the previous page
*
* @param context context in which the shortcut is to be checked
* @param shortcut shortcut that is to be checked
* @returns
*/
shortcuts.set(window, ['Backspace'], async () => {
if (document.body !== document.activeElement) {
has(context: Context, shortcut?: string[]) {
if (!shortcut) {
return this.shortcuts.has(context);
}
const contextualShortcuts = this.shortcuts.get(context);
if (!contextualShortcuts) {
return false;
}
const key = this.getKey(shortcut);
return contextualShortcuts.has(key);
}
/**
* Assign a function to a shortcut in a given context.
*
* @param context context object to which the shortcut belongs
* @param shortcut keyboard event codes used as shortcut chord
* @param callback function to be called when the shortcut is pressed
* @param propagate whether to check and executs shortcuts in parent contexts
* @param removeIfSet whether to delete the set shortcut
*/
set(
context: Context,
shortcut: string[],
callback: ShortcutFunction,
propagate: boolean = false,
removeIfSet: boolean = true
) {
if (!this.shortcuts.has(context)) {
this.shortcuts.set(context, new Map());
}
const contextualShortcuts = this.shortcuts.get(context)!;
const key = this.getKey(shortcut);
if (removeIfSet) {
contextualShortcuts.delete(key);
}
if (contextualShortcuts.has(key)) {
throw new Error(`Shortcut ${key} already exists.`);
}
this.#pushContext(context);
contextualShortcuts.set(key, { callback, propagate });
}
/**
* Either delete a single shortcut or all the shortcuts in
* a given context.
*
* @param context context from which the shortcut is to be removed
* @param shortcut shortcut that is to be deleted
* @returns boolean indicating success
*/
delete(context: Context, shortcut?: string[]): boolean {
if (!shortcut) {
this.#removeContext(context);
return this.shortcuts.delete(context);
}
const contextualShortcuts = this.shortcuts.get(context);
if (!contextualShortcuts) {
return false;
}
const key = this.getKey(shortcut);
return contextualShortcuts.delete(key);
}
/**
* Converts shortcuts list and modMap to a string to be
* used as a shortcut key.
*
* @param shortcut array of shortcut keys
* @param modMap boolean map of mod keys to be used
* @returns string to be used as the shortcut Map key
*/
getKey(shortcut: string[], modMap?: Partial<ModMap>): string {
const _modMap = modMap || this.modMap;
this.modMap = {};
const shortcutString = shortcut.sort().join('+');
const modString = mods.filter((k) => _modMap[k]).join('+');
if (shortcutString && modString) {
return modString + '+' + shortcutString;
}
if (!modString) {
return shortcutString;
}
if (!shortcutString) {
return modString;
}
return '';
}
#pushContext(context: Context) {
this.#removeContext(context);
this.contextStack.push(context);
}
#removeContext(context: Context) {
const index = this.contextStack.indexOf(context);
if (index === -1) {
return;
}
router.back();
});
this.contextStack = [
this.contextStack.slice(0, index),
this.contextStack.slice(index + 1),
].flat();
}
/**
* Shortcut Modifiers
*/
get alt() {
this.modMap['alt'] = true;
return this;
}
get ctrl() {
this.modMap['ctrl'] = true;
return this;
}
get meta() {
this.modMap['meta'] = true;
return this;
}
get shift() {
this.modMap['shift'] = true;
return this;
}
get repeat() {
this.modMap['repeat'] = true;
return this;
}
get pmod() {
if (this.isMac) {
return this.meta;
}
return this.ctrl;
}
}

View File

@ -1,226 +1,6 @@
import { Keys } from 'utils/types';
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
interface ModMap {
alt: boolean;
ctrl: boolean;
meta: boolean;
shift: boolean;
repeat: boolean;
}
type Mod = keyof ModMap;
interface Keys extends ModMap {
pressed: Set<string>;
}
type Context = unknown;
type ShortcutFunction = () => void;
type ShortcutConfig = {
callback: ShortcutFunction;
propagate: boolean;
};
type ShortcutMap = Map<Context, Map<string, ShortcutConfig>>;
const mods: Readonly<Mod[]> = ['alt', 'ctrl', 'meta', 'repeat', 'shift'];
export class Shortcuts {
keys: Keys;
isMac: boolean;
contextStack: Context[];
shortcuts: ShortcutMap;
modMap: Partial<Record<Mod, boolean>>;
constructor(keys?: Keys) {
this.contextStack = [];
this.modMap = {};
this.keys = keys ?? useKeys();
this.shortcuts = new Map();
this.isMac = getIsMac();
watch(this.keys, (keys) => {
this.#trigger(keys);
});
}
#trigger(keys: Keys) {
const key = this.getKey(Array.from(keys.pressed), keys);
for (const context of this.contextStack.reverse()) {
const obj = this.shortcuts.get(context)?.get(key);
if (!obj) {
continue;
}
obj.callback();
if (!obj.propagate) {
break;
}
}
}
/**
* Check if a context is present or if a shortcut
* is present in a context.
*
*
* @param context context in which the shortcut is to be checked
* @param shortcut shortcut that is to be checked
* @returns
*/
has(context: Context, shortcut?: string[]) {
if (!shortcut) {
return this.shortcuts.has(context);
}
const contextualShortcuts = this.shortcuts.get(context);
if (!contextualShortcuts) {
return false;
}
const key = this.getKey(shortcut);
return contextualShortcuts.has(key);
}
/**
* Assign a function to a shortcut in a given context.
*
* @param context context object to which the shortcut belongs
* @param shortcut keyboard event codes used as shortcut chord
* @param callback function to be called when the shortcut is pressed
* @param propagate whether to check and executs shortcuts in parent contexts
* @param removeIfSet whether to delete the set shortcut
*/
set(
context: Context,
shortcut: string[],
callback: ShortcutFunction,
propagate: boolean = false,
removeIfSet: boolean = true
) {
if (!this.shortcuts.has(context)) {
this.shortcuts.set(context, new Map());
}
const contextualShortcuts = this.shortcuts.get(context)!;
const key = this.getKey(shortcut);
if (removeIfSet) {
contextualShortcuts.delete(key);
}
if (contextualShortcuts.has(key)) {
throw new Error(`Shortcut ${key} already exists.`);
}
this.#pushContext(context);
contextualShortcuts.set(key, { callback, propagate });
}
/**
* Either delete a single shortcut or all the shortcuts in
* a given context.
*
* @param context context from which the shortcut is to be removed
* @param shortcut shortcut that is to be deleted
* @returns boolean indicating success
*/
delete(context: Context, shortcut?: string[]): boolean {
if (!shortcut) {
this.#removeContext(context);
return this.shortcuts.delete(context);
}
const contextualShortcuts = this.shortcuts.get(context);
if (!contextualShortcuts) {
return false;
}
const key = this.getKey(shortcut);
return contextualShortcuts.delete(key);
}
/**
* Converts shortcuts list and modMap to a string to be
* used as a shortcut key.
*
* @param shortcut array of shortcut keys
* @param modMap boolean map of mod keys to be used
* @returns string to be used as the shortcut Map key
*/
getKey(shortcut: string[], modMap?: Partial<ModMap>): string {
const _modMap = modMap || this.modMap;
this.modMap = {};
const shortcutString = shortcut.sort().join('+');
const modString = mods.filter((k) => _modMap[k]).join('+');
if (shortcutString && modString) {
return modString + '+' + shortcutString;
}
if (!modString) {
return shortcutString;
}
if (!shortcutString) {
return modString;
}
return '';
}
#pushContext(context: Context) {
this.#removeContext(context);
this.contextStack.push(context);
}
#removeContext(context: Context) {
const index = this.contextStack.indexOf(context);
if (index === -1) {
return;
}
this.contextStack = [
this.contextStack.slice(0, index),
this.contextStack.slice(index + 1),
].flat();
}
/**
* Shortcut Modifiers
*/
get alt() {
this.modMap['alt'] = true;
return this;
}
get ctrl() {
this.modMap['ctrl'] = true;
return this;
}
get meta() {
this.modMap['meta'] = true;
return this;
}
get shift() {
this.modMap['shift'] = true;
return this;
}
get repeat() {
this.modMap['repeat'] = true;
return this;
}
get pmod() {
if (this.isMac) {
return this.meta;
}
return this.ctrl;
}
}
import { getIsMac } from './misc';
export function useKeys() {
const isMac = getIsMac();
@ -292,7 +72,3 @@ export function useMouseLocation() {
return loc;
}
function getIsMac() {
return navigator.userAgent.indexOf('Mac') !== -1;
}

View File

@ -54,3 +54,15 @@ export type PropertyEnum<T extends Record<string, any>> = {
};
export type TemplateFile = { file: string; template: string; modified: string };
export interface Keys extends ModMap {
pressed: Set<string>;
}
interface ModMap {
alt: boolean;
ctrl: boolean;
meta: boolean;
shift: boolean;
repeat: boolean;
}