Files
books/src/components/Dropdown.vue
T

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>