@itfin/components 2.0.47 → 2.0.49

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.47",
3
+ "version": "2.0.49",
4
4
  "author": "Vitalii Savchuk <esvit666@gmail.com>",
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -20,16 +20,26 @@
20
20
  </div>
21
21
  </div>
22
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)">
24
- <span class="facet-name text-dark d-flex align-items-center">
25
- <itf-checkbox ungrouped :value="val.isSelected" class="m-0" />
26
- <div class="w-100 text-truncate">{{ val.label }} <span v-if="val.description" class="small"><br/>{{ val.description }}</span></div>
27
- </span>
28
- <span v-if="val.count" class="facet-stat">
29
- {{ val.count }}
30
- <span class="facet-bar"><span :style="{'--bar-width': `${getPercent(val)}%`}" class="facet-bar-progress" /></span>
31
- </span>
32
- </div>
23
+ <div v-for="(group, g) of groupedList">
24
+ <div v-if="group.group" class="dropdown-item ps-1 d-flex align-items-center"
25
+ :class="{'active': isGroupSelected(group.items)}" @click="groupSelected(!isGroupSelected(group.items), group.items)">
26
+ <span class="facet-name text-dark d-flex align-items-center">
27
+ <itf-checkbox ungrouped :value="isGroupSelected(group.items)" @input="groupSelected($event, group.items)" class="m-0" />
28
+ <div class="w-100 text-truncate">{{ group.group }}</div>
29
+ </span>
30
+ </div>
31
+
32
+ <div v-for="(val, n) of group.items" :key="n" class="dropdown-item ps-1" :class="{'active': val.isSelected, 'ps-4': group.group}" @click="onFilterClick(val)">
33
+ <span class="facet-name text-dark d-flex align-items-center">
34
+ <itf-checkbox ungrouped :value="val.isSelected" class="m-0" />
35
+ <div class="w-100 text-truncate">{{ val.label }} <span v-if="val.description" class="small"><br/>{{ val.description }}</span></div>
36
+ </span>
37
+ <span v-if="val.count" class="facet-stat">
38
+ {{ val.count }}
39
+ <span class="facet-bar"><span :style="{'--bar-width': `${getPercent(val)}%`}" class="facet-bar-progress" /></span>
40
+ </span>
41
+ </div>
42
+ </div>
33
43
  </div>
34
44
 
35
45
  <itf-button default class="mt-1" v-if="hasMore" small block @click="toggleMore">
@@ -111,6 +121,8 @@
111
121
  }
112
122
  </style>
113
123
  <script>
124
+ import uniq from 'lodash/uniq';
125
+ import sortBy from 'lodash/sortBy';
114
126
  import { Vue, Prop, Model, Component } from 'vue-property-decorator';
115
127
  import itfTextField from '../text-field/TextField.vue';
116
128
  import itfButton from '../button/Button';
@@ -149,6 +161,33 @@ class FilterFacetsList extends Vue {
149
161
  return this.visibleList.length > this.limit;
150
162
  }
151
163
 
164
+ get hasGroups() {
165
+ const groups = uniq(this.items && this.items.map(item => item.group).filter(Boolean));
166
+ return groups.length > 1
167
+ }
168
+
169
+ isGroupSelected(items) {
170
+ return items.every(item => item.isSelected);
171
+ }
172
+
173
+ groupSelected(value, items) {
174
+ let newVal = this.value ? [...Array.isArray(this.value) ? this.value : [this.value]] : [];
175
+ if (value) {
176
+ items.forEach((item) => {
177
+ const itemValue = `${item.value}`;
178
+ if (!newVal.includes(itemValue)) {
179
+ newVal.push(itemValue);
180
+ }
181
+ });
182
+ } else {
183
+ items.forEach((item) => {
184
+ const itemValue = `${item.value}`;
185
+ newVal = newVal.filter(val => val !== itemValue);
186
+ });
187
+ }
188
+ this.$emit('input', this.multiple ? newVal : (newVal.length > 0 ? newVal[0] : null));
189
+ }
190
+
152
191
  get visibleList() {
153
192
  let list = this.items.map(val => {
154
193
  const isSelected = this.multiple
@@ -163,7 +202,22 @@ class FilterFacetsList extends Vue {
163
202
  if (this.isShowSelected) {
164
203
  return list.filter((val) => val.isSelected);
165
204
  }
166
- return list;
205
+ return sortBy(list, (item) => this.hasGroups ? item.group || item.label : item.label);
206
+ }
207
+
208
+ get groupedList() {
209
+ if (!this.hasGroups) {
210
+ return [{ items: this.mappedValues }];
211
+ }
212
+ const groups = {};
213
+ this.mappedValues.forEach((item) => {
214
+ const group = item.group || '';
215
+ if (!groups[group]) {
216
+ groups[group] = [];
217
+ }
218
+ groups[group].push(item);
219
+ });
220
+ return Object.entries(groups).map(([group, items]) => ({ group, items }));
167
221
  }
168
222
 
169
223
  get mappedValues() {
@@ -0,0 +1,305 @@
1
+ <template>
2
+ <div>
3
+ <div class="px-1">
4
+ <div class="facets-filter-header">
5
+ <div v-if="title">{{title}}</div>
6
+ </div>
7
+
8
+ <itf-text-field small v-model="query" class="mb-2" :placeholder="$t('components.filter.search')" clearable />
9
+
10
+ <div class="d-flex justify-content-between small mb-2">
11
+ <a v-if="isSelectedAll" href="" @click.stop.prevent="onSelectAll(false)">{{$t('components.filter.deselectAll')}}</a>
12
+ <a v-else href="" @click.stop.prevent="onSelectAll(true)">{{$t('components.filter.selectAll')}}</a>
13
+
14
+ <span class="text-muted" v-if="!isHasSelected" href="" @click.stop.prevent="onShowSelected(true)">{{$t('components.filter.showSelected')}}</span>
15
+ <a v-else-if="!isShowSelected" href="" @click.stop.prevent="onShowSelected(true)">{{$t('components.filter.showSelected')}}</a>
16
+ <a v-else href="" @click.stop.prevent="onShowSelected(false)">{{$t('components.filter.showAll')}}</a>
17
+ </div>
18
+ <div v-if="!mappedValues.length">
19
+ <div class="text-muted text-center py-4">{{ $t('components.filter.noResults') }}</div>
20
+ </div>
21
+ </div>
22
+ <div class="facets-list">
23
+ <div v-for="(val, g) of groupedList" :key="g" :style="{paddingLeft: `${((val.level || 0) + 0.5) * 1}rem`}"
24
+ class="dropdown-item" :class="{'active': val.isSelected, 'active': isGroupSelected(val)}" @click="onFilterClick(val)">
25
+ <span class="facet-name text-dark d-flex align-items-center">
26
+ <itf-checkbox ungrouped :value="val.isSelected" class="m-0" />
27
+ <itf-icon new v-if="val.isGroup && groupIcon" :name="groupIcon" class="me-1" />
28
+ <div class="w-100 text-truncate">{{ val.label }} <span v-if="val.description" class="small"><br/>{{ val.description }}</span></div>
29
+ </span>
30
+ <span v-if="val.count" class="facet-stat">
31
+ {{ val.count }}
32
+ <span class="facet-bar"><span :style="{'--bar-width': `${getPercent(val)}%`}" class="facet-bar-progress" /></span>
33
+ </span>
34
+ </div>
35
+ </div>
36
+
37
+ <itf-button default class="mt-1" v-if="hasMore" small block @click="toggleMore">
38
+ <span v-if="showMore">{{ $t('components.filter.hideMore', { count: visibleList.length }) }}</span>
39
+ <span v-else>{{ $t('components.filter.showMore', { count: visibleList.length }) }}</span>
40
+ </itf-button>
41
+ </div>
42
+ </template>
43
+ <style lang="scss" scoped>
44
+ .facets-filter-header {
45
+ border-bottom: 1px solid var(--bs-border-color-translucent);
46
+ color: #A5A5A9;
47
+ padding: 0 0.75rem .5rem;
48
+ margin: 0 -.75rem .75rem;
49
+ }
50
+ .facets-list {
51
+ max-height: 50vh;
52
+ overflow: auto;
53
+ }
54
+ .dropdown-item {
55
+ --bs-dropdown-link-active-bg: rgba(var(--bs-primary-rgb), .25);
56
+
57
+ cursor: pointer;
58
+ display: inline-flex;
59
+ -webkit-box-align: center;
60
+ align-items: center;
61
+ -webkit-box-pack: justify;
62
+ justify-content: space-between;
63
+ position: relative;
64
+ box-sizing: border-box;
65
+ min-height: 1.75rem;
66
+ width: 100%;
67
+ font-size: 0.875rem;
68
+ line-height: 1.25rem;
69
+ font-weight: 400;
70
+ white-space: normal;
71
+ user-select: none;
72
+ border-radius: 0.25rem;
73
+ border-width: 1px;
74
+ border-style: solid;
75
+ border-color: transparent;
76
+ border-image: initial;
77
+ transition: none 0s ease 0s;
78
+ margin: 1px 0;
79
+ &.active {
80
+ .facet-bar-progress {
81
+ background-color: var(--bs-blue);
82
+ }
83
+ }
84
+ .facet-name {
85
+ min-width: 0;
86
+ text-align: left;
87
+ line-height: 100%;
88
+ white-space: nowrap;
89
+
90
+ .itf-checkbox {
91
+ min-height: 1.25rem;
92
+ }
93
+ }
94
+ .facet-stat {
95
+ display: flex;
96
+ -webkit-box-align: center;
97
+ align-items: center;
98
+ margin-left: 0.25rem;
99
+ }
100
+ .facet-bar {
101
+ display: inline-block;
102
+ margin-left: 0.5rem;
103
+ width: 60px;
104
+ }
105
+ .facet-bar-progress {
106
+ display: block;
107
+ width: var(--bar-width);
108
+ min-width: 5px;
109
+ height: 10px;
110
+ background-color: rgba(var(--bs-blue-rgb), 50%);
111
+ transition: width 0.3s ease 0s;
112
+ }
113
+ }
114
+ </style>
115
+ <script>
116
+ import uniq from 'lodash/uniq';
117
+ import sortBy from 'lodash/sortBy';
118
+ import groupBy from 'lodash/groupBy';
119
+ import { Vue, Prop, Model, Component } from 'vue-property-decorator';
120
+ import itfTextField from '../text-field/TextField.vue';
121
+ import itfIcon from '../icon/Icon';
122
+ import itfButton from '../button/Button';
123
+ import itfCheckbox from '../checkbox/Checkbox.vue';
124
+
125
+ export default @Component({
126
+ name: 'FilterFacetsList',
127
+ components: {
128
+ itfCheckbox,
129
+ itfIcon,
130
+ itfButton,
131
+ itfTextField
132
+ }
133
+ })
134
+ class FilterFacetsList extends Vue {
135
+ @Model('input') value;
136
+ @Prop() items;
137
+ @Prop() item;
138
+ @Prop() total;
139
+ @Prop() options;
140
+ @Prop({ type: Number, default: 5 }) limit;
141
+ @Prop(Boolean) multiple;
142
+ @Prop(Boolean) showAll;
143
+ @Prop(String) title;
144
+
145
+ query = '';
146
+ showMore = false;
147
+ isShowSelected = false;
148
+
149
+ toggleMore() {
150
+ this.showMore = !this.showMore;
151
+ }
152
+
153
+ get groupIcon() {
154
+ return this.options?.groupIcon;
155
+ }
156
+
157
+ get flatList() {
158
+ return treeToFlat(this.items || []);
159
+
160
+ function getIdsDeep(items, mapFunc) {
161
+ return (items ?? []).reduce((acc, item) => {
162
+ const id = mapFunc(item);
163
+ if (id) {
164
+ acc.push(id);
165
+ }
166
+ if (item.items && item.items.length) {
167
+ acc.push(...getIdsDeep(item.items, mapFunc));
168
+ }
169
+ return acc;
170
+ }, []);
171
+ }
172
+ function treeToFlat(items, level = 0) {
173
+ return (items ?? []).reduce((acc, item) => {
174
+ acc.push({
175
+ ...item,
176
+ level,
177
+ ids: [item.isGroup ? `g:${item.value}` : item.value].concat(
178
+ getIdsDeep(item.items, (item) => item.isGroup ? `g:${item.value}` : item.value)
179
+ )
180
+ .filter(Boolean),
181
+ });
182
+ if (item.items && item.items.length) {
183
+ // acc.push({ ...item, group: item.label, level });
184
+ acc.push(...treeToFlat(item.items, level + 1));
185
+ }
186
+ return acc;
187
+ }, []);
188
+ }
189
+ }
190
+
191
+ get hasMore() {
192
+ if (this.showAll) {
193
+ return false;
194
+ }
195
+ return this.visibleList.length > this.limit;
196
+ }
197
+
198
+ get hasGroups() {
199
+ const groups = uniq(this.flatList && this.flatList.map(item => item.group).filter(Boolean));
200
+ return groups.length > 1
201
+ }
202
+
203
+ isGroupSelected(val) {
204
+ return (val.ids ?? []).every(id => this.value.includes(`${id}`));
205
+ }
206
+
207
+ groupSelected(value, ids) {
208
+ let newVal = this.value ? [...Array.isArray(this.value) ? this.value : [this.value]] : [];
209
+ console.info(value, ids);
210
+ if (value) {
211
+ ids.forEach((id) => {
212
+ const itemValue = `${id}`;
213
+ if (!newVal.includes(itemValue)) {
214
+ newVal.push(itemValue);
215
+ }
216
+ });
217
+ } else {
218
+ ids.forEach((id) => {
219
+ const itemValue = `${id}`;
220
+ newVal = newVal.filter(val => val !== itemValue);
221
+ });
222
+ }
223
+ this.$emit('input', this.multiple ? newVal : (newVal.length > 0 ? newVal[0] : null));
224
+ }
225
+
226
+ get visibleList() {
227
+ let list = this.flatList.map(val => {
228
+ const isSelected = this.multiple
229
+ ? Array.isArray(this.value) && this.value.map(String).includes(`${val.value}`)
230
+ : `${this.value}` === `${val.value}`;
231
+
232
+ return { ...val, isSelected };
233
+ });
234
+ if (this.query) {
235
+ list = list.filter((val) => val.label.toLowerCase().includes(this.query.toLowerCase()));
236
+ }
237
+ if (this.isShowSelected) {
238
+ return list.filter((val) => val.isSelected);
239
+ }
240
+ return this.hasGroups ? sortBy(list, (item) => item.group || item.label) : list;//sortBy(list, (item) => this.hasGroups ? item.group || item.label : item.label);
241
+ }
242
+
243
+ get groupedList() {
244
+ if (!this.hasGroups) {
245
+ return this.mappedValues;
246
+ }
247
+ const groups = groupBy(this.mappedValues, (item) => item.group || '');
248
+ return Object.entries(groups).reduce((acc, [group, items]) => [...acc, { label: group, isGroup: true }, ...(items.map(item => ({ ...item, level: 1 })))], []);
249
+ }
250
+
251
+ get mappedValues() {
252
+ const list = this.visibleList;
253
+ if (!this.showMore && !this.showAll) {
254
+ return list.slice(0, this.limit);
255
+ }
256
+ return list;
257
+ }
258
+
259
+ getPercent(item) {
260
+ return this.total ? Math.round((item.count / this.total) * 100) : 0;
261
+ }
262
+
263
+ onFilterClick(val) {
264
+ if (val.isGroup) {
265
+ this.groupSelected(!val.isSelected, val.ids);
266
+ return;
267
+ }
268
+ const value = `${val.value}`;
269
+ if (!this.multiple) {
270
+ return this.$emit('input', `${this.value}` === value ? null : value);
271
+ }
272
+ const newVal = [...Array.isArray(this.value) ? [...this.value] : []].map((s) => s.toString());
273
+ if (newVal.includes(value)) {
274
+ newVal.splice(newVal.indexOf(value), 1);
275
+ } else {
276
+ newVal.push(value);
277
+ }
278
+ this.$emit('input', newVal);
279
+ }
280
+
281
+ get isSelectedAll() {
282
+ return this.mappedValues.every(o => o.isSelected);
283
+ }
284
+
285
+ onSelectAll(isSelect = false) {
286
+ if (isSelect) {
287
+ this.$emit('input', this.visibleList.map((val) => `${val.value}`));
288
+ } else {
289
+ this.$emit('input', []);
290
+ }
291
+ this.isShowSelected = false;
292
+ }
293
+
294
+ get isHasSelected() {
295
+ return this.value && this.value.length > 0 && !this.query; // тільки коли не пошук
296
+ }
297
+
298
+ onShowSelected(isShow = false) {
299
+ if (!this.value.length && isShow) { // не показувати, якщо нічого не вибрано
300
+ return;
301
+ }
302
+ this.isShowSelected = isShow;
303
+ }
304
+ }
305
+ </script>
@@ -33,6 +33,7 @@
33
33
  class="btn btn-lg btn-link fs-6 text-decoration-none col-6 py-3 m-0 rounded-0 border-end fw-bold"
34
34
  :class="confirmClass"
35
35
  @click="onConfirm"
36
+ :disabled="confirmDisabled"
36
37
  >
37
38
  <span v-html="deleteCaption"></span>
38
39
  </button>
@@ -58,6 +59,7 @@ export default @Component({
58
59
  class itfDeleteConfirmModal extends Vue {
59
60
  @Prop(Boolean) loading;
60
61
  @Prop({ type: Boolean, default: false }) disabled;
62
+ @Prop({ type: Boolean, default: false }) confirmDisabled;
61
63
  @Prop({ type: String, default: 'text-danger' }) confirmClass;
62
64
  @Prop({ type: String, default () { return this.$t('components.popover.noKeepIt'); } }) cancelCaption;
63
65
  @Prop({ type: String, default () { return this.$t('components.popover.yesDelete'); } }) deleteCaption;