filteringPanel.vue 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <template>
  2. <div class="filter-modal" v-show="show" @click="closeFilter">
  3. <div class="filter-panel" ref="panel">
  4. <div class="filter-title" v-if="title">{{ title }}</div>
  5. <div class="filter" v-for="(f, idx) in filters" :key="idx">
  6. <date-picker
  7. v-if="f.type == 'date'"
  8. :label="f.name"
  9. lang="fr"
  10. target="day"
  11. :modelValue="defaultValues[idx]"
  12. @update:modelValue="updateDateFilter($event, idx)"
  13. />
  14. <date-picker
  15. v-if="f.type == 'hour'"
  16. :label="f.name"
  17. lang="fr"
  18. target="hour"
  19. :modelValue="defaultValues[idx]"
  20. @update:modelValue="updateDateFilter($event, idx)"
  21. />
  22. <select-chip-input
  23. v-if="f.type == 'category'"
  24. :label="f.name"
  25. :modelValue="defaultValues[idx]"
  26. @update:modelValue="updateCategoryFilter($event, idx)"
  27. :autocompleteList="autocompleteList[idx]"
  28. :placeholder="f.placeholder"
  29. secondaryPlaceholder="+1"
  30. />
  31. </div>
  32. </div>
  33. </div>
  34. </template>
  35. <script lang="ts">
  36. import { defineComponent, PropType } from "vue";
  37. import SelectChipInput from "@/components/SelectChipInput.vue";
  38. import DatePicker from "@/components/date-picker.vue";
  39. import dayjs, { ConfigType } from "dayjs";
  40. export type FilterParam = {
  41. name: string;
  42. key: string;
  43. type: "hour" | "date" | "category";
  44. direction?: "bigger" | "smaller";
  45. default?: string;
  46. placeholder?: string;
  47. };
  48. export default defineComponent({
  49. components: { DatePicker, SelectChipInput },
  50. emits: ["filterChange"],
  51. props: {
  52. title: { type: String, default: "Apply filter" },
  53. data: { type: Array as PropType<Array<Record<string, unknown>>>, default: () => [] },
  54. filters: { type: Array as PropType<Array<FilterParam>>, default: () => [] },
  55. },
  56. data() {
  57. return {
  58. defaultValues: [] as Array<unknown>,
  59. filterFunctions: [] as Array<(o: Record<string, unknown>) => boolean>,
  60. show: false,
  61. };
  62. },
  63. computed: {
  64. autocompleteList(): Array<Array<unknown>> {
  65. return this.filters.map((f) => {
  66. return f.type == "category" ? Array.from(new Set(this.data.flatMap((o) => o[f.key]))) : [];
  67. });
  68. },
  69. },
  70. watch: {
  71. data() {
  72. this.emitFilteredValue();
  73. },
  74. filters() {
  75. this.reset();
  76. },
  77. },
  78. methods: {
  79. open() {
  80. this.show = true;
  81. },
  82. closeFilter(e: MouseEvent) {
  83. if (!(this.$refs.panel as HTMLElement).contains(e.target as Node)) {
  84. this.show = false;
  85. }
  86. },
  87. reset() {
  88. this.filterFunctions = this.filters.map(() => () => true);
  89. this.defaultValues = this.filters.map(this.getDefault);
  90. },
  91. getDefault(f: FilterParam) {
  92. if (["date", "hour"].includes(f.type)) {
  93. if (f.default == undefined) return "";
  94. const d = dayjs(f.default);
  95. return d.isValid() ? d.toISOString() : "";
  96. }
  97. if (f.type == "category") {
  98. if (f.default == undefined) return [];
  99. return JSON.parse(f.default) ?? [];
  100. }
  101. },
  102. emitFilteredValue() {
  103. this.$emit(
  104. "filterChange",
  105. this.data.filter((o) => !this.filterFunctions.some((f) => !f(o)))
  106. );
  107. },
  108. updateDateFilter(value: Date, idx: number) {
  109. const f = this.filters[idx];
  110. const d = dayjs(value);
  111. const compareFunction = f.direction == "smaller" ? d.isBefore : d.isAfter;
  112. this.filterFunctions[idx] = d.isValid()
  113. ? (o) => compareFunction(dayjs(o[f.key] as ConfigType))
  114. : () => true;
  115. this.defaultValues[idx] = value;
  116. this.emitFilteredValue();
  117. },
  118. updateCategoryFilter(value: Array<unknown>, idx: number) {
  119. const f = this.filters[idx];
  120. this.filterFunctions[idx] =
  121. value.length > 0
  122. ? (o) => !value.some((k) => !(o[f.key] as Array<unknown>).includes(k))
  123. : () => true;
  124. this.defaultValues[idx] = value;
  125. this.emitFilteredValue();
  126. },
  127. },
  128. mounted() {
  129. this.reset();
  130. },
  131. });
  132. </script>
  133. <style scoped>
  134. .filter-modal {
  135. position: fixed;
  136. top: 0;
  137. bottom: 0;
  138. left: 0;
  139. right: 0;
  140. background-color: rgba(0, 0, 0, 0.2);
  141. z-index: 1300;
  142. }
  143. .filter-panel {
  144. position: relative;
  145. top: 0;
  146. left: 0;
  147. bottom: 0;
  148. overflow-x: auto;
  149. background-color: #fff;
  150. width: 350px;
  151. height: 100vh;
  152. }
  153. .filter-title {
  154. padding: 16px;
  155. font-size: 2rem;
  156. }
  157. .filter {
  158. padding: 8px 16px;
  159. }
  160. </style>