<template> <div class="grow flex flex-col gap-2 overflow-hidden"> <div v-if="useSearch" class="relative self-end flex flex-row items-center gap-2"> <Spinner v-if="deferingSearch" /> <input type="text" class="!max-w-64 !w-64 rounded-md shadow-sm relative block px-3 py-2 pr-5 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Suche" v-model="searchString" /> <XMarkIcon class="absolute h-4 stroke-2 right-2 top-1/2 -translate-y-1/2 cursor-pointer z-10" @click="searchString = ''" /> </div> <div class="flex flex-col w-full grow gap-2 pr-2 overflow-y-scroll"> <div v-if="indicateLoading" class="flex flex-row justify-center items-center w-full p-1"> <Spinner /> </div> <p v-if="visibleRows.length == 0" class="flex flex-row w-full gap-2 p-1">Kein Inhalt</p> <slot v-else name="pageRow" v-for="(item, index) in visibleRows" :key="index" :row="item" @click="$emit('clickRow', item)" > <p>{{ item }}</p> </slot> </div> <div class="flex flex-row w-full justify-between select-none"> <p class="text-sm font-normal text-gray-500"> Elemente <span class="font-semibold text-gray-900">{{ showingText }}</span> von <span class="font-semibold text-gray-900">{{ entryCount }}</span> </p> <ul class="flex flex-row text-sm h-8"> <li class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg" :class="[currentPage > 0 ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50']" @click="loadPage(currentPage - 1)" > <ChevronLeftIcon class="h-4" /> </li> <li v-for="page in displayedPagesNumbers" :key="page" class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 first:rounded-s-lg last:rounded-e-lg" :class="[currentPage == page ? 'font-bold border-primary' : '', page != '.' ? ' cursor-pointer' : '']" @click="loadPage(page)" > {{ typeof page == "number" ? page + 1 : "..." }} </li> <li class="flex h-8 w-8 items-center justify-center text-gray-500 bg-white border border-gray-300 first:rounded-s-lg last:rounded-e-lg" :class="[ currentPage + 1 < countOfPages ? 'cursor-pointer hover:bg-gray-100 hover:text-gray-700' : 'opacity-50', ]" @click="loadPage(currentPage + 1)" > <ChevronRightIcon class="h-4" /> </li> </ul> </div> </div> </template> <script setup lang="ts" generic="T extends { id: FieldType }"> import { computed, ref, watch } from "vue"; import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid"; import Spinner from "./Spinner.vue"; import type { FieldType } from "@/types/dynamicQueries"; const props = defineProps({ items: { type: Array<T>, default: [] }, maxEntriesPerPage: { type: Number, default: 25 }, totalCount: { type: Number, default: null }, config: { type: Array<{ key: string }>, default: [] }, useSearch: { type: Boolean, default: false }, enablePreSearch: { type: Boolean, default: false }, indicateLoading: { type: Boolean, default: false }, }); const slots = defineSlots<{ pageRow(props: { row: T; key: number }): void; }>(); const timer = ref(undefined) as undefined | any; const currentPage = ref(0); const searchString = ref(""); const deferingSearch = ref(false); watch(searchString, async () => { deferingSearch.value = true; clearTimeout(timer.value); timer.value = setTimeout(() => { currentPage.value = 0; deferingSearch.value = false; emit("search", searchString.value); }, 600); }); watch( () => props.totalCount, async () => { currentPage.value = 0; } ); const emit = defineEmits({ submit(id: number) { return typeof id == "number"; }, loadData(offset: number, count: number, searchString: string) { return typeof offset == "number" && typeof offset == "number" && typeof searchString == "number"; }, search(search: string) { return typeof search == "string"; }, clickRow(elem: T) { return true; }, }); const entryCount = computed(() => props.totalCount ?? props.items.length); const showingStart = computed(() => currentPage.value * props.maxEntriesPerPage); const showingEnd = computed(() => { let max = currentPage.value * props.maxEntriesPerPage + props.maxEntriesPerPage; if (max > entryCount.value) max = entryCount.value; return max; }); const showingText = computed(() => `${entryCount.value != 0 ? showingStart.value + 1 : 0} - ${showingEnd.value}`); const countOfPages = computed(() => Math.ceil(entryCount.value / props.maxEntriesPerPage)); const displayedPagesNumbers = computed(() => { let stateOfPush = false; return [...new Array(countOfPages.value)].reduce((acc, curr, index) => { if ( index <= 1 || index >= countOfPages.value - 2 || (currentPage.value - 1 <= index && index <= currentPage.value + 1) ) { acc.push(index); stateOfPush = false; return acc; } if (stateOfPush == true) return acc; acc.push("."); stateOfPush = true; return acc; }, []); }); const visibleRows = computed(() => filterData(props.items, searchString.value, showingStart.value, showingEnd.value)); const loadPage = (newPage: number | ".") => { if (newPage == ".") return; if (newPage < 0 || newPage >= countOfPages.value) return; let pageStart = newPage * props.maxEntriesPerPage; let pageEnd = newPage * props.maxEntriesPerPage + props.maxEntriesPerPage; if (pageEnd > entryCount.value) pageEnd = entryCount.value; let loadedElementCount = filterData(props.items, searchString.value, pageStart, pageEnd).length; if (loadedElementCount < props.maxEntriesPerPage && (pageEnd != props.totalCount || loadedElementCount == 0)) emit("loadData", pageStart, props.maxEntriesPerPage, searchString.value); currentPage.value = newPage; }; const filterData = (array: Array<any>, searchString: string, start: number, end: number): Array<any> => { return array .filter( (elem) => !props.enablePreSearch || searchString.trim() == "" || props.config.some( (col) => typeof elem?.[col.key] == "string" && elem[col.key].toLowerCase().includes(searchString.trim().toLowerCase()) ) ) .filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end); }; </script> <!-- <script lang="ts"> export default defineComponent({ computed: { entryCount() { return this.totalCount ?? this.items.length; }, showingStart() { return this.currentPage * this.maxEntriesPerPage; }, showingEnd() { let max = this.currentPage * this.maxEntriesPerPage + this.maxEntriesPerPage; if (max > this.entryCount) max = this.entryCount; return max; }, showingText() { return `${this.entryCount != 0 ? this.showingStart + 1 : 0} - ${this.showingEnd}`; }, countOfPages() { return Math.ceil(this.entryCount / this.maxEntriesPerPage); }, displayedPagesNumbers(): Array<number | "."> { //indicate if "." or page number gets pushed let stateOfPush = false; return [...new Array(this.countOfPages)].reduce((acc, curr, index) => { if ( // always display first 2 pages index <= 1 || // always display last 2 pages index >= this.countOfPages - 2 || // always display 1 pages around current page (this.currentPage - 1 <= index && index <= this.currentPage + 1) ) { acc.push(index); stateOfPush = false; return acc; } // abort if placeholder already added to array if (stateOfPush == true) return acc; // show placeholder if pagenumber is not actively rendered acc.push("."); stateOfPush = true; return acc; }, []); }, visibleRows() { return this.filterData(this.items, this.searchString, this.showingStart, this.showingEnd); }, }, methods: { loadPage(newPage: number | ".") { if (newPage == ".") return; if (newPage < 0 || newPage >= this.countOfPages) return; let pageStart = newPage * this.maxEntriesPerPage; let pageEnd = newPage * this.maxEntriesPerPage + this.maxEntriesPerPage; if (pageEnd > this.entryCount) pageEnd = this.entryCount; let loadedElementCount = this.filterData(this.items, this.searchString, pageStart, pageEnd).length; if (loadedElementCount < this.maxEntriesPerPage) this.$emit("loadData", { offset: pageStart, count: this.maxEntriesPerPage, search: this.searchString }); this.currentPage = newPage; }, filterData(array: Array<any>, searchString: string, start: number, end: number): Array<any> { return array .filter( (elem) => !this.enablePreSearch || searchString.trim() == "" || this.config.some((col) => typeof elem?.[col.key] == "string" && elem[col.key].includes(searchString.trim())) ) .filter((elem, index) => (elem?.tab_pos ?? index) >= start && (elem?.tab_pos ?? index) < end); }, }, }); </script> -->