@itfin/components 2.0.16 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,41 +1,56 @@
1
1
  <template>
2
2
  <div class="itf-filter-panel d-flex flex-column gap-3 align-items-start">
3
- <div v-if="search" class="d-flex gap-2 justify-content-between w-100">
3
+ <div v-if="!filtersOnly" class="d-flex gap-2 justify-content-between w-100">
4
4
  <slot name="search">
5
- <itf-text-field
6
- style="width: 300px"
7
- small
8
- :placeholder="searchPlaceholder"
9
- prepend-icon="search"
10
- :delay-input="250"
11
- clearable
12
- :value="filterValue.query"
13
- @input="(e) => onFilterChange({ type: 'text', name: 'query' }, { value: e })"
14
- />
5
+ <div>
6
+ <itf-text-field
7
+ v-if="search"
8
+ style="width: 300px"
9
+ small
10
+ :placeholder="searchPlaceholder"
11
+ prepend-icon="search"
12
+ :delay-input="250"
13
+ clearable
14
+ :value="filterValueQuery"
15
+ @input="(e) => onFilterChange({ type: 'text', name: 'query' }, { value: e })"
16
+ />
17
+ </div>
15
18
  </slot>
16
19
  <div class="d-flex gap-2">
17
- <itf-button default icon class="position-relative" @click="toggleFilters">
20
+ <!--itf-button v-if="showFilter" default icon class="position-relative" @click="toggleFilters" :class="{'active': showFilters}">
18
21
  <itf-icon new name="filter" />
19
22
  <span v-if="activeFiltersCount" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-primary">
20
23
  {{activeFiltersCount}}
21
24
  <span class="visually-hidden">active filters</span>
22
25
  </span>
23
- </itf-button>
26
+ </itf-button-->
24
27
  <slot name="after-filter-btn"></slot>
25
28
  </div>
26
29
  </div>
27
- <div v-if="showFilters" class="d-flex gap-2 flex-nowrap filters-row">
28
- <filter-badge
29
- v-for="(facet, n) in visibleFilters"
30
- :key="n"
31
- v-model="filter[facet.name]"
32
- :is-default="filter[facet.name].isDefault"
33
- :text="filter[facet.name].label"
34
- :type="facet.type"
35
- :icon="facet.icon"
36
- :options="facet.options"
37
- @change="onFilterChange(facet, $event)"
38
- />
30
+ <div class="d-flex align-items-start justify-content-between w-100">
31
+ <div v-show="showFilters && showFilter" class="gap-2 filters-row" ref="container" :class="{'expanded': isFilterExpanded}">
32
+ <filter-badge
33
+ v-for="(facet, n) in visibleFilters"
34
+ :key="n"
35
+ class="itf-filter-panel__badge"
36
+ :ref="'item-' + n"
37
+ v-model="filter[facet.name]"
38
+ :is-default="filter[facet.name].isDefault"
39
+ :text="filter[facet.name].label"
40
+ :type="facet.type"
41
+ :icon="facet.icon"
42
+ :options="facet.options"
43
+ :class="{ hidden: !visibleItems.has(n) && !isFilterExpanded }"
44
+ @change="onFilterChange(facet, $event)"
45
+ />
46
+ </div>
47
+ <div v-if="showFilters && showFilter && (visibleItems.size < visibleFilters.length || (isFilterExpanded && visibleItems.size === visibleFilters.length))">
48
+ <itf-button icon default small class="itf-filter-panel__filters" @click="toggleExpandFilter">
49
+ <itf-icon v-if="isFilterExpanded" name="minus" />
50
+ <itf-icon v-else name="plus" />
51
+ </itf-button>
52
+ </div>
53
+ <slot name="after-filters"></slot>
39
54
  </div>
40
55
  <div v-if="loading">
41
56
  <span class="itf-spinner"></span>
@@ -45,6 +60,18 @@
45
60
  </template>
46
61
  <style lang="scss">
47
62
  .itf-filter-panel {
63
+ &__badge {
64
+ transition: opacity 0.3s ease-in-out;
65
+
66
+ &.hidden {
67
+ opacity: 0;
68
+ pointer-events: none;
69
+ visibility: hidden;
70
+ }
71
+ }
72
+ &__filters {
73
+ outline: 1px solid var(--filter-badge__default-border-color);
74
+ }
48
75
  .itf-text-field:not(.is-valid):not(.is-invalid) .itf-icon {
49
76
  color: #8E97A5;
50
77
  }
@@ -63,18 +90,28 @@
63
90
  }
64
91
 
65
92
  .filters-row {
66
- @media (max-width: 768px) {
67
- overflow: auto;
68
- width: 100%;
69
- padding: 2px;
70
- margin: -2px;
93
+ display: flex;
94
+ overflow: hidden;
95
+ width: 100%;
96
+ padding: 2px;
97
+ margin: -2px;
98
+ flex-wrap: nowrap;
99
+
100
+ &.expanded {
101
+ flex-wrap: wrap;
102
+
103
+ .itf-filter-panel__badge.hidden {
104
+ opacity: 1;
105
+ visibility: visible;
106
+ pointer-events: all;
107
+ }
71
108
  }
72
109
  }
73
110
  }
74
111
  </style>
75
112
  <script>
76
113
  import { DateTime } from 'luxon';
77
- import { Vue, Model, Prop, Component } from 'vue-property-decorator';
114
+ import { Vue, Watch, Model, Prop, Component } from 'vue-property-decorator';
78
115
  import tooltip from '../../directives/tooltip';
79
116
  import itfIcon from '../icon/Icon';
80
117
  import itfButton from '../button/Button';
@@ -103,18 +140,75 @@ class FilterPanel extends Vue {
103
140
  @Prop() panel;
104
141
  @Prop(String) stateName;
105
142
  @Prop(Boolean) search;
143
+ @Prop({ type: Boolean, default: true }) showFilter;
144
+ @Prop({ type: Boolean, default: false }) filtersOnly;
106
145
  @Prop(Boolean) mini;
107
146
  @Prop({ type: String, default: function() { return this.$t('components.filter.search'); } }) searchPlaceholder;
108
147
 
109
148
  filter = {};
110
- filterValue = {};
149
+ filterValue = null;
111
150
  filters = [];
112
151
  loading = false;
113
152
  showFilters = true;
153
+ isFilterExpanded = false;
154
+ observer = null;
155
+
156
+ periodFilters = ['period', 'timeframe'];
157
+ visibleItems = new Set();
158
+
159
+ toggleExpandFilter() {
160
+ this.isFilterExpanded = !this.isFilterExpanded;
161
+ }
162
+
163
+ beforeDestroy() {
164
+ if (this.observer) {
165
+ this.observer.disconnect();
166
+ }
167
+ }
168
+
169
+ initObserver() {
170
+ if (this.observer) {
171
+ this.observer.disconnect();
172
+ }
173
+ if (!this.$refs.container) {
174
+ return;
175
+ }
176
+ this.observer = new IntersectionObserver(
177
+ (entries) => {
178
+ entries.forEach(entry => {
179
+ const index = parseInt(entry.target.dataset.index);
180
+ const filter = this.filters[index];
181
+ const value = this.filter[filter.name];
182
+ if (entry.isIntersecting) {
183
+ this.visibleItems.add(index); // Додаємо, якщо елемент у полі зору
184
+ } else {
185
+ this.visibleItems.delete(index); // Видаляємо, якщо вийшов за межі
186
+ }
187
+ this.$forceUpdate(); // Оновлюємо Vue, бо Set не є реактивним
188
+ });
189
+ },
190
+ { root: this.$refs.container, threshold: 1.0 }
191
+ );
192
+
193
+ // Спостерігаємо за кожним елементом
194
+ this.$nextTick(() => {
195
+ for (const index in this.visibleFilters) {
196
+ const item = this.$refs[`item-${index}`][0];
197
+ if (item) {
198
+ item.$el.dataset.index = index; // Зберігаємо індекс у dataset
199
+ this.observer.observe(item.$el);
200
+ }
201
+ }
202
+ });
203
+ }
204
+
205
+ get filterValueQuery() {
206
+ return this.filterValue?.query ?? '';
207
+ }
114
208
 
115
209
  get visibleFilters() {
116
210
  if (this.mini) {
117
- return sortBy(this.filters, (f) => this.filter[f.name].isDefault).filter(f => !f.options?.hidden).slice(0, 2);
211
+ return sortBy(this.filters, (f) => this.filter[f.name].isDefault).filter(f => !f.options?.hidden);
118
212
  }
119
213
  return this.filters.filter(f => !f.options?.hidden);
120
214
  }
@@ -127,6 +221,12 @@ class FilterPanel extends Vue {
127
221
  return `filter-panel-${this.stateName}-filters`;
128
222
  }
129
223
 
224
+ @Watch('staticFilters', { deep: true })
225
+ onStaticFiltersUpdate() {
226
+ this.filters = this.staticFilters ?? [];
227
+ this.loadFiltersValue();
228
+ }
229
+
130
230
  async mounted() {
131
231
  if (this.stateName) {
132
232
  const item = localStorage.getItem(this.localstorageKey);
@@ -137,8 +237,10 @@ class FilterPanel extends Vue {
137
237
  if (this.endpoint) {
138
238
  this.loading = true;
139
239
  await this.$try(async () => {
140
- const {filters} = await this.$axios.$get(this.endpoint);
240
+ const payload = this.panel ? this.panel.getPayload() : {};
241
+ const {filters, tableSchema} = await this.$axios.$get(this.endpoint, { params: payload });
141
242
  this.filters = filters;
243
+ this.$emit('set-table-schema', tableSchema);
142
244
  this.loadFiltersValue();
143
245
  });
144
246
  this.loading = false;
@@ -163,7 +265,7 @@ class FilterPanel extends Vue {
163
265
  const filterValue = {};
164
266
  if (this.filters) {
165
267
  for (const item of this.filters) {
166
- if (item.type === 'period') {
268
+ if (this.periodFilters.includes(item.type)) {
167
269
  filter[item.name] = payload.from ? this.formatValue(item, { value: [payload.from, payload.to] }) : { isDefault: true, ...item.options.defaultValue };
168
270
  filterValue.from = payload.from;
169
271
  filterValue.to = payload.to;
@@ -176,13 +278,25 @@ class FilterPanel extends Vue {
176
278
  if (this.search) {
177
279
  filterValue.query = payload.query;
178
280
  }
179
- const prevFilter = JSON.stringify(this.filter).concat(JSON.stringify(this.filterValue));
180
- const newFilter = JSON.stringify(filter).concat(JSON.stringify(filterValue));
181
- if (prevFilter !== newFilter) {
281
+ const prevFilter = JSON.stringify(this.getVisibleFilters(this.filterValue));//.concat(JSON.stringify(this.filterValue));
282
+ const newFilter = JSON.stringify(this.getVisibleFilters(filterValue));//.concat(JSON.stringify(filterValue));
283
+ if (prevFilter !== newFilter || !this.filterValue) {
182
284
  this.filter = filter;
183
285
  this.filterValue = filterValue;
184
286
  this.$emit('input', this.filterValue);
287
+ this.initObserver();
288
+ }
289
+ }
290
+
291
+ getVisibleFilters(filter) {
292
+ const result = [];
293
+ const facets = Object.values(this.filter);
294
+ for (const facet of facets) {
295
+ if (!facet.isDefault && !facet.hidden) {
296
+ result.push(filter[facet.name]);
297
+ }
185
298
  }
299
+ return result.filter(Boolean);
186
300
  }
187
301
 
188
302
  setFilter(field, value) {
@@ -195,7 +309,7 @@ class FilterPanel extends Vue {
195
309
 
196
310
  onFilterChange(facet, value) {
197
311
  this.filter[facet.name] = this.formatValue(facet, value);
198
- if (facet.type === 'period') {
312
+ if (this.periodFilters.includes(facet.type)) {
199
313
  this.filterValue.from = this.filter[facet.name].isDefault ? undefined : value.value[0];
200
314
  this.filterValue.to = this.filter[facet.name].isDefault ? undefined : value.value[1];
201
315
  } else {
@@ -226,7 +340,7 @@ class FilterPanel extends Vue {
226
340
  }
227
341
 
228
342
  formatValue(facet, value) {
229
- if (facet.type === 'period') {
343
+ if (this.periodFilters.includes(facet.type)) {
230
344
  if (value.value) {
231
345
  let from = DateTime.fromFormat(value.value[0], 'yyyy-MM-dd');
232
346
  let to = DateTime.fromFormat(value.value[1], 'yyyy-MM-dd');
@@ -285,7 +399,7 @@ class FilterPanel extends Vue {
285
399
  value.isDefault = facet.options.defaultValue ? JSON.stringify(value.value) === JSON.stringify(facet.options.defaultValue.value) : false;
286
400
  value.label = item ? item.label : facet.options.defaultValue.label;
287
401
  } else if (facet.type === 'text') {
288
- value.value = value.value.length ? value.value : undefined;
402
+ value.value = value.value.length ? value.value : (facet.options?.defaultValue ?? undefined);
289
403
  value.isDefault = !value.value;
290
404
  }
291
405
  value.hidden = facet.options?.hidden ?? false;
@@ -0,0 +1,4 @@
1
+ <template><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M11.4 5V8C11.4 8.88366 12.1163 9.6 13 9.6H16V15.7372L15.8527 15.5899C15.2279 14.9651 14.2148 14.9651 13.59 15.5899C12.9651 16.2148 12.9651 17.2278 13.59 17.8527L14.7373 19H7C6.44772 19 6 18.5523 6 18V6C6 5.44772 6.44772 5 7 5H11.4ZM12.6 5.6V8C12.6 8.22091 12.7791 8.4 13 8.4H15.4L12.6 5.6ZM18.1456 20.1456C18.0881 20.2031 18.0218 20.2465 17.951 20.2758C17.8803 20.3051 17.8027 20.3213 17.7213 20.3213C17.6401 20.323 17.4416 20.2901 17.2971 20.1456L14.2971 17.1456C14.0627 16.9113 14.0627 16.5314 14.2971 16.297C14.5314 16.0627 14.9113 16.0627 15.1456 16.297L17.1213 18.2728V13.7213C17.1213 13.3899 17.39 13.1213 17.7213 13.1213C18.0527 13.1213 18.3213 13.3899 18.3213 13.7213V18.2728L20.2971 16.297C20.5314 16.0627 20.9113 16.0627 21.1456 16.297C21.3799 16.5314 21.3799 16.9113 21.1456 17.1456L18.1456 20.1456Z" fill="currentColor"/>
3
+ </svg>
4
+ </template>
@@ -0,0 +1,4 @@
1
+ <template><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M12 4.3999C13.9882 4.3999 15.6 6.01168 15.6 7.9999V10.0489C16.033 10.1018 16.3329 10.2114 16.5607 10.4392C17 10.8786 17 11.5857 17 12.9999V14.9999C17 16.4141 17 17.1212 16.5607 17.5606C16.1213 17.9999 15.4142 17.9999 14 17.9999H10C8.58579 17.9999 7.87868 17.9999 7.43934 17.5606C7 17.1212 7 16.4141 7 14.9999V12.9999C7 11.5857 7 10.8786 7.43934 10.4392C7.66715 10.2114 7.96695 10.1018 8.4 10.0489L8.4 7.9999C8.4 6.01168 10.0118 4.3999 12 4.3999ZM14.4 7.9999V10.0003C14.2733 9.99991 14.1401 9.99991 14 9.99991H10C9.85987 9.99991 9.72668 9.99991 9.6 10.0003L9.6 7.9999C9.6 6.67442 10.6745 5.5999 12 5.5999C13.3255 5.5999 14.4 6.67442 14.4 7.9999ZM12.6 12.9999C12.6 12.6685 12.3314 12.3999 12 12.3999C11.6686 12.3999 11.4 12.6685 11.4 12.9999V14.9999C11.4 15.3313 11.6686 15.5999 12 15.5999C12.3314 15.5999 12.6 15.3313 12.6 14.9999V12.9999Z" fill="currentColor"/>
3
+ </svg>
4
+ </template>