@itfin/components 2.0.17 → 2.0.19

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itfin/components",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
4
4
  "author": "Vitalii Savchuk <esvit666@gmail.com>",
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -129,7 +129,7 @@ class itfDateGranularityPicker extends Vue {
129
129
  @Prop({ type: [String, Date], default: ''}) maxDate;
130
130
  @Prop(Boolean) disabled;
131
131
 
132
- granularity = 'yearly';
132
+ granularity = 'monthly';
133
133
 
134
134
  get granularities() {
135
135
  return [
@@ -137,6 +137,7 @@ class itfDateGranularityPicker extends Vue {
137
137
  { title: 'Quarterly', value: 'quarterly' },
138
138
  { title: 'Monthly', value: 'monthly' },
139
139
  { title: 'Weekly', value: 'weekly' },
140
+ { title: 'Daily', value: 'daily' },
140
141
  { title: 'Custom', value: 'manual' },
141
142
  ];
142
143
  }
@@ -11,85 +11,27 @@
11
11
  :placeholder="placeholder"
12
12
  />
13
13
  <div style="display: none">
14
- <div ref="dropdown" class="itf-periodpicker__dropdown">
15
- <div class="row mb-3">
16
- <div class="col">
17
- <itf-button @click="setYear(prevYear)">
18
- <itf-icon name="chevron_left" />
19
- </itf-button>
20
- </div>
21
- <div class="col">
22
- <itf-button block @click="setYear(prevYear)">
23
- {{prevYear}}
24
- </itf-button>
25
- </div>
26
- <div class="col">
27
- <itf-button color="outline-primary" block>
28
- {{year}}
29
- </itf-button>
30
- </div>
31
- <div class="col">
32
- <itf-button block @click="setYear(nextYear)">
33
- {{nextYear}}
34
- </itf-button>
35
- </div>
36
- <div class="col text-end">
37
- <itf-button @click="setYear(nextYear)">
38
- <itf-icon name="chevron_right" />
39
- </itf-button>
40
- </div>
41
- </div>
42
-
43
- <itf-button block class="mb-3" :class="{'btn-whole': !isCurrentYear()}" :primary="isCurrentYear()" @click="onYearSelect(year)">
44
- {{ $t('components.wholeYear') }}
45
- </itf-button>
46
-
47
- <div class="itf-periodpicker__quarters">
48
- <div
49
- v-for="quarter of quarters"
50
- :key="quarter.Number"
51
- class="itf-periodpicker__quarter"
52
- :class="{ 'active': isOnlyQuarter(quarter.Number), 'active-inside': isCurrentQuarter(quarter.Number) }">
53
- <div class="px-3 pt-2" @click="onQuarterSelect([quarter.Months[0], quarter.Months[2]])">
54
- <small><b>QUARTER</b></small><br>
55
- <span class="quarter-number">{{ quarter.Number }}</span>
56
- </div>
57
-
58
- <div class="itf-periodpicker__months">
59
- <itf-button
60
- class="itf-periodpicker__month px-1"
61
- v-for="month of quarter.Months"
62
- :key="month"
63
- :primary="isCurrentMonth(month)"
64
- @click="onMonthSelect(month)"
65
- >
66
- {{ month | formatMonth }}
67
- </itf-button>
68
- </div>
69
- </div>
70
- </div>
71
- </div>
14
+ <itf-period-picker-inline
15
+ ref="dropdown"
16
+ class="itf-periodpicker__dropdown"
17
+ v-model="value"
18
+ :value-format="valueFormat"
19
+ @input="onInput"
20
+ />
72
21
  </div>
73
22
  </div>
74
23
  </template>
75
24
  <script>
76
25
  import { Vue, Component, Prop, Inject } from 'vue-property-decorator';
77
- import { DateTime } from 'luxon';
78
26
  import tippy from 'tippy.js';
79
- import itfIcon from '../icon/Icon';
80
- import itfButton from '../button/Button';
27
+ import itfPeriodPickerInline from "./PeriodPickerInline.vue";
28
+ import {DateTime} from "luxon";
81
29
  import ITFSettings from '../../ITFSettings';
82
30
 
83
31
  export default @Component({
84
32
  name: 'itfPeriodPicker',
85
33
  components: {
86
- itfIcon,
87
- itfButton
88
- },
89
- filters: {
90
- formatMonth(month) {
91
- return DateTime.local().set({ month }).toFormat('MMM');
92
- }
34
+ itfPeriodPickerInline
93
35
  }
94
36
  })
95
37
  class itfPeriodPicker extends Vue {
@@ -97,51 +39,12 @@ class itfPeriodPicker extends Vue {
97
39
 
98
40
  @Prop({ type: Array }) value;
99
41
  @Prop({ type: String, default: 'ISO' }) valueFormat;
100
- @Prop({ type: String }) displayFormat;
101
42
  @Prop({ type: String, default: 'bottom-start' }) placement;
102
- @Prop({ type: String, default: 'days', validator: (value) => ['days', 'months', 'years'].includes(value) }) startView;
103
- @Prop({ type: Boolean, default: false }) onlyCalendar;
104
43
  @Prop({ type: String, default: '' }) placeholder;
105
-
106
- year = null;
44
+ @Prop({ type: String }) displayFormat;
107
45
 
108
46
  focused = false;
109
47
 
110
- tooltip = null;
111
-
112
- get quarters() {
113
- return [
114
- { Name: `${this.$t('components.quarter')} 1`, Months: [1, 2, 3], Number: 1 },
115
- { Name: `${this.$t('components.quarter')} 2`, Months: [4, 5, 6], Number: 2 },
116
- { Name: `${this.$t('components.quarter')} 3`, Months: [7, 8, 9], Number: 3 },
117
- { Name: `${this.$t('components.quarter')} 4`, Months: [10, 11, 12], Number: 4 },
118
- ];
119
- }
120
-
121
- get dateFormat() {
122
- return this.displayFormat || ITFSettings.defaultDisplayDateFormat;
123
- }
124
-
125
- get nextYear() {
126
- return this.year + 1;
127
- }
128
-
129
- get prevYear() {
130
- return this.year - 1;
131
- }
132
-
133
- setYear(year) {
134
- this.year = year;
135
- }
136
-
137
- isInvalid() {
138
- return this.itemLabel && this.itemLabel.isHasError();
139
- }
140
-
141
- isSuccess() {
142
- return this.itemLabel && this.itemLabel.isHasSuccess();
143
- }
144
-
145
48
  mounted() {
146
49
  const context = this.$el.closest('.itf-append-context') || document.body;
147
50
  this.tooltip = tippy(this.$refs.input, {
@@ -149,30 +52,17 @@ class itfPeriodPicker extends Vue {
149
52
  interactiveDebounce: 75,
150
53
  animation: 'scale',
151
54
  arrow: true,
152
- content: this.$refs.dropdown,
55
+ content: this.$refs.dropdown.$el,
153
56
  allowHTML: true,
154
57
  trigger: 'click',
155
58
  interactive: true,
156
59
  placement: this.placement,
157
60
  appendTo: context,
158
61
  });
159
- this.year = (this.value && this.value[0]) ? this.valueAsLuxon[0].year : DateTime.local().year;
160
62
  }
161
63
 
162
- get valueAsLuxon() {
163
- if (!this.value || this.value.length < 2) {
164
- return null;
165
- }
166
- if (this.valueFormat === 'ISO') {
167
- return [
168
- DateTime.fromISO(this.value[0]),
169
- DateTime.fromISO(this.value[1]),
170
- ];
171
- }
172
- return [
173
- DateTime.fromFormat(this.value[0], this.valueFormat),
174
- DateTime.fromFormat(this.value[1], this.valueFormat),
175
- ];
64
+ get dateFormat() {
65
+ return this.displayFormat || ITFSettings.defaultDisplayDateFormat;
176
66
  }
177
67
 
178
68
  get displayText() {
@@ -183,12 +73,29 @@ class itfPeriodPicker extends Vue {
183
73
  if (!this.valueAsLuxon || this.valueAsLuxon.length < 2) {
184
74
  return [];
185
75
  }
76
+ console.info(this.valueAsLuxon);
186
77
  return [
187
78
  this.valueAsLuxon[0].toFormat(this.dateFormat),
188
79
  this.valueAsLuxon[1].toFormat(this.dateFormat)
189
80
  ];
190
81
  }
191
82
 
83
+ get valueAsLuxon() {
84
+ if (!this.value || this.value.length < 2) {
85
+ return null;
86
+ }
87
+ if (this.valueFormat === 'ISO') {
88
+ return [
89
+ DateTime.fromISO(this.value[0]),
90
+ DateTime.fromISO(this.value[1]),
91
+ ];
92
+ }
93
+ return [
94
+ DateTime.fromFormat(this.value[0], this.valueFormat),
95
+ DateTime.fromFormat(this.value[1], this.valueFormat),
96
+ ];
97
+ }
98
+
192
99
  onFocus() {
193
100
  this.focused = true;
194
101
  }
@@ -197,71 +104,21 @@ class itfPeriodPicker extends Vue {
197
104
  this.focused = false;
198
105
  }
199
106
 
200
- selectInlineDate(date) {
201
- this.$emit('input', date);
202
- this.tooltip.hide();
203
- }
204
-
205
- isCurrentMonth(month) {
206
- if (!this.valueAsLuxon) {
207
- return false;
208
- }
209
- return Number(this.valueAsLuxon[0].toFormat('M')) === month && Number(this.valueAsLuxon[1].toFormat('M')) === month;
210
- }
211
-
212
- isOnlyQuarter(quarter) {
213
- if (!this.valueAsLuxon) {
214
- return false;
215
- }
216
- return this.isCurrentQuarter(quarter) && this.valueAsLuxon[0].month !== this.valueAsLuxon[1].month;
107
+ isInvalid() {
108
+ return this.itemLabel && this.itemLabel.isHasError();
217
109
  }
218
110
 
219
- isCurrentQuarter(quarter) {
220
- if (!this.valueAsLuxon) {
221
- return false;
222
- }
223
- return this.valueAsLuxon[0].quarter === quarter && this.valueAsLuxon[1].quarter === quarter;
111
+ isSuccess() {
112
+ return this.itemLabel && this.itemLabel.isHasSuccess();
224
113
  }
225
114
 
226
- isCurrentYear() {
227
- if (!this.valueAsLuxon) {
228
- return false;
229
- }
230
- return this.valueAsLuxon[0].hasSame(this.valueAsLuxon[0].startOf('year'), 'day')
231
- && this.valueAsLuxon[1].hasSame(this.valueAsLuxon[0].endOf('year'), 'day');
115
+ selectInlineDate(date) {
116
+ this.$emit('input', date);
232
117
  }
233
118
 
234
- updateValue(start, end) {
235
- if (!start) {
236
- this.$emit('input', []);
237
- return;
238
- }
239
- this.$emit('input', [
240
- (this.valueFormat === 'ISO') ? start.toISO() : start.toFormat(this.valueFormat),
241
- (this.valueFormat === 'ISO') ? end.toISO() : end.toFormat(this.valueFormat),
242
- ]);
119
+ onInput(val) {
120
+ this.$emit('input', val);
243
121
  this.tooltip.hide();
244
122
  }
245
-
246
- onYearSelect(year) {
247
- this.updateValue(
248
- DateTime.local().set({ year }).startOf('year'),
249
- DateTime.local().set({ year }).endOf('year'),
250
- );
251
- }
252
-
253
- onMonthSelect(month) {
254
- this.updateValue(
255
- DateTime.local().set({ month, year: this.year }).startOf('month'),
256
- DateTime.local().set({ month, year: this.year }).endOf('month'),
257
- );
258
- }
259
-
260
- onQuarterSelect([startMonth, endMonth]) {
261
- this.updateValue(
262
- DateTime.local().set({ month: startMonth, year: this.year }).startOf('quarter'),
263
- DateTime.local().set({ month: endMonth, year: this.year }).endOf('quarter'),
264
- );
265
- }
266
123
  }
267
124
  </script>
@@ -0,0 +1,187 @@
1
+ <template>
2
+ <div>
3
+ <div class="row mb-3">
4
+ <div class="col">
5
+ <itf-button icon @click="setYear(prevYear)">
6
+ <itf-icon name="chevron_left" />
7
+ </itf-button>
8
+ </div>
9
+ <div class="col">
10
+ <itf-button block @click="setYear(prevYear)">
11
+ {{prevYear}}
12
+ </itf-button>
13
+ </div>
14
+ <div class="col">
15
+ <itf-button color="outline-primary" block>
16
+ {{year}}
17
+ </itf-button>
18
+ </div>
19
+ <div class="col">
20
+ <itf-button block @click="setYear(nextYear)">
21
+ {{nextYear}}
22
+ </itf-button>
23
+ </div>
24
+ <div class="col text-end">
25
+ <itf-button icon @click="setYear(nextYear)">
26
+ <itf-icon name="chevron_right" />
27
+ </itf-button>
28
+ </div>
29
+ </div>
30
+
31
+ <itf-button block class="mb-3" :class="{'btn-whole': !isCurrentYear()}" :primary="isCurrentYear()" @click="onYearSelect(year)">
32
+ {{ $t('components.wholeYear') }}
33
+ </itf-button>
34
+
35
+ <div class="itf-periodpicker__quarters">
36
+ <div
37
+ v-for="quarter of quarters"
38
+ :key="quarter.Number"
39
+ class="itf-periodpicker__quarter"
40
+ :class="{ 'active': isOnlyQuarter(quarter.Number), 'active-inside': isCurrentQuarter(quarter.Number) }">
41
+ <div class="px-3 pt-2" @click="onQuarterSelect([quarter.Months[0], quarter.Months[2]])">
42
+ <small><b>{{ $t('components.quarter') }}</b></small><br>
43
+ <span class="quarter-number">{{ quarter.Number }}</span>
44
+ </div>
45
+
46
+ <div class="itf-periodpicker__months">
47
+ <itf-button
48
+ class="itf-periodpicker__month px-1"
49
+ v-for="month of quarter.Months"
50
+ :key="month"
51
+ :primary="isCurrentMonth(month)"
52
+ @click="onMonthSelect(month)"
53
+ >
54
+ {{ month | formatMonth }}
55
+ </itf-button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </template>
61
+ <script>
62
+ import { Vue, Component, Prop } from 'vue-property-decorator';
63
+ import { DateTime } from 'luxon';
64
+ import itfIcon from '../icon/Icon';
65
+ import itfButton from '../button/Button';
66
+
67
+ export default @Component({
68
+ name: 'itfPeriodPickerInline',
69
+ components: {
70
+ itfIcon,
71
+ itfButton
72
+ },
73
+ filters: {
74
+ formatMonth(month) {
75
+ return DateTime.local().set({ month }).toFormat('MMM');
76
+ }
77
+ }
78
+ })
79
+ class itfPeriodPickerInline extends Vue {
80
+ @Prop({ type: Array }) value;
81
+ @Prop({ type: String, default: 'ISO' }) valueFormat;
82
+
83
+ year = null;
84
+
85
+ get quarters() {
86
+ return [
87
+ { Name: `${this.$t('components.quarter')} 1`, Months: [1, 2, 3], Number: 1 },
88
+ { Name: `${this.$t('components.quarter')} 2`, Months: [4, 5, 6], Number: 2 },
89
+ { Name: `${this.$t('components.quarter')} 3`, Months: [7, 8, 9], Number: 3 },
90
+ { Name: `${this.$t('components.quarter')} 4`, Months: [10, 11, 12], Number: 4 },
91
+ ];
92
+ }
93
+
94
+ get nextYear() {
95
+ return this.year + 1;
96
+ }
97
+
98
+ get prevYear() {
99
+ return this.year - 1;
100
+ }
101
+
102
+ setYear(year) {
103
+ this.year = year;
104
+ }
105
+
106
+ mounted() {
107
+ this.year = (this.value && this.value[0]) ? this.valueAsLuxon[0].year : DateTime.local().year;
108
+ }
109
+
110
+ get valueAsLuxon() {
111
+ if (!this.value || this.value.length < 2) {
112
+ return null;
113
+ }
114
+ if (this.valueFormat === 'ISO') {
115
+ return [
116
+ DateTime.fromISO(this.value[0]),
117
+ DateTime.fromISO(this.value[1]),
118
+ ];
119
+ }
120
+ return [
121
+ DateTime.fromFormat(this.value[0], this.valueFormat),
122
+ DateTime.fromFormat(this.value[1], this.valueFormat),
123
+ ];
124
+ }
125
+
126
+ isCurrentMonth(month) {
127
+ if (!this.valueAsLuxon) {
128
+ return false;
129
+ }
130
+ return Number(this.valueAsLuxon[0].toFormat('M')) === month && Number(this.valueAsLuxon[1].toFormat('M')) === month;
131
+ }
132
+
133
+ isOnlyQuarter(quarter) {
134
+ if (!this.valueAsLuxon) {
135
+ return false;
136
+ }
137
+ return this.isCurrentQuarter(quarter) && this.valueAsLuxon[0].month !== this.valueAsLuxon[1].month;
138
+ }
139
+
140
+ isCurrentQuarter(quarter) {
141
+ if (!this.valueAsLuxon) {
142
+ return false;
143
+ }
144
+ return this.valueAsLuxon[0].quarter === quarter && this.valueAsLuxon[1].quarter === quarter;
145
+ }
146
+
147
+ isCurrentYear() {
148
+ if (!this.valueAsLuxon) {
149
+ return false;
150
+ }
151
+ return this.valueAsLuxon[0].hasSame(this.valueAsLuxon[0].startOf('year'), 'day')
152
+ && this.valueAsLuxon[1].hasSame(this.valueAsLuxon[0].endOf('year'), 'day');
153
+ }
154
+
155
+ updateValue(start, end) {
156
+ if (!start) {
157
+ this.$emit('input', []);
158
+ return;
159
+ }
160
+ this.$emit('input', [
161
+ (this.valueFormat === 'ISO') ? start.toISO() : start.toFormat(this.valueFormat),
162
+ (this.valueFormat === 'ISO') ? end.toISO() : end.toFormat(this.valueFormat),
163
+ ]);
164
+ }
165
+
166
+ onYearSelect(year) {
167
+ this.updateValue(
168
+ DateTime.local().set({ year }).startOf('year'),
169
+ DateTime.local().set({ year }).endOf('year'),
170
+ );
171
+ }
172
+
173
+ onMonthSelect(month) {
174
+ this.updateValue(
175
+ DateTime.local().set({ month, year: this.year }).startOf('month'),
176
+ DateTime.local().set({ month, year: this.year }).endOf('month'),
177
+ );
178
+ }
179
+
180
+ onQuarterSelect([startMonth, endMonth]) {
181
+ this.updateValue(
182
+ DateTime.local().set({ month: startMonth, year: this.year }).startOf('quarter'),
183
+ DateTime.local().set({ month: endMonth, year: this.year }).endOf('quarter'),
184
+ );
185
+ }
186
+ }
187
+ </script>
@@ -2,6 +2,7 @@ import { storiesOf } from '@storybook/vue';
2
2
  import itfDatePicker from './DatePicker.vue';
3
3
  import itfMonthPicker from './MonthPicker.vue';
4
4
  import itfPeriodPicker from './PeriodPicker';
5
+ import itfPeriodPickerInline from './PeriodPickerInline';
5
6
  import itfDatePickerInline from './DatePickerInline.vue';
6
7
  import itfDateRangePicker from './DateRangePicker.vue';
7
8
  import itfDateRangePickerInline from './DateRangePickerInline.vue';
@@ -13,6 +14,7 @@ storiesOf('Common', module)
13
14
  itfApp,
14
15
  itfDatePicker,
15
16
  itfPeriodPicker,
17
+ itfPeriodPickerInline,
16
18
  itfMonthPicker,
17
19
  itfDateRangePicker,
18
20
  itfDatePickerInline,
@@ -86,6 +88,8 @@ storiesOf('Common', module)
86
88
  <h2>Period</h2>
87
89
 
88
90
  <itf-period-picker :value="dateRange" v-model.lazy="dateRange"></itf-period-picker>
91
+
92
+ <itf-period-picker-inline :value="dateRange" v-model.lazy="dateRange"></itf-period-picker-inline>
89
93
  </itf-app>
90
94
  </div>`,
91
95
  }));
@@ -29,6 +29,13 @@
29
29
  @input="onFilterChange({ value: $event })"
30
30
  />
31
31
  </template>
32
+ <template v-else-if="type === 'timeframe'">
33
+ <itf-period-picker-inline
34
+ :value="value.value"
35
+ value-format="yyyy-MM-dd"
36
+ @input="onFilterChange({ value: $event })"
37
+ />
38
+ </template>
32
39
  <template v-else-if="type === 'date'">
33
40
  <itf-date-picker-inline
34
41
  style="margin: -.5rem"
@@ -61,6 +68,7 @@
61
68
  <style lang="scss">
62
69
  :root {
63
70
  --filter-badge__default-color: #475266;
71
+ --filter-badge__icon-color: #A7AFBB;
64
72
  --filter-badge__default-border-color: #0000001A;
65
73
  --filter-badge__default-bg-color: transparent;
66
74
  --filter-badge__default-bg-color-hover: #1A4A970D;
@@ -75,6 +83,10 @@
75
83
  --filter-badge__padding-y: .5rem;
76
84
  --filter-badge__padding-x: .75rem;
77
85
  }
86
+ body[data-theme="dark"] {
87
+ --filter-badge__default-border-color: #FFFFFF1A;
88
+ --filter-badge__selected-color: #efd877;
89
+ }
78
90
  .filter-pill {
79
91
  align-items: center;
80
92
  font-size: 14px;
@@ -87,6 +99,7 @@
87
99
 
88
100
  .icon {
89
101
  margin: -2px;
102
+ color: var(--filter-badge__icon-color);
90
103
  }
91
104
  &:hover {
92
105
  background-color: var(--filter-badge__default-bg-color-hover);
@@ -95,16 +108,20 @@
95
108
  &.filter-not-default-pill {
96
109
  background-color: var(--filter-badge__selected-bg-color);
97
110
  outline: 1px solid var(--filter-badge__selected-color);
111
+
112
+ .icon { color: var(--filter-badge__selected-color) }
98
113
  }
99
114
  &.filter-invalid-pill {
100
115
  background-color: var(--filter-badge__invalid-bg-color);
116
+
117
+ .icon { color: var(--filter-badge__invalid-color) }
101
118
  }
102
119
  &.filter-pill__default-value {
103
120
  padding: var(--filter-badge__padding-y) var(--filter-badge__padding-x);
104
121
  }
105
122
  }
106
123
  .filter-pill__label {
107
- color: var(--filter-badge__default-color);
124
+ //color: var(--filter-badge__default-color);
108
125
  padding: var(--filter-badge__padding-y) 0 var(--filter-badge__padding-y) var(--filter-badge__padding-x);
109
126
  max-width: 330px;
110
127
  text-overflow: ellipsis;
@@ -129,7 +146,7 @@
129
146
  padding: 0 calc(var(--filter-badge__padding-x) / 2) 0 calc(var(--filter-badge__padding-x) / 4);
130
147
 
131
148
  svg {
132
- color: var(--filter-badge__default-color);
149
+ //color: var(--filter-badge__default-color);
133
150
  }
134
151
  &.filter-pill__icon-invalid svg {
135
152
  color: var(--filter-badge__invalid-color);
@@ -146,6 +163,7 @@ import itfButton from '../button/Button';
146
163
  import itfDropdown from '../dropdown/Dropdown.vue';
147
164
  import itfDatePickerInline from '../datepicker/DatePickerInline.vue';
148
165
  import itfDateRangePickerInline from '../datepicker/DateRangePickerInline.vue';
166
+ import itfPeriodPickerInline from '../datepicker/PeriodPickerInline.vue'
149
167
  import itfTextField from '../text-field/TextField.vue';
150
168
  import FilterFacetsList from './FilterFacetsList';
151
169
  import FilterAmountRange from './FilterAmountRange.vue';
@@ -157,6 +175,7 @@ export default @Component({
157
175
  itfDropdown,
158
176
  itfDatePickerInline,
159
177
  itfDateRangePickerInline,
178
+ itfPeriodPickerInline,
160
179
  itfTextField,
161
180
  FilterFacetsList,
162
181
  FilterAmountRange
@@ -19,16 +19,18 @@
19
19
  <div class="text-muted text-center py-4">{{ $t('components.filter.noResults') }}</div>
20
20
  </div>
21
21
  </div>
22
- <div v-for="(val, n) of mappedValues" :key="n" class="dropdown-item" :class="{'active': val.isSelected}" @click="onFilterClick(val)">
22
+ <div class="facets-list">
23
+ <div v-for="(val, n) of mappedValues" :key="n" class="dropdown-item px-2" :class="{'active': val.isSelected}" @click="onFilterClick(val)">
23
24
  <span class="facet-name text-dark d-flex align-items-center">
24
25
  <itf-checkbox ungrouped :value="val.isSelected" class="m-0" />
25
- <div class="w-100 text-truncate">{{ val.label }}</div>
26
+ <div class="w-100 text-truncate">{{ val.label }} <span v-if="val.description" class="small"><br/>{{ val.description }}</span></div>
26
27
  </span>
27
- <span class="facet-stat text-muted">
28
+ <span v-if="val.count" class="facet-stat">
28
29
  {{ val.count }}
29
30
  <span class="facet-bar"><span :style="{'--bar-width': `${getPercent(val)}%`}" class="facet-bar-progress" /></span>
30
31
  </span>
31
32
  </div>
33
+ </div>
32
34
 
33
35
  <itf-button default class="mt-1" v-if="hasMore" small block @click="toggleMore">
34
36
  <span v-if="showMore">{{ $t('components.filter.hideMore', { count: visibleList.length }) }}</span>
@@ -43,7 +45,13 @@
43
45
  padding: 0 0.75rem .5rem;
44
46
  margin: 0 -.75rem .75rem;
45
47
  }
48
+ .facets-list {
49
+ max-height: 50vh;
50
+ overflow: auto;
51
+ }
46
52
  .dropdown-item {
53
+ --bs-dropdown-link-active-bg: rgba(var(--bs-primary-rgb), .25);
54
+
47
55
  cursor: pointer;
48
56
  display: inline-flex;
49
57
  -webkit-box-align: center;
@@ -52,7 +60,7 @@
52
60
  justify-content: space-between;
53
61
  position: relative;
54
62
  box-sizing: border-box;
55
- height: 1.75rem;
63
+ min-height: 1.75rem;
56
64
  width: 100%;
57
65
  font-size: 0.875rem;
58
66
  line-height: 1.25rem;
@@ -74,10 +82,11 @@
74
82
  .facet-name {
75
83
  min-width: 0;
76
84
  text-align: left;
85
+ line-height: 100%;
77
86
  white-space: nowrap;
78
87
 
79
88
  .itf-checkbox {
80
- min-height: auto;
89
+ min-height: 1.25rem;
81
90
  }
82
91
  }
83
92
  .facet-stat {
@@ -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;
@@ -145,7 +145,7 @@ class itfMoneyField extends Vue {
145
145
  }
146
146
 
147
147
  onCurrencyChanged(e) {
148
- const currency = this.currenciesList.find((c) => c[this.itemKey] === parseInt(e.target.value));
148
+ const currency = this.currenciesList.find((c) => `${c[this.itemKey]}` === `${e.target.value}`);
149
149
  this.$emit('update:currency', currency)
150
150
  }
151
151