|
|
@@ -0,0 +1,165 @@
|
|
|
+<template>
|
|
|
+ <div class="filter-modal" v-show="show" @click="closeFilter">
|
|
|
+ <div class="filter-panel" ref="panel">
|
|
|
+ <div class="filter-title" v-if="title">{{ title }}</div>
|
|
|
+ <div class="filter" v-for="(f, idx) in filters" :key="idx">
|
|
|
+ <date-picker
|
|
|
+ v-if="f.type == 'date'"
|
|
|
+ :label="f.name"
|
|
|
+ lang="fr"
|
|
|
+ target="day"
|
|
|
+ :modelValue="defaultValues[idx]"
|
|
|
+ @update:modelValue="updateDateFilter($event, idx)"
|
|
|
+ />
|
|
|
+ <date-picker
|
|
|
+ v-if="f.type == 'hour'"
|
|
|
+ :label="f.name"
|
|
|
+ lang="fr"
|
|
|
+ target="hour"
|
|
|
+ :modelValue="defaultValues[idx]"
|
|
|
+ @update:modelValue="updateDateFilter($event, idx)"
|
|
|
+ />
|
|
|
+ <select-chip-input
|
|
|
+ v-if="f.type == 'category'"
|
|
|
+ :label="f.name"
|
|
|
+ :modelValue="defaultValues[idx]"
|
|
|
+ @update:modelValue="updateCategoryFilter($event, idx)"
|
|
|
+ :autocompleteList="autocompleteList[idx]"
|
|
|
+ :placeholder="f.placeholder"
|
|
|
+ secondaryPlaceholder="+1"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts">
|
|
|
+import { defineComponent, PropType } from "vue";
|
|
|
+import SelectChipInput from "@/components/SelectChipInput.vue";
|
|
|
+import DatePicker from "@/components/date-picker.vue";
|
|
|
+import dayjs, { ConfigType } from "dayjs";
|
|
|
+
|
|
|
+export type FilterParam = {
|
|
|
+ name: string;
|
|
|
+ key: string;
|
|
|
+ type: "hour" | "date" | "category";
|
|
|
+ direction?: "bigger" | "smaller";
|
|
|
+ default?: string;
|
|
|
+ placeholder?: string;
|
|
|
+};
|
|
|
+export default defineComponent({
|
|
|
+ components: { DatePicker, SelectChipInput },
|
|
|
+ emits: ["filterChange"],
|
|
|
+ props: {
|
|
|
+ title: { type: String, default: "Apply filter" },
|
|
|
+ data: { type: Array as PropType<Array<Record<string, unknown>>>, default: () => [] },
|
|
|
+ filters: { type: Array as PropType<Array<FilterParam>>, default: () => [] },
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ defaultValues: [] as Array<unknown>,
|
|
|
+ filterFunctions: [] as Array<(o: Record<string, unknown>) => boolean>,
|
|
|
+ show: false,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ autocompleteList(): Array<Array<unknown>> {
|
|
|
+ return this.filters.map((f) => {
|
|
|
+ return f.type == "category" ? Array.from(new Set(this.data.flatMap((o) => o[f.key]))) : [];
|
|
|
+ });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ data() {
|
|
|
+ this.emitFilteredValue();
|
|
|
+ },
|
|
|
+ filters() {
|
|
|
+ this.reset();
|
|
|
+ },
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ open() {
|
|
|
+ this.show = true;
|
|
|
+ },
|
|
|
+ closeFilter(e: MouseEvent) {
|
|
|
+ if (!(this.$refs.panel as HTMLElement).contains(e.target as Node)) {
|
|
|
+ this.show = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ reset() {
|
|
|
+ this.filterFunctions = this.filters.map(() => () => true);
|
|
|
+ this.defaultValues = this.filters.map(this.getDefault);
|
|
|
+ },
|
|
|
+ getDefault(f: FilterParam) {
|
|
|
+ if (["date", "hour"].includes(f.type)) {
|
|
|
+ if (f.default == undefined) return "";
|
|
|
+ const d = dayjs(f.default);
|
|
|
+ return d.isValid() ? d.toISOString() : "";
|
|
|
+ }
|
|
|
+ if (f.type == "category") {
|
|
|
+ if (f.default == undefined) return [];
|
|
|
+ return JSON.parse(f.default) ?? [];
|
|
|
+ }
|
|
|
+ },
|
|
|
+ emitFilteredValue() {
|
|
|
+ this.$emit(
|
|
|
+ "filterChange",
|
|
|
+ this.data.filter((o) => !this.filterFunctions.some((f) => !f(o)))
|
|
|
+ );
|
|
|
+ },
|
|
|
+ updateDateFilter(value: Date, idx: number) {
|
|
|
+ const f = this.filters[idx];
|
|
|
+ const d = dayjs(value);
|
|
|
+ const compareFunction = f.direction == "smaller" ? d.isBefore : d.isAfter;
|
|
|
+ this.filterFunctions[idx] = d.isValid()
|
|
|
+ ? (o) => compareFunction(dayjs(o[f.key] as ConfigType))
|
|
|
+ : () => true;
|
|
|
+ this.defaultValues[idx] = value;
|
|
|
+ this.emitFilteredValue();
|
|
|
+ },
|
|
|
+ updateCategoryFilter(value: Array<unknown>, idx: number) {
|
|
|
+ const f = this.filters[idx];
|
|
|
+
|
|
|
+ this.filterFunctions[idx] =
|
|
|
+ value.length > 0
|
|
|
+ ? (o) => !value.some((k) => !(o[f.key] as Array<unknown>).includes(k))
|
|
|
+ : () => true;
|
|
|
+
|
|
|
+ this.defaultValues[idx] = value;
|
|
|
+ this.emitFilteredValue();
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.reset();
|
|
|
+ },
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.filter-modal {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background-color: rgba(0, 0, 0, 0.2);
|
|
|
+ z-index: 1300;
|
|
|
+}
|
|
|
+.filter-panel {
|
|
|
+ position: relative;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ bottom: 0;
|
|
|
+ overflow-x: auto;
|
|
|
+ background-color: #fff;
|
|
|
+ width: 350px;
|
|
|
+ height: 100vh;
|
|
|
+}
|
|
|
+.filter-title {
|
|
|
+ padding: 16px;
|
|
|
+ font-size: 2rem;
|
|
|
+}
|
|
|
+.filter {
|
|
|
+ padding: 8px 16px;
|
|
|
+}
|
|
|
+</style>
|