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:
commit
2f3409cdf5
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
src/App.vue
11
src/App.vue
@ -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';
|
||||||
|
@ -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">
|
||||||
|
@ -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
|
||||||
>
|
>
|
||||||
|
@ -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}`;
|
||||||
|
199
src/components/ShortcutsHelper.vue
Normal file
199
src/components/ShortcutsHelper.vue
Normal 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>
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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}`);
|
||||||
|
@ -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}`);
|
||||||
|
@ -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}`);
|
||||||
|
@ -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) {
|
||||||
|
@ -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() {
|
||||||
|
@ -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() {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
8
src/utils/refs.ts
Normal 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()
|
||||||
|
);
|
@ -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
78
src/utils/shortcuts.ts
Normal 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() {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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';
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user