mirror of
https://github.com/frappe/books.git
synced 2026-08-24 02:24:17 -05:00
223 lines
5.7 KiB
Vue
223 lines
5.7 KiB
Vue
<template>
|
|
<Popover
|
|
:show-popup="isShown"
|
|
:hide-arrow="true"
|
|
:placement="right ? 'bottom-end' : 'bottom-start'"
|
|
>
|
|
<template #target>
|
|
<div v-on-outside-click="() => (isShown = false)" class="h-full">
|
|
<slot
|
|
:toggle-dropdown="toggleDropdown"
|
|
:highlight-item-up="highlightItemUp"
|
|
:highlight-item-down="highlightItemDown"
|
|
:select-highlighted-item="selectHighlightedItem"
|
|
></slot>
|
|
</div>
|
|
</template>
|
|
<template #content>
|
|
<div
|
|
class="bg-white dark:bg-gray-850 dark:text-white rounded w-full min-w-40 overflow-hidden"
|
|
>
|
|
<div class="p-1 max-h-64 overflow-auto custom-scroll text-sm">
|
|
<div
|
|
v-if="isLoading"
|
|
class="p-2 text-gray-600 dark:text-gray-400 italic"
|
|
>
|
|
{{ t`Loading...` }}
|
|
</div>
|
|
<div
|
|
v-else-if="dropdownItems.length === 0"
|
|
class="p-2 text-gray-600 dark:text-gray-400 italic"
|
|
>
|
|
{{ getEmptyMessage() }}
|
|
</div>
|
|
<template v-else>
|
|
<div
|
|
v-for="(d, index) in dropdownItems"
|
|
:key="`key-${index}`"
|
|
ref="items"
|
|
>
|
|
<div
|
|
v-if="d.isGroup"
|
|
class="px-2 pt-3 pb-1 text-xs uppercase text-gray-700 dark:text-gray-400 font-semibold tracking-wider"
|
|
>
|
|
{{ d.label }}
|
|
</div>
|
|
<a
|
|
v-else
|
|
class="block p-2 rounded-md mt-1 first:mt-0 cursor-pointer truncate"
|
|
:class="
|
|
index === highlightedIndex
|
|
? 'bg-gray-100 dark:bg-gray-875'
|
|
: ''
|
|
"
|
|
@mouseenter="highlightedIndex = index"
|
|
@mousedown.prevent
|
|
@click="selectItem(d)"
|
|
>
|
|
<component :is="d.component" v-if="d.component" />
|
|
<template v-else>{{ d.label }}</template>
|
|
</a>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Popover>
|
|
</template>
|
|
<script lang="ts">
|
|
import { Doc } from 'fyo/model/doc';
|
|
import { Field } from 'schemas/types';
|
|
import { fyo } from 'src/initFyo';
|
|
import { DropdownItem } from 'src/utils/types';
|
|
import { defineComponent, PropType } from 'vue';
|
|
import Popover from './Popover.vue';
|
|
|
|
export default defineComponent({
|
|
name: 'Dropdown',
|
|
components: {
|
|
Popover,
|
|
},
|
|
props: {
|
|
items: {
|
|
type: Array as PropType<DropdownItem[]>,
|
|
default: () => [],
|
|
},
|
|
right: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
isLoading: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
df: {
|
|
type: Object as PropType<Field | null>,
|
|
default: null,
|
|
},
|
|
doc: {
|
|
type: Object as PropType<Doc | null>,
|
|
default: null,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isShown: false,
|
|
highlightedIndex: -1,
|
|
};
|
|
},
|
|
computed: {
|
|
dropdownItems(): DropdownItem[] {
|
|
const groupedItems = getGroupedItems(this.items ?? []);
|
|
const groupNames = Object.keys(groupedItems).filter(Boolean).sort();
|
|
|
|
const items: DropdownItem[] = groupedItems[''] ?? [];
|
|
for (let group of groupNames) {
|
|
items.push({
|
|
label: group,
|
|
isGroup: true,
|
|
});
|
|
|
|
const grouped = groupedItems[group] ?? [];
|
|
items.push(...grouped);
|
|
}
|
|
|
|
return items;
|
|
},
|
|
},
|
|
watch: {
|
|
highlightedIndex() {
|
|
this.scrollToHighlighted();
|
|
},
|
|
dropdownItems() {
|
|
const maxed = Math.max(this.highlightedIndex, -1);
|
|
this.highlightedIndex = Math.min(maxed, this.dropdownItems.length - 1);
|
|
},
|
|
},
|
|
methods: {
|
|
getEmptyMessage(): string {
|
|
const { schemaName, fieldname } = this.df ?? {};
|
|
if (!schemaName || !fieldname || !this.doc) {
|
|
return this.t`Empty`;
|
|
}
|
|
|
|
const emptyMessage = fyo.models[schemaName]?.emptyMessages[fieldname]?.(
|
|
this.doc
|
|
);
|
|
|
|
if (!emptyMessage) {
|
|
return this.t`Empty`;
|
|
}
|
|
|
|
return emptyMessage;
|
|
},
|
|
async selectItem(d?: DropdownItem): Promise<void> {
|
|
if (!d || !d?.action) {
|
|
return;
|
|
}
|
|
|
|
if (this.doc) {
|
|
return await d.action(this.doc, this.$router);
|
|
}
|
|
|
|
await d.action();
|
|
},
|
|
toggleDropdown(flag?: boolean): void {
|
|
if (typeof flag !== 'boolean') {
|
|
flag = !this.isShown;
|
|
}
|
|
|
|
this.isShown = flag;
|
|
},
|
|
async selectHighlightedItem(): Promise<void> {
|
|
let item = this.items[this.highlightedIndex];
|
|
if (!item && this.dropdownItems.length === 1) {
|
|
item = this.dropdownItems[0];
|
|
}
|
|
|
|
return await this.selectItem(item);
|
|
},
|
|
highlightItemUp(e?: Event): void {
|
|
e?.preventDefault();
|
|
|
|
this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
|
|
},
|
|
highlightItemDown(e?: Event): void {
|
|
e?.preventDefault();
|
|
|
|
this.highlightedIndex = Math.min(
|
|
this.dropdownItems.length - 1,
|
|
this.highlightedIndex + 1
|
|
);
|
|
},
|
|
scrollToHighlighted(): void {
|
|
const elems = this.$refs.items;
|
|
if (!Array.isArray(elems)) {
|
|
return;
|
|
}
|
|
|
|
const highlightedElement = elems[this.highlightedIndex];
|
|
if (!(highlightedElement instanceof Element)) {
|
|
return;
|
|
}
|
|
|
|
highlightedElement.scrollIntoView({ block: 'nearest' });
|
|
},
|
|
},
|
|
});
|
|
|
|
function getGroupedItems(
|
|
items: DropdownItem[]
|
|
): Record<string, DropdownItem[]> {
|
|
const groupedItems: Record<string, DropdownItem[]> = {};
|
|
for (let item of items) {
|
|
const group = item.group ?? '';
|
|
|
|
groupedItems[group] ??= [];
|
|
groupedItems[group].push(item);
|
|
}
|
|
|
|
return groupedItems;
|
|
}
|
|
</script>
|