2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

Merge pull request #531 from frappe/shortcuts

incr: add mods to shortcut class
This commit is contained in:
Alan 2023-01-30 06:42:38 -08:00 committed by GitHub
commit 2f3409cdf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 732 additions and 252 deletions

View File

@ -5,7 +5,6 @@ import { Verb } from 'fyo/telemetry/types';
import { DEFAULT_USER } from 'fyo/utils/consts'; import { DEFAULT_USER } from 'fyo/utils/consts';
import { ConflictError, MandatoryError, NotFoundError } from 'fyo/utils/errors'; import { ConflictError, MandatoryError, NotFoundError } from 'fyo/utils/errors';
import Observable from 'fyo/utils/observable'; import Observable from 'fyo/utils/observable';
import { Money } from 'pesa';
import { import {
DynamicLinkField, DynamicLinkField,
Field, Field,
@ -142,6 +141,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
return false; return false;
} }
if (this.schema.isChild) {
return false;
}
if (!this.schema.isSubmittable) { if (!this.schema.isSubmittable) {
return true; return true;
} }
@ -186,6 +189,14 @@ export class Doc extends Observable<DocValue | Doc[]> {
return false; return false;
} }
if (this.schema.isSingle) {
return false;
}
if (this.schema.isChild) {
return false;
}
return true; return true;
} }

View File

@ -41,6 +41,7 @@
import { ConfigKeys } from 'fyo/core/types'; import { ConfigKeys } from 'fyo/core/types';
import { RTL_LANGUAGES } from 'fyo/utils/consts'; import { RTL_LANGUAGES } from 'fyo/utils/consts';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { systemLanguageRef } from 'src/utils/refs';
import { computed } from 'vue'; import { computed } from 'vue';
import WindowsTitleBar from './components/WindowsTitleBar.vue'; import WindowsTitleBar from './components/WindowsTitleBar.vue';
import { handleErrorWithDialog } from './errorHandling'; import { handleErrorWithDialog } from './errorHandling';
@ -54,7 +55,8 @@ import { initializeInstance } from './utils/initialization';
import { checkForUpdates } from './utils/ipcCalls'; 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, systemLanguage } from './utils/ui'; import { setGlobalShortcuts } from './utils/shortcuts';
import { routeTo } from './utils/ui';
import { Shortcuts, useKeys } from './utils/vueUtils'; import { Shortcuts, useKeys } from './utils/vueUtils';
export default { export default {
@ -86,7 +88,8 @@ export default {
WindowsTitleBar, WindowsTitleBar,
}, },
async mounted() { async mounted() {
this.shortcuts = new Shortcuts(this.keys); const shortcuts = new Shortcuts(this.keys);
this.shortcuts = shortcuts;
const lastSelectedFilePath = fyo.config.get( const lastSelectedFilePath = fyo.config.get(
ConfigKeys.LastSelectedFilePath, ConfigKeys.LastSelectedFilePath,
null null
@ -102,10 +105,12 @@ export default {
await handleErrorWithDialog(err, undefined, true, true); await handleErrorWithDialog(err, undefined, true, true);
await this.showDbSelector(); await this.showDbSelector();
} }
setGlobalShortcuts(shortcuts);
}, },
computed: { computed: {
language() { language() {
return systemLanguage.value; return systemLanguageRef.value;
}, },
languageDirection() { languageDirection() {
return RTL_LANGUAGES.includes(this.language) ? 'rtl' : 'ltr'; return RTL_LANGUAGES.includes(this.language) ? 'rtl' : 'ltr';

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex bg-gray-25"> <div class="flex bg-gray-25 overflow-x-auto">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<!-- Page Header (Title, Buttons, etc) --> <!-- Page Header (Title, Buttons, etc) -->
<PageHeader :title="title" :border="false" :searchborder="searchborder"> <PageHeader :title="title" :border="false" :searchborder="searchborder">

View File

@ -17,14 +17,7 @@
v-if="openModal" v-if="openModal"
> >
<div <div
class=" class="bg-white rounded-lg shadow-2xl border overflow-hidden inner"
bg-white
rounded-lg
shadow-2xl
border
overflow-hidden
inner
"
v-bind="$attrs" v-bind="$attrs"
@click.stop @click.stop
> >

View File

@ -4,8 +4,8 @@
<Button @click="open" class="px-2" :padding="false"> <Button @click="open" class="px-2" :padding="false">
<feather-icon name="search" class="w-4 h-4 me-1 text-gray-800" /> <feather-icon name="search" class="w-4 h-4 me-1 text-gray-800" />
<p>{{ t`Search` }}</p> <p>{{ t`Search` }}</p>
<div class="text-gray-500 px-1 ms-4 text-sm"> <div class="text-gray-500 px-1 ms-4 text-sm whitespace-nowrap">
{{ modKey('k') }} {{ modKeyText('k') }}
</div> </div>
</Button> </Button>
</div> </div>
@ -16,174 +16,183 @@
@closemodal="close" @closemodal="close"
:set-close-listener="false" :set-close-listener="false"
> >
<!-- Search Input --> <div class="w-form">
<div class="p-1 w-form"> <!-- Search Input -->
<input <div class="p-1">
ref="input" <input
type="search" ref="input"
autocomplete="off" type="search"
spellcheck="false" autocomplete="off"
:placeholder="t`Type to search...`" spellcheck="false"
v-model="inputValue" :placeholder="t`Type to search...`"
@focus="search" v-model="inputValue"
@input="search" @focus="search"
@keydown.up="up" @input="search"
@keydown.down="down" @keydown.up="up"
@keydown.enter="() => select()" @keydown.down="down"
@keydown.esc="close" @keydown.enter="() => select()"
class=" @keydown.esc="close"
bg-gray-100 class="
text-2xl bg-gray-100
focus:outline-none text-2xl
w-full focus:outline-none
placeholder-gray-500 w-full
text-gray-900 placeholder-gray-500
rounded-md text-gray-900
p-3 rounded-md
" p-3
/> "
</div> />
<hr v-if="suggestions.length" /> </div>
<hr v-if="suggestions.length" />
<!-- Search List --> <!-- Search List -->
<div :style="`max-height: ${49 * 6 - 1}px`" class="overflow-auto"> <div :style="`max-height: ${49 * 6 - 1}px`" class="overflow-auto">
<div
v-for="(si, i) in suggestions"
:key="`${i}-${si.key}`"
ref="suggestions"
class="hover:bg-gray-50 cursor-pointer"
:class="idx === i ? 'border-blue-500 bg-gray-50 border-s-4' : ''"
@click="select(i)"
>
<!-- Search List Item -->
<div <div
class="flex w-full justify-between px-3 items-center" v-for="(si, i) in suggestions"
style="height: var(--h-row-mid)" :key="`${i}-${si.key}`"
ref="suggestions"
class="hover:bg-gray-50 cursor-pointer"
:class="idx === i ? 'border-blue-500 bg-gray-50 border-s-4' : ''"
@click="select(i)"
> >
<div class="flex items-center"> <!-- Search List Item -->
<div
class="flex w-full justify-between px-3 items-center"
style="height: var(--h-row-mid)"
>
<div class="flex items-center">
<p
:class="idx === i ? 'text-blue-600' : 'text-gray-900'"
:style="idx === i ? 'margin-left: -4px' : ''"
>
{{ si.label }}
</p>
<p class="text-gray-600 text-sm ms-3" v-if="si.group === 'Docs'">
{{ si.more.filter(Boolean).join(', ') }}
</p>
</div>
<p <p
:class="idx === i ? 'text-blue-600' : 'text-gray-900'" class="text-sm text-end justify-self-end"
:style="idx === i ? 'margin-left: -4px' : ''" :class="`text-${groupColorMap[si.group]}-500`"
> >
{{ si.label }} {{
</p> si.group === 'Docs' ? si.schemaLabel : groupLabelMap[si.group]
<p class="text-gray-600 text-sm ms-3" v-if="si.group === 'Docs'"> }}
{{ si.more.filter(Boolean).join(', ') }}
</p> </p>
</div> </div>
<p
class="text-sm text-end justify-self-end"
:class="`text-${groupColorMap[si.group]}-500`"
>
{{ si.group === 'Docs' ? si.schemaLabel : groupLabelMap[si.group] }}
</p>
</div>
<hr v-if="i !== suggestions.length - 1" /> <hr v-if="i !== suggestions.length - 1" />
</div>
</div>
<!-- Footer -->
<hr />
<div class="m-1 flex justify-between flex-col gap-2 text-sm select-none">
<!-- Group Filters -->
<div class="flex justify-between">
<div class="flex gap-1">
<button
v-for="g in searchGroups"
:key="g"
class="border px-1 py-0.5 rounded-lg"
:class="getGroupFilterButtonClass(g)"
@click="searcher.set(g, !searcher.filters.groupFilters[g])"
>
{{ groupLabelMap[g] }}
</button>
</div>
<button
class="hover:text-gray-900 py-0.5 rounded text-gray-700"
@click="showMore = !showMore"
>
{{ showMore ? t`Less Filters` : t`More Filters` }}
</button>
</div>
<!-- Additional Filters -->
<div v-if="showMore" class="-mt-1">
<!-- Group Skip Filters -->
<div class="flex gap-1 text-gray-800">
<button
v-for="s in ['skipTables', 'skipTransactions']"
:key="s"
class="border px-1 py-0.5 rounded-lg"
:class="{ 'bg-gray-200': searcher.filters[s] }"
@click="searcher.set(s, !searcher.filters[s])"
>
{{
s === 'skipTables' ? t`Skip Child Tables` : t`Skip Transactions`
}}
</button>
</div>
<!-- Schema Name Filters -->
<div class="flex mt-1 gap-1 text-blue-500 flex-wrap">
<button
v-for="sf in schemaFilters"
:key="sf.value"
class="
border
px-1
py-0.5
rounded-lg
border-blue-100
whitespace-nowrap
"
:class="{ 'bg-blue-100': searcher.filters.schemaFilters[sf.value] }"
@click="
searcher.set(sf.value, !searcher.filters.schemaFilters[sf.value])
"
>
{{ sf.label }}
</button>
</div> </div>
</div> </div>
<!-- Keybindings Help --> <!-- Footer -->
<div class="flex text-sm text-gray-500 justify-between items-baseline"> <hr />
<div class="flex gap-4"> <div class="m-1 flex justify-between flex-col gap-2 text-sm select-none">
<p> {{ t`Navigate` }}</p> <!-- Group Filters -->
<p> {{ t`Select` }}</p> <div class="flex justify-between">
<p><span class="tracking-tighter">esc</span> {{ t`Close` }}</p> <div class="flex gap-1">
<button
class="flex items-center hover:text-gray-800"
@click="openDocs"
>
<feather-icon name="help-circle" class="w-4 h-4 me-1" />
{{ t`Help` }}
</button>
</div>
<p v-if="searcher?.numSearches" class="ms-auto">
{{ t`${suggestions.length} out of ${searcher.numSearches}` }}
</p>
<div
class="border border-gray-100 rounded flex justify-self-end ms-2"
v-if="(searcher?.numSearches ?? 0) > 50"
>
<template
v-for="c in allowedLimits.filter(
(c) => c < searcher.numSearches || c === -1
)"
:key="c + '-count'"
>
<button <button
@click="limit = parseInt(c)" v-for="g in searchGroups"
class="w-9" :key="g"
:class="limit === c ? 'bg-gray-100' : ''" class="border px-1 py-0.5 rounded-lg"
:class="getGroupFilterButtonClass(g)"
@click="searcher.set(g, !searcher.filters.groupFilters[g])"
> >
{{ c === -1 ? t`All` : c }} {{ groupLabelMap[g] }}
</button> </button>
</template> </div>
<button
class="hover:text-gray-900 py-0.5 rounded text-gray-700"
@click="showMore = !showMore"
>
{{ showMore ? t`Less Filters` : t`More Filters` }}
</button>
</div>
<!-- Additional Filters -->
<div v-if="showMore" class="-mt-1">
<!-- Group Skip Filters -->
<div class="flex gap-1 text-gray-800">
<button
v-for="s in ['skipTables', 'skipTransactions']"
:key="s"
class="border px-1 py-0.5 rounded-lg"
:class="{ 'bg-gray-200': searcher.filters[s] }"
@click="searcher.set(s, !searcher.filters[s])"
>
{{
s === 'skipTables' ? t`Skip Child Tables` : t`Skip Transactions`
}}
</button>
</div>
<!-- Schema Name Filters -->
<div class="flex mt-1 gap-1 text-blue-500 flex-wrap">
<button
v-for="sf in schemaFilters"
:key="sf.value"
class="
border
px-1
py-0.5
rounded-lg
border-blue-100
whitespace-nowrap
"
:class="{
'bg-blue-100': searcher.filters.schemaFilters[sf.value],
}"
@click="
searcher.set(
sf.value,
!searcher.filters.schemaFilters[sf.value]
)
"
>
{{ sf.label }}
</button>
</div>
</div>
<!-- Keybindings Help -->
<div class="flex text-sm text-gray-500 justify-between items-baseline">
<div class="flex gap-4">
<p> {{ t`Navigate` }}</p>
<p> {{ t`Select` }}</p>
<p><span class="tracking-tighter">esc</span> {{ t`Close` }}</p>
<button
class="flex items-center hover:text-gray-800"
@click="openDocs"
>
<feather-icon name="help-circle" class="w-4 h-4 me-1" />
{{ t`Help` }}
</button>
</div>
<p v-if="searcher?.numSearches" class="ms-auto">
{{ t`${suggestions.length} out of ${searcher.numSearches}` }}
</p>
<div
class="border border-gray-100 rounded flex justify-self-end ms-2"
v-if="(searcher?.numSearches ?? 0) > 50"
>
<template
v-for="c in allowedLimits.filter(
(c) => c < searcher.numSearches || c === -1
)"
:key="c + '-count'"
>
<button
@click="limit = parseInt(c)"
class="w-9"
:class="limit === c ? 'bg-gray-100' : ''"
>
{{ c === -1 ? t`All` : c }}
</button>
</template>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -195,7 +204,6 @@ 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 { getModKeyCode } from 'src/utils/vueUtils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import Button from './Button.vue'; import Button from './Button.vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
@ -233,18 +241,19 @@ export default {
openLink('https://docs.frappebooks.com/' + docsPathMap.Search); openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
}, },
getShortcuts() { getShortcuts() {
const modKey = getModKeyCode(this.platform);
const ifOpen = (cb) => () => this.openModal && cb(); const ifOpen = (cb) => () => this.openModal && cb();
const ifClose = (cb) => () => !this.openModal && cb(); const ifClose = (cb) => () => !this.openModal && cb();
const shortcuts = [ const shortcuts = [
{ shortcut: ['KeyK', modKey], callback: ifClose(() => this.open()) }, {
{ shortcut: ['Escape'], callback: ifOpen(() => this.close()) }, shortcut: 'KeyK',
callback: ifClose(() => this.open()),
},
]; ];
for (const i in searchGroups) { for (const i in searchGroups) {
shortcuts.push({ shortcuts.push({
shortcut: [modKey, `Digit${Number(i) + 1}`], shortcut: `Digit${Number(i) + 1}`,
callback: ifOpen(() => { callback: ifOpen(() => {
const group = searchGroups[i]; const group = searchGroups[i];
const value = this.searcher.filters.groupFilters[group]; const value = this.searcher.filters.groupFilters[group];
@ -261,15 +270,15 @@ export default {
}, },
setShortcuts() { setShortcuts() {
for (const { shortcut, callback } of this.getShortcuts()) { for (const { shortcut, callback } of this.getShortcuts()) {
this.shortcuts.set(shortcut, callback); this.shortcuts.pmod.set([shortcut], callback);
} }
}, },
deleteShortcuts() { deleteShortcuts() {
for (const { shortcut } of this.getShortcuts()) { for (const { shortcut } of this.getShortcuts()) {
this.shortcuts.delete(shortcut); this.shortcuts.pmod.delete([shortcut]);
} }
}, },
modKey(key) { modKeyText(key) {
key = key.toUpperCase(); key = key.toUpperCase();
if (this.platform === 'Mac') { if (this.platform === 'Mac') {
return `${key}`; return `${key}`;

View File

@ -0,0 +1,199 @@
<template>
<div>
<FormHeader :form-title="t`Shortcuts`" />
<hr />
<div class="h-96 overflow-y-auto text-gray-900">
<template v-for="g in groups" :key="g.label">
<div class="p-4 w-full">
<!-- Shortcut Group Header -->
<div @click="g.collapsed = !g.collapsed" class="cursor-pointer mb-4">
<div class="font-semibold">
{{ g.label }}
</div>
<div class="text-base">
{{ g.description }}
</div>
</div>
<!-- Shortcuts -->
<div v-if="!g.collapsed" class="flex flex-col gap-4">
<div
v-for="(s, i) in g.shortcuts"
:key="g.label + ' ' + i"
class="grid gap-4 items-start"
style="grid-template-columns: 6rem auto"
>
<!-- <div class="w-2 text-base">{{ i + 1 }}.</div> -->
<div
class="
text-base
font-medium
flex-shrink-0 flex
items-center
gap-1
bg-gray-200
text-gray-700
px-1.5
py-0.5
rounded
"
style="width: fit-content"
>
<span
v-for="k in s.shortcut"
:key="k"
class="tracking-tighter"
>{{ k }}</span
>
</div>
<div class="whitespace-normal text-base">{{ s.description }}</div>
</div>
</div>
<!-- Shortcut count if collapsed -->
<div v-else class="text-base text-gray-600">
{{ t`${g.shortcuts.length} shortcuts` }}
</div>
</div>
<hr />
</template>
<div class="p-4 text-base text-gray-600">
{{ t`More shortcuts will be added soon.` }}
</div>
</div>
</div>
</template>
<script lang="ts">
import { t } from 'fyo';
import { defineComponent } from 'vue';
import FormHeader from './FormHeader.vue';
type Group = {
label: string;
description: string;
collapsed: boolean;
shortcuts: { shortcut: string[]; description: string }[];
};
export default defineComponent({
data() {
return { groups: [] } as { groups: Group[] };
},
mounted() {
this.groups = [
{
label: t`Global`,
description: t`Applicable anywhere in Frappe Books`,
collapsed: false,
shortcuts: [
{
shortcut: [this.pmod, 'K'],
description: t`Open Quick Search`,
},
{
shortcut: [this.del],
description: t`Go back to the previous page`,
},
{
shortcut: [this.shift, 'H'],
description: t`Toggle sidebar`,
},
{
shortcut: ['F1'],
description: t`Open Documentation`,
},
],
},
{
label: t`Doc`,
description: t`Applicable when a Doc is open in the Form view or Quick Edit view`,
collapsed: false,
shortcuts: [
{
shortcut: [this.pmod, 'S'],
description: [
t`Save or Submit a doc.`,
t`A doc is submitted only if it is submittable and is in the saved state.`,
].join(' '),
},
{
shortcut: [this.pmod, this.del],
description: [
t`Cancel or Delete a doc.`,
t`A doc is cancelled only if it is in the submitted state.`,
t`A submittable doc is deleted only if it is in the cancelled state.`,
].join(' '),
},
],
},
{
label: t`Quick Search`,
description: t`Applicable when Quick Search is open`,
collapsed: false,
shortcuts: [
{ shortcut: [this.esc], description: t`Close Quick Search` },
{
shortcut: [this.pmod, '1'],
description: t`Toggle the Docs filter`,
},
{
shortcut: [this.pmod, '2'],
description: t`Toggle the List filter`,
},
{
shortcut: [this.pmod, '3'],
description: t`Toggle the Create filter`,
},
{
shortcut: [this.pmod, '4'],
description: t`Toggle the Report filter`,
},
{
shortcut: [this.pmod, '5'],
description: t`Toggle the Page filter`,
},
],
},
];
},
computed: {
pmod() {
if (this.isMac) {
return '⌘';
}
return 'Ctrl';
},
shift() {
if (this.isMac) {
return 'shift';
}
return '⇧';
},
alt() {
if (this.isMac) {
return '⌥';
}
return 'Alt';
},
del() {
if (this.isMac) {
return 'delete';
}
return 'Backspace';
},
esc() {
if (this.isMac) {
return 'esc';
}
return 'Esc';
},
isMac() {
return this.platform === 'Mac';
},
},
components: { FormHeader },
});
</script>

View File

@ -100,6 +100,20 @@
</p> </p>
</button> </button>
<button
class="
flex
text-sm text-gray-600
hover:text-gray-800
gap-1
items-center
"
@click="viewShortcuts = true"
>
<feather-icon name="command" class="h-4 w-4 flex-shrink-0" />
<p>{{ t`Shortcuts` }}</p>
</button>
<button <button
class=" class="
flex flex
@ -155,6 +169,10 @@
> >
<feather-icon name="chevrons-left" class="w-4 h-4" /> <feather-icon name="chevrons-left" class="w-4 h-4" />
</button> </button>
<Modal :open-modal="viewShortcuts" @closemodal="viewShortcuts = false">
<ShortcutsHelper class="w-form" />
</Modal>
</div> </div>
</template> </template>
<script> <script>
@ -162,19 +180,23 @@ import Button from 'src/components/Button.vue';
import { reportIssue } from 'src/errorHandling'; import { reportIssue } from 'src/errorHandling';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { openLink } from 'src/utils/ipcCalls'; import { openLink } from 'src/utils/ipcCalls';
import { docsPathRef } from 'src/utils/refs';
import { getSidebarConfig } from 'src/utils/sidebarConfig'; import { getSidebarConfig } from 'src/utils/sidebarConfig';
import { docsPath, routeTo } from 'src/utils/ui'; import { routeTo } from 'src/utils/ui';
import router from '../router'; import router from '../router';
import Icon from './Icon.vue'; import Icon from './Icon.vue';
import Modal from './Modal.vue';
import ShortcutsHelper from './ShortcutsHelper.vue';
export default { export default {
components: [Button], components: [Button],
inject: ['languageDirection'], inject: ['languageDirection', 'shortcuts'],
emits: ['change-db-file', 'toggle-sidebar'], emits: ['change-db-file', 'toggle-sidebar'],
data() { data() {
return { return {
companyName: '', companyName: '',
groups: [], groups: [],
viewShortcuts: false,
activeGroup: null, activeGroup: null,
}; };
}, },
@ -185,6 +207,8 @@ export default {
}, },
components: { components: {
Icon, Icon,
Modal,
ShortcutsHelper,
}, },
async mounted() { async mounted() {
const { companyName } = await fyo.doc.getDoc('AccountingSettings'); const { companyName } = await fyo.doc.getDoc('AccountingSettings');
@ -195,12 +219,23 @@ export default {
router.afterEach(() => { router.afterEach(() => {
this.setActiveGroup(); this.setActiveGroup();
}); });
this.shortcuts.shift.set(['KeyH'], () => {
if (document.body === document.activeElement) {
this.$emit('toggle-sidebar');
}
});
this.shortcuts.set(['F1'], () => this.openDocumentation());
},
unmounted() {
this.shortcuts.alt.delete(['KeyH']);
this.shortcuts.delete(['F1']);
}, },
methods: { methods: {
routeTo, routeTo,
reportIssue, reportIssue,
openDocumentation() { openDocumentation() {
openLink('https://docs.frappebooks.com/' + docsPath.value); openLink('https://docs.frappebooks.com/' + docsPathRef.value);
}, },
setActiveGroup() { setActiveGroup() {
const { fullPath } = this.$router.currentRoute.value; const { fullPath } = this.$router.currentRoute.value;

View File

@ -147,7 +147,8 @@ import { ModelNameEnum } from 'models/types';
import PageHeader from 'src/components/PageHeader.vue'; import PageHeader from 'src/components/PageHeader.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPath, openQuickEdit } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { openQuickEdit } from 'src/utils/ui';
import { getMapFromList, removeAtIndex } from 'utils/index'; import { getMapFromList, removeAtIndex } from 'utils/index';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import Button from '../components/Button.vue'; import Button from '../components/Button.vue';
@ -184,7 +185,7 @@ export default {
window.coa = this; window.coa = this;
} }
docsPath.value = docsPathMap.ChartOfAccounts; docsPathRef.value = docsPathMap.ChartOfAccounts;
if (this.refetchTotals) { if (this.refetchTotals) {
await this.setTotalDebitAndCredit(); await this.setTotalDebitAndCredit();
@ -192,7 +193,7 @@ export default {
} }
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
}, },
methods: { methods: {
async expand() { async expand() {

View File

@ -65,12 +65,12 @@
<script> <script>
import PageHeader from 'src/components/PageHeader.vue'; import PageHeader from 'src/components/PageHeader.vue';
import { docsPath } from 'src/utils/ui';
import UnpaidInvoices from './UnpaidInvoices.vue'; import UnpaidInvoices from './UnpaidInvoices.vue';
import Cashflow from './Cashflow.vue'; import Cashflow from './Cashflow.vue';
import Expenses from './Expenses.vue'; import Expenses from './Expenses.vue';
import PeriodSelector from './PeriodSelector.vue'; import PeriodSelector from './PeriodSelector.vue';
import ProfitAndLoss from './ProfitAndLoss.vue'; import ProfitAndLoss from './ProfitAndLoss.vue';
import { docsPathRef } from 'src/utils/refs';
export default { export default {
name: 'Dashboard', name: 'Dashboard',
@ -86,10 +86,10 @@ export default {
return { period: 'This Year' }; return { period: 'This Year' };
}, },
activated() { activated() {
docsPath.value = 'analytics/dashboard'; docsPathRef.value = 'analytics/dashboard';
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
}, },
methods: { methods: {
handlePeriodChange(period) { handlePeriodChange(period) {

View File

@ -355,7 +355,8 @@ import { importable, Importer } from 'src/dataImport';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls'; import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPath, showMessageDialog } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { showMessageDialog } from 'src/utils/ui';
import Loading from '../components/Loading.vue'; import Loading from '../components/Loading.vue';
export default { export default {
@ -484,10 +485,10 @@ export default {
}, },
}, },
activated() { activated() {
docsPath.value = docsPathMap.DataImport; docsPathRef.value = docsPathMap.DataImport;
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
if (!this.complete) { if (!this.complete) {
return; return;
} }

View File

@ -130,7 +130,6 @@
<template #quickedit v-if="quickEditDoc"> <template #quickedit v-if="quickEditDoc">
<QuickEditForm <QuickEditForm
class="w-quick-edit"
:name="quickEditDoc.name" :name="quickEditDoc.name"
:show-name="false" :show-name="false"
:show-save="false" :show-save="false"
@ -160,8 +159,8 @@ import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue'; import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import { import {
docsPath,
getGroupedActionsForDoc, getGroupedActionsForDoc,
routeTo, routeTo,
showMessageDialog, showMessageDialog,
@ -232,14 +231,17 @@ export default {
}, },
}, },
activated() { activated() {
docsPath.value = docsPathMap[this.schemaName]; docsPathRef.value = docsPathMap[this.schemaName];
focusedDocsRef.add(this.doc);
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
}, },
async mounted() { async mounted() {
try { try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name); this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) { } catch (error) {
if (error instanceof fyo.errors.NotFoundError) { if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`); routeTo(`/list/${this.schemaName}`);

View File

@ -273,7 +273,6 @@
<Transition name="quickedit"> <Transition name="quickedit">
<QuickEditForm <QuickEditForm
v-if="quickEditDoc && !linked" v-if="quickEditDoc && !linked"
class="w-quick-edit"
:name="quickEditDoc.name" :name="quickEditDoc.name"
:show-name="false" :show-name="false"
:show-save="false" :show-save="false"
@ -313,8 +312,8 @@ import StatusBadge from 'src/components/StatusBadge.vue';
import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue'; import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import { import {
docsPath,
getGroupedActionsForDoc, getGroupedActionsForDoc,
routeTo, routeTo,
showMessageDialog, showMessageDialog,
@ -339,6 +338,7 @@ export default {
LinkedEntryWidget, LinkedEntryWidget,
Barcode, Barcode,
}, },
inject: ['shortcuts'],
provide() { provide() {
return { return {
schemaName: this.schemaName, schemaName: this.schemaName,
@ -455,14 +455,17 @@ export default {
}, },
}, },
activated() { activated() {
docsPath.value = docsPathMap[this.schemaName]; docsPathRef.value = docsPathMap[this.schemaName];
focusedDocsRef.add(this.doc);
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
}, },
async mounted() { async mounted() {
try { try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name); this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) { } catch (error) {
if (error instanceof fyo.errors.NotFoundError) { if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`); routeTo(`/list/${this.schemaName}`);

View File

@ -148,8 +148,8 @@ import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue'; import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import { import {
docsPath,
getGroupedActionsForDoc, getGroupedActionsForDoc,
routeTo, routeTo,
showMessageDialog, showMessageDialog,
@ -182,14 +182,17 @@ export default {
}; };
}, },
activated() { activated() {
docsPath.value = docsPathMap.JournalEntry; docsPathRef.value = docsPathMap.JournalEntry;
focusedDocsRef.add(this.doc);
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
}, },
async mounted() { async mounted() {
try { try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name); this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) { } catch (error) {
if (error instanceof fyo.errors.NotFoundError) { if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`); routeTo(`/list/${this.schemaName}`);

View File

@ -51,7 +51,8 @@ import {
docsPathMap, docsPathMap,
getCreateFiltersFromListViewFilters, getCreateFiltersFromListViewFilters,
} from 'src/utils/misc'; } from 'src/utils/misc';
import { docsPath, openQuickEdit, routeTo } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { openQuickEdit, routeTo } from 'src/utils/ui';
import List from './List.vue'; import List from './List.vue';
export default { export default {
@ -82,14 +83,14 @@ export default {
} }
this.listConfig = getListConfig(this.schemaName); this.listConfig = getListConfig(this.schemaName);
docsPath.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries; docsPathRef.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries;
if (this.fyo.store.isDevelopment) { if (this.fyo.store.isDevelopment) {
window.lv = this; window.lv = this;
} }
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
}, },
methods: { methods: {
updatedData(listFilters) { updatedData(listFilters) {

View File

@ -105,6 +105,7 @@ import StatusBadge from 'src/components/StatusBadge.vue';
import TwoColumnForm from 'src/components/TwoColumnForm.vue'; import TwoColumnForm from 'src/components/TwoColumnForm.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { getQuickEditWidget } from 'src/utils/quickEditWidgets'; import { getQuickEditWidget } from 'src/utils/quickEditWidgets';
import { focusedDocsRef } from 'src/utils/refs';
import { getActionsForDoc, openQuickEdit } from 'src/utils/ui'; import { getActionsForDoc, openQuickEdit } from 'src/utils/ui';
export default { export default {
@ -131,6 +132,7 @@ export default {
DropdownWithActions, DropdownWithActions,
}, },
emits: ['close'], emits: ['close'],
inject: ['shortcuts'],
provide() { provide() {
return { return {
schemaName: this.schemaName, schemaName: this.schemaName,
@ -147,17 +149,26 @@ export default {
statusText: null, statusText: null,
}; };
}, },
mounted() { async mounted() {
if (this.defaults) { if (this.defaults) {
this.values = JSON.parse(this.defaults); this.values = JSON.parse(this.defaults);
} }
await this.fetchFieldsAndDoc();
focusedDocsRef.add(this.doc);
if (fyo.store.isDevelopment) { if (fyo.store.isDevelopment) {
window.qef = this; window.qef = this;
} }
}, },
async created() { activated() {
await this.fetchFieldsAndDoc(); focusedDocsRef.add(this.doc);
},
deactivated() {
focusedDocsRef.delete(this.doc);
},
unmounted() {
focusedDocsRef.delete(this.doc);
}, },
computed: { computed: {
isChild() { isChild() {

View File

@ -46,7 +46,7 @@ import PageHeader from 'src/components/PageHeader.vue';
import ListReport from 'src/components/Report/ListReport.vue'; import ListReport from 'src/components/Report/ListReport.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPath } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
@ -70,7 +70,7 @@ export default defineComponent({
}, },
components: { PageHeader, FormControl, ListReport, DropdownWithActions }, components: { PageHeader, FormControl, ListReport, DropdownWithActions },
async activated() { async activated() {
docsPath.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports; docsPathRef.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports;
await this.setReportData(); await this.setReportData();
const filters = JSON.parse(this.defaultFilters); const filters = JSON.parse(this.defaultFilters);
@ -88,7 +88,7 @@ export default defineComponent({
} }
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
}, },
computed: { computed: {
title() { title() {

View File

@ -49,7 +49,8 @@ import Row from 'src/components/Row.vue';
import StatusBadge from 'src/components/StatusBadge.vue'; import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPath, showToast } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { showToast } from 'src/utils/ui';
import { IPC_MESSAGES } from 'utils/messages'; import { IPC_MESSAGES } from 'utils/messages';
import { h, markRaw } from 'vue'; import { h, markRaw } from 'vue';
import TabBase from './TabBase.vue'; import TabBase from './TabBase.vue';
@ -112,10 +113,10 @@ export default {
}, },
activated() { activated() {
this.setActiveTab(); this.setActiveTab();
docsPath.value = docsPathMap.Settings; docsPathRef.value = docsPathMap.Settings;
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
if (this.fieldsChanged.length === 0) { if (this.fieldsChanged.length === 0) {
return; return;
} }

View File

@ -82,6 +82,7 @@ input[type='number']::-webkit-inner-spin-button {
.w-quick-edit { .w-quick-edit {
width: var(--w-quick-edit); width: var(--w-quick-edit);
flex-shrink: 0;
} }
.h-form { .h-form {

View File

@ -3,7 +3,8 @@ import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation'; import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages'; import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { showToast, systemLanguage } from './ui'; import { systemLanguageRef } from './refs';
import { showToast } from './ui';
// Language: Language Code in books/translations // Language: Language Code in books/translations
export const languageCodeMap: Record<string, string> = { export const languageCodeMap: Record<string, string> = {
@ -42,7 +43,7 @@ export async function setLanguageMap(
if (success && !usingDefault) { if (success && !usingDefault) {
fyo.config.set('language', language); fyo.config.set('language', language);
systemLanguage.value = language; systemLanguageRef.value = language;
} }
if (!dontReload && success && initLanguage !== oldLanguage) { if (!dontReload && success && initLanguage !== oldLanguage) {

View File

@ -1,5 +1,6 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { ConfigFile, ConfigKeys } from 'fyo/core/types'; import { ConfigFile, ConfigKeys } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard'; import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
@ -160,3 +161,45 @@ export function getCreateFiltersFromListViewFilters(filters: QueryFilter) {
return createFilters; return createFilters;
} }
export class FocusedDocContextSet {
set: Doc[];
constructor() {
this.set = [];
}
add(doc: unknown) {
if (!(doc instanceof Doc)) {
return;
}
const index = this.findIndex(doc);
if (index !== -1) {
this.delete(index);
}
return this.set.push(doc);
}
delete(index: Doc | number) {
if (typeof index !== 'number') {
index = this.findIndex(index);
}
if (index === -1) {
return;
}
this.set = this.set.filter((_, i) => i !== index);
}
last() {
return this.set.at(-1);
}
findIndex(doc: Doc) {
return this.set.findIndex(
(d) => d.name === doc.name && d.schemaName === doc.schemaName
);
}
}

8
src/utils/refs.ts Normal file
View File

@ -0,0 +1,8 @@
import { reactive, ref } from 'vue';
import { FocusedDocContextSet } from './misc';
export const docsPathRef = ref<string>('');
export const systemLanguageRef = ref<string>('');
export const focusedDocsRef = reactive<FocusedDocContextSet>(
new FocusedDocContextSet()
);

View File

@ -594,10 +594,7 @@ export class Search {
keys.sort((a, b) => safeParseFloat(b) - safeParseFloat(a)); keys.sort((a, b) => safeParseFloat(b) - safeParseFloat(a));
const array: SearchItems = []; const array: SearchItems = [];
for (const key of keys) { for (const key of keys) {
const keywords = groupedKeywords[key]; const keywords = groupedKeywords[key] ?? [];
if (!keywords?.length) {
continue;
}
this._pushDocSearchItems(keywords, array, input); this._pushDocSearchItems(keywords, array, input);
if (key === '0') { if (key === '0') {

78
src/utils/shortcuts.ts Normal file
View File

@ -0,0 +1,78 @@
import { t } from 'fyo';
import type { Doc } from 'fyo/model/doc';
import { fyo } from 'src/initFyo';
import router from 'src/router';
import { focusedDocsRef } from './refs';
import { showMessageDialog } from './ui';
import { Shortcuts } from './vueUtils';
export function setGlobalShortcuts(shortcuts: Shortcuts) {
/**
* PMod : if macOS then Meta () else Ctrl, both Left and Right
*
* Backspace : Go to the previous page
* PMod + S : Save or Submit focused doc if possible
* PMod + Backspace : Cancel or Delete focused doc if possible
*/
shortcuts.set(['Backspace'], async () => {
if (document.body !== document.activeElement) {
return;
}
router.back();
});
shortcuts.pmod.set(['KeyS'], async () => {
const doc = focusedDocsRef.last();
if (!doc) {
return;
}
if (doc.canSave) {
await showDocStateChangeMessageDialog(doc, 'sync');
} else if (doc.canSubmit) {
await showDocStateChangeMessageDialog(doc, 'submit');
}
});
shortcuts.pmod.set(['Backspace'], async () => {
const doc = focusedDocsRef.last();
if (!doc) {
return;
}
if (doc.canCancel) {
await showDocStateChangeMessageDialog(doc, 'cancel');
} else if (doc.canDelete) {
await showDocStateChangeMessageDialog(doc, 'delete');
}
});
}
async function showDocStateChangeMessageDialog(
doc: Doc,
state: 'sync' | 'submit' | 'cancel' | 'delete'
) {
const label = fyo.schemaMap[doc.schemaName]?.label ?? t`Doc`;
const name = doc.name ?? '';
const message =
{ sync: t`Save`, submit: t`Submit`, cancel: t`Cancel`, delete: t`Delete` }[
state
] + ` ${label} ${name}`;
await showMessageDialog({
message,
buttons: [
{
label: t`Yes`,
async action() {
await doc[state]();
},
},
{
label: t`No`,
action() {},
},
],
});
}

View File

@ -4,7 +4,7 @@
*/ */
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { t } from 'fyo'; import { t } from 'fyo';
import { Doc } from 'fyo/model/doc'; import type { Doc } from 'fyo/model/doc';
import { Action } from 'fyo/model/types'; import { Action } from 'fyo/model/types';
import { getActions } from 'fyo/utils'; import { getActions } from 'fyo/utils';
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors'; import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
@ -23,9 +23,6 @@ import {
ToastOptions, ToastOptions,
} from './types'; } from './types';
export const docsPath = ref('');
export const systemLanguage = ref('');
export async function openQuickEdit({ export async function openQuickEdit({
doc, doc,
schemaName, schemaName,

View File

@ -1,7 +1,6 @@
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue'; import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
interface Keys { interface ModMap {
pressed: Set<string>;
alt: boolean; alt: boolean;
ctrl: boolean; ctrl: boolean;
meta: boolean; meta: boolean;
@ -9,13 +8,27 @@ interface Keys {
repeat: boolean; repeat: boolean;
} }
export class Shortcuts { type Mod = keyof ModMap;
keys: Ref<Keys>;
shortcuts: Map<string, Function>;
constructor(keys?: Ref<Keys>) { interface Keys extends ModMap {
pressed: Set<string>;
}
type ShortcutFunction = () => void;
const mods: Readonly<Mod[]> = ['alt', 'ctrl', 'meta', 'repeat', 'shift'];
export class Shortcuts {
keys: Keys;
isMac: boolean;
shortcuts: Map<string, ShortcutFunction>;
modMap: Partial<Record<Mod, boolean>>;
constructor(keys?: Keys) {
this.modMap = {};
this.keys = keys ?? useKeys(); this.keys = keys ?? useKeys();
this.shortcuts = new Map(); this.shortcuts = new Map();
this.isMac = getIsMac();
watch(this.keys, (keys) => { watch(this.keys, (keys) => {
this.#trigger(keys); this.#trigger(keys);
@ -23,17 +36,22 @@ export class Shortcuts {
} }
#trigger(keys: Keys) { #trigger(keys: Keys) {
const key = Array.from(keys.pressed).sort().join('+'); const key = this.getKey(Array.from(keys.pressed), keys);
this.shortcuts.get(key)?.(); this.shortcuts.get(key)?.();
} }
has(shortcut: string[]) { has(shortcut: string[]) {
const key = shortcut.sort().join('+'); const key = this.getKey(shortcut);
return this.shortcuts.has(key); return this.shortcuts.has(key);
} }
set(shortcut: string[], callback: Function, removeIfSet: boolean = true) { set(
const key = shortcut.sort().join('+'); shortcut: string[],
callback: ShortcutFunction,
removeIfSet: boolean = true
) {
const key = this.getKey(shortcut);
if (removeIfSet) { if (removeIfSet) {
this.shortcuts.delete(key); this.shortcuts.delete(key);
} }
@ -46,13 +64,68 @@ export class Shortcuts {
} }
delete(shortcut: string[]) { delete(shortcut: string[]) {
const key = shortcut.sort().join('+'); const key = this.getKey(shortcut);
this.shortcuts.delete(key); this.shortcuts.delete(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 '';
}
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;
} else {
return this.ctrl;
}
}
} }
export function useKeys() { export function useKeys() {
const keys: Ref<Keys> = ref({ const isMac = getIsMac();
const keys: Keys = reactive({
pressed: new Set<string>(), pressed: new Set<string>(),
alt: false, alt: false,
ctrl: false, ctrl: false,
@ -62,21 +135,32 @@ export function useKeys() {
}); });
const keydownListener = (e: KeyboardEvent) => { const keydownListener = (e: KeyboardEvent) => {
keys.value.pressed.add(e.code); keys.alt = e.altKey;
keys.value.alt = e.altKey; keys.ctrl = e.ctrlKey;
keys.value.ctrl = e.ctrlKey; keys.meta = e.metaKey;
keys.value.meta = e.metaKey; keys.shift = e.shiftKey;
keys.value.shift = e.shiftKey; keys.repeat = e.repeat;
keys.value.repeat = e.repeat;
const { code } = e;
if (
code.startsWith('Alt') ||
code.startsWith('Control') ||
code.startsWith('Meta') ||
code.startsWith('Shift')
) {
return;
}
keys.pressed.add(code);
}; };
const keyupListener = (e: KeyboardEvent) => { const keyupListener = (e: KeyboardEvent) => {
keys.value.pressed.delete(e.code); const { code } = e;
if (code.startsWith('Meta') && isMac) {
// Key up won't trigger on macOS for other keys. return keys.pressed.clear();
if (e.code === 'MetaLeft') {
keys.value.pressed.clear();
} }
keys.pressed.delete(code);
}; };
onMounted(() => { onMounted(() => {
@ -110,10 +194,6 @@ export function useMouseLocation() {
return loc; return loc;
} }
export function getModKeyCode(platform: 'Windows' | 'Linux' | 'Mac') { function getIsMac() {
if (platform === 'Mac') { return navigator.userAgent.indexOf('Mac') !== -1;
return 'MetaLeft';
}
return 'CtrlLeft';
} }