@itfin/components 1.3.86 → 1.3.88
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 +1 -1
- package/src/assets/scss/components/_button.scss +0 -1
- package/src/assets/scss/components/_range.scss +276 -0
- package/src/assets/scss/main.scss +1 -0
- package/src/components/filter/FilterAmountRange.vue +92 -0
- package/src/components/filter/FilterBadge.vue +182 -0
- package/src/components/filter/FilterFacetsList.vue +206 -0
- package/src/components/filter/FilterPanel.vue +184 -0
- package/src/components/panels/PanelList.vue +36 -11
- package/src/components/range/Range.vue +1261 -0
- package/src/components/range/utils.js +74 -0
- package/src/components/text-field/TextField.vue +2 -1
- package/src/locales/en.js +10 -0
- package/src/locales/uk.js +10 -0
- package/src/components/filter/ConditionGroup.vue +0 -196
- package/src/components/filter/FacetFilter.vue +0 -138
- package/src/components/filter/FacetItem.vue +0 -162
- package/src/components/filter/FilterInput.vue +0 -64
- package/src/components/filter/FilterItem.vue +0 -423
- package/src/components/filter/constants.js +0 -20
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="px-1">
|
|
4
|
+
<div v-if="title" class="fw-bold mb-1">{{title}}</div>
|
|
5
|
+
|
|
6
|
+
<itf-text-field small v-model="query" class="mb-2" :placeholder="$t('components.filter.search')" clearable />
|
|
7
|
+
|
|
8
|
+
<div v-if="mappedValues.length" class="d-flex justify-content-between small mb-2">
|
|
9
|
+
<a v-if="isSelectedAll" href="" @click.stop.prevent="onSelectAll(false)">{{$t('components.filter.deselectAll')}}</a>
|
|
10
|
+
<a v-else href="" @click.stop.prevent="onSelectAll(true)">{{$t('components.filter.selectAll')}}</a>
|
|
11
|
+
|
|
12
|
+
<span class="text-muted" v-if="!isHasSelected" href="" @click.stop.prevent="onShowSelected(true)">{{$t('components.filter.showSelected')}}</span>
|
|
13
|
+
<a v-else-if="!isShowSelected" href="" @click.stop.prevent="onShowSelected(true)">{{$t('components.filter.showSelected')}}</a>
|
|
14
|
+
<a v-else href="" @click.stop.prevent="onShowSelected(false)">{{$t('components.filter.showSelected')}}</a>
|
|
15
|
+
</div>
|
|
16
|
+
<div v-else>
|
|
17
|
+
<div class="text-muted text-center py-4">{{ $t('components.filter.noResults') }}</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div v-for="(val, n) of mappedValues" :key="n" class="facet-item" :class="{'active': val.isSelected}" @click="onFilterClick(val)">
|
|
21
|
+
<span class="facet-name text-dark d-flex align-items-center">
|
|
22
|
+
<itf-checkbox ungrouped :value="val.isSelected" class="m-0" />
|
|
23
|
+
<div class="w-100 text-truncate">{{ val.label }}</div>
|
|
24
|
+
</span>
|
|
25
|
+
<span class="facet-stat text-muted">
|
|
26
|
+
{{ val.count }}
|
|
27
|
+
<span class="facet-bar"><span :style="{'--bar-width': `${getPercent(val)}%`}" class="facet-bar-progress" /></span>
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<itf-button class="mt-1" v-if="hasMore" small block @click="toggleMore">
|
|
32
|
+
<span v-if="showMore">{{ $t('components.filter.hideMore', { count: visibleList.length }) }}</span>
|
|
33
|
+
<span v-else>{{ $t('components.filter.showMore', { count: visibleList.length }) }}</span>
|
|
34
|
+
</itf-button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
<style lang="scss" scoped>
|
|
38
|
+
.facet-item {
|
|
39
|
+
background: transparent;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
-webkit-box-align: center;
|
|
43
|
+
align-items: center;
|
|
44
|
+
-webkit-box-pack: justify;
|
|
45
|
+
justify-content: space-between;
|
|
46
|
+
position: relative;
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
height: 1.75rem;
|
|
49
|
+
width: 100%;
|
|
50
|
+
padding-left: 0.25rem;
|
|
51
|
+
padding-right: 0.25rem;
|
|
52
|
+
font-size: 0.875rem;
|
|
53
|
+
line-height: 1.25rem;
|
|
54
|
+
font-weight: 400;
|
|
55
|
+
white-space: normal;
|
|
56
|
+
user-select: none;
|
|
57
|
+
border-radius: 0.25rem;
|
|
58
|
+
border-width: 1px;
|
|
59
|
+
border-style: solid;
|
|
60
|
+
border-color: transparent;
|
|
61
|
+
border-image: initial;
|
|
62
|
+
transition: none 0s ease 0s;
|
|
63
|
+
margin: 1px 0;
|
|
64
|
+
&.active {
|
|
65
|
+
background-color: rgba(var(--bs-primary-rgb), 25%);
|
|
66
|
+
|
|
67
|
+
.facet-bar-progress {
|
|
68
|
+
background-color: rgba(var(--bs-primary-rgb), 75%);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
&:hover, &:focus, &.active {
|
|
72
|
+
border-color: var(--bs-primary);
|
|
73
|
+
}
|
|
74
|
+
.facet-name {
|
|
75
|
+
text-align: left;
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
|
|
78
|
+
.itf-checkbox {
|
|
79
|
+
min-height: auto;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
.facet-stat {
|
|
83
|
+
display: flex;
|
|
84
|
+
-webkit-box-align: center;
|
|
85
|
+
align-items: center;
|
|
86
|
+
margin-left: 0.25rem;
|
|
87
|
+
}
|
|
88
|
+
.facet-bar {
|
|
89
|
+
display: inline-block;
|
|
90
|
+
margin-left: 0.5rem;
|
|
91
|
+
width: 60px;
|
|
92
|
+
}
|
|
93
|
+
.facet-bar-progress {
|
|
94
|
+
display: block;
|
|
95
|
+
width: var(--bar-width);
|
|
96
|
+
min-width: 5px;
|
|
97
|
+
height: 10px;
|
|
98
|
+
background-color: rgb(197, 205, 223);
|
|
99
|
+
transition: width 0.3s ease 0s;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
103
|
+
<script>
|
|
104
|
+
import { Vue, Prop, Model, Component } from 'vue-property-decorator';
|
|
105
|
+
import itfTextField from '../text-field/TextField.vue';
|
|
106
|
+
import itfButton from '../button/Button';
|
|
107
|
+
import itfCheckbox from '../checkbox/Checkbox.vue';
|
|
108
|
+
|
|
109
|
+
export default @Component({
|
|
110
|
+
name: 'FilterFacetsList',
|
|
111
|
+
components: {
|
|
112
|
+
itfCheckbox,
|
|
113
|
+
itfButton,
|
|
114
|
+
itfTextField
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
class FilterFacetsList extends Vue {
|
|
118
|
+
@Model('input') value;
|
|
119
|
+
@Prop() items;
|
|
120
|
+
@Prop() item;
|
|
121
|
+
@Prop() total;
|
|
122
|
+
@Prop({ type: Number, default: 5 }) limit;
|
|
123
|
+
@Prop(Boolean) multiple;
|
|
124
|
+
@Prop(Boolean) showAll;
|
|
125
|
+
@Prop(String) title;
|
|
126
|
+
|
|
127
|
+
query = '';
|
|
128
|
+
showMore = false;
|
|
129
|
+
isShowSelected = false;
|
|
130
|
+
|
|
131
|
+
toggleMore() {
|
|
132
|
+
this.showMore = !this.showMore;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get hasMore() {
|
|
136
|
+
if (this.showAll) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return this.visibleList.length > this.limit;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get visibleList() {
|
|
143
|
+
let list = this.items.map(val => {
|
|
144
|
+
const isSelected = this.multiple
|
|
145
|
+
? Array.isArray(this.value) && this.value.map(String).includes(`${val.value}`)
|
|
146
|
+
: `${this.value}` === `${val.value}`;
|
|
147
|
+
|
|
148
|
+
return { ...val, isSelected };
|
|
149
|
+
});
|
|
150
|
+
if (this.query) {
|
|
151
|
+
list = list.filter((val) => val.label.toLowerCase().includes(this.query.toLowerCase()));
|
|
152
|
+
}
|
|
153
|
+
if (this.isShowSelected) {
|
|
154
|
+
return list.filter((val) => val.isSelected);
|
|
155
|
+
}
|
|
156
|
+
return list;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get mappedValues() {
|
|
160
|
+
const list = this.visibleList;
|
|
161
|
+
if (!this.showMore && !this.showAll) {
|
|
162
|
+
return list.slice(0, this.limit);
|
|
163
|
+
}
|
|
164
|
+
return list;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getPercent(item) {
|
|
168
|
+
return this.total ? Math.round((item.count / this.total) * 100) : 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
onFilterClick(val) {
|
|
172
|
+
const value = `${val.value}`;
|
|
173
|
+
if (!this.multiple) {
|
|
174
|
+
return this.$emit('input', `${this.value}` === value ? null : value);
|
|
175
|
+
}
|
|
176
|
+
const newVal = [...Array.isArray(this.value) ? [...this.value] : []].map((s) => s.toString());
|
|
177
|
+
if (newVal.includes(value)) {
|
|
178
|
+
newVal.splice(newVal.indexOf(value), 1);
|
|
179
|
+
} else {
|
|
180
|
+
newVal.push(value);
|
|
181
|
+
}
|
|
182
|
+
this.$emit('input', newVal);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
get isSelectedAll() {
|
|
186
|
+
return this.mappedValues.every(o => o.isSelected);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
onSelectAll(isSelect = false) {
|
|
190
|
+
if (isSelect) {
|
|
191
|
+
this.$emit('input', this.visibleList.map((val) => `${val.value}`));
|
|
192
|
+
} else {
|
|
193
|
+
this.$emit('input', []);
|
|
194
|
+
}
|
|
195
|
+
this.isShowSelected = false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get isHasSelected() {
|
|
199
|
+
return this.value && this.value.length > 0 && !this.query; // тільки коли не пошук
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
onShowSelected(isShow = false) {
|
|
203
|
+
this.isShowSelected = isShow;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
</script>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="d-flex gap-2 align-items-center flex-wrap">
|
|
3
|
+
<template v-if="search">
|
|
4
|
+
<itf-text-field
|
|
5
|
+
style="width: 200px"
|
|
6
|
+
small
|
|
7
|
+
:placeholder="$t('components.filter.search')"
|
|
8
|
+
prepend---icon="search"
|
|
9
|
+
:delay-input="250"
|
|
10
|
+
clearable
|
|
11
|
+
:value="filterValue.query"
|
|
12
|
+
@input="(e) => onFilterChange({ type: 'text', name: 'query' }, { value: e })"
|
|
13
|
+
/>
|
|
14
|
+
</template>
|
|
15
|
+
<filter-badge
|
|
16
|
+
v-for="(facet, n) in filters"
|
|
17
|
+
:key="n"
|
|
18
|
+
v-model="filter[facet.name]"
|
|
19
|
+
:is-default="filter[facet.name].isDefault"
|
|
20
|
+
:text="filter[facet.name].label"
|
|
21
|
+
:type="facet.type"
|
|
22
|
+
:icon="facet.icon"
|
|
23
|
+
:options="facet.options"
|
|
24
|
+
@change="onFilterChange(facet, $event)"
|
|
25
|
+
/>
|
|
26
|
+
<div v-if="loading">
|
|
27
|
+
<span class="itf-spinner"></span>
|
|
28
|
+
{{$t('loading')}}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
<style lang="scss">
|
|
33
|
+
</style>
|
|
34
|
+
<script>
|
|
35
|
+
import { DateTime } from 'luxon';
|
|
36
|
+
import { Vue, Model, Prop, Component } from 'vue-property-decorator';
|
|
37
|
+
import tooltip from '../../directives/tooltip';
|
|
38
|
+
import itfIcon from '../icon/Icon';
|
|
39
|
+
import itfButton from '../button/Button';
|
|
40
|
+
import itfDropdown from '../dropdown/Dropdown.vue';
|
|
41
|
+
import itfTextField from '../text-field/TextField.vue';
|
|
42
|
+
import { formatDateonly, formatMoney, formatRangeDates } from '../../helpers/formatters';
|
|
43
|
+
import FilterBadge from './FilterBadge.vue';
|
|
44
|
+
|
|
45
|
+
export default @Component({
|
|
46
|
+
components: {
|
|
47
|
+
itfIcon,
|
|
48
|
+
itfButton,
|
|
49
|
+
itfDropdown,
|
|
50
|
+
itfTextField,
|
|
51
|
+
FilterBadge
|
|
52
|
+
},
|
|
53
|
+
directives: {
|
|
54
|
+
tooltip
|
|
55
|
+
},
|
|
56
|
+
filters: {
|
|
57
|
+
formatMoney,
|
|
58
|
+
formatDateonly
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
class FilterPanel extends Vue {
|
|
62
|
+
@Model('input') value;
|
|
63
|
+
@Prop({ type: String }) endpoint;
|
|
64
|
+
@Prop() panel;
|
|
65
|
+
@Prop(Boolean) search;
|
|
66
|
+
|
|
67
|
+
filter = {};
|
|
68
|
+
filterValue = {};
|
|
69
|
+
filters = [];
|
|
70
|
+
loading = false;
|
|
71
|
+
|
|
72
|
+
async mounted() {
|
|
73
|
+
this.loading = true;
|
|
74
|
+
await this.$try(async () => {
|
|
75
|
+
const { filters } = await this.$axios.$get(this.endpoint);
|
|
76
|
+
this.filters = filters;
|
|
77
|
+
this.loadFiltersValue();
|
|
78
|
+
});
|
|
79
|
+
this.loading = false;
|
|
80
|
+
this.panel.on('panels.changed', () => this.loadFiltersValue());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
loadFiltersValue() {
|
|
84
|
+
const payload = this.panel ? this.panel.getPayload() : {};
|
|
85
|
+
const filter = {};
|
|
86
|
+
const filterValue = {};
|
|
87
|
+
for (const item of this.filters) {
|
|
88
|
+
filter[item.name] = payload[item.name] ? this.formatValue(item, { value: payload[item.name] }) : { isDefault: true, ...item.options.defaultValue };
|
|
89
|
+
if (item.type === 'period') {
|
|
90
|
+
filterValue.from = payload.from;
|
|
91
|
+
filterValue.to = payload.to;
|
|
92
|
+
} else {
|
|
93
|
+
filterValue[item.name] = payload[item.name];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (this.search) {
|
|
97
|
+
filterValue.query = payload.query;
|
|
98
|
+
}
|
|
99
|
+
this.filter = filter;
|
|
100
|
+
this.filterValue = filterValue;
|
|
101
|
+
this.$emit('input', this.filterValue);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
onFilterChange(facet, value) {
|
|
105
|
+
this.filter[facet.name] = this.formatValue(facet, value);
|
|
106
|
+
if (facet.type === 'period') {
|
|
107
|
+
this.filterValue.from = this.filter[facet.name].isDefault ? undefined : value.value[0];
|
|
108
|
+
this.filterValue.to = this.filter[facet.name].isDefault ? undefined : value.value[1];
|
|
109
|
+
} else {
|
|
110
|
+
this.filterValue[facet.name] = this.filter[facet.name].isDefault ? undefined : value.value;
|
|
111
|
+
}
|
|
112
|
+
if (this.panel) {
|
|
113
|
+
const payload = this.panel.getPayload();
|
|
114
|
+
this.panel.setPayload({ ...payload, ...this.filterValue });
|
|
115
|
+
}
|
|
116
|
+
this.$emit('input', this.filterValue);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get daysList() {
|
|
120
|
+
return [// { title: 'Today', date: () => [DateTime.local(), DateTime.local()] },
|
|
121
|
+
{ title: this.$t('components.thisWeek'), date: () => [DateTime.local().startOf('week'), DateTime.local().endOf('week')] },
|
|
122
|
+
{ title: this.$t('components.lastWeek'), date: () => [DateTime.local().minus({ week: 1 }).startOf('week'), DateTime.local().minus({ week: 1 }).endOf('week')] },
|
|
123
|
+
{ title: this.$t('components.thisMonth'), date: () => [DateTime.local().startOf('month'), DateTime.local().endOf('month')] },
|
|
124
|
+
{ title: this.$t('components.lastMonth'), date: () => [DateTime.local().minus({ months: 1 }).startOf('month'), DateTime.local().minus({ months: 1 }).endOf('month')] },
|
|
125
|
+
{ title: this.$t('components.thisQuarter'), date: () => [DateTime.local().startOf('quarter'), DateTime.local().endOf('quarter')] },
|
|
126
|
+
{ title: this.$t('components.lastQuarter'), date: () => [DateTime.local().minus({ quarter: 1 }).startOf('quarter'), DateTime.local().minus({ quarter: 1 }).endOf('quarter')] },
|
|
127
|
+
{ title: this.$t('components.thisYear'), date: () => [DateTime.local().startOf('year'), DateTime.local().endOf('year')] },
|
|
128
|
+
{ title: this.$t('components.lastYear'), date: () => [DateTime.local().minus({ year: 1 }).startOf('year'), DateTime.local().minus({ year: 1 }).endOf('year')] },
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
formatValue(facet, value) {
|
|
133
|
+
if (facet.type === 'period') {
|
|
134
|
+
if (value.value) {
|
|
135
|
+
let from = DateTime.fromISO(value.value[0]);
|
|
136
|
+
let to = DateTime.fromISO(value.value[1]);
|
|
137
|
+
if (!from.isValid || !to.isValid) {
|
|
138
|
+
from = DateTime.fromISO(facet.options.defaultValue.value[0]);
|
|
139
|
+
to = DateTime.fromISO(facet.options.defaultValue.value[1]);
|
|
140
|
+
}
|
|
141
|
+
const namedItem = this.daysList.find(item => {
|
|
142
|
+
const [start, end] = item.date();
|
|
143
|
+
return start.equals(from.startOf('day')) && end.equals(to.endOf('day'));
|
|
144
|
+
});
|
|
145
|
+
if (namedItem) {
|
|
146
|
+
value.label = namedItem.title;
|
|
147
|
+
} else {
|
|
148
|
+
value.label = formatRangeDates(from, to);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (facet.options.defaultValue?.value) {
|
|
152
|
+
value.isDefault = facet.options.defaultValue ? value.value[0] === facet.options.defaultValue.value[0] && value.value[1] === facet.options.defaultValue.value[1] : false;
|
|
153
|
+
} else {
|
|
154
|
+
value.isDefault = !value.value;
|
|
155
|
+
}
|
|
156
|
+
} else if (facet.type === 'date') {
|
|
157
|
+
const date = DateTime.fromISO(value.value);
|
|
158
|
+
value.label = formatDateonly(date.isValid ? value.value : facet.options.defaultValue.value, 'dd MMM yyyy');
|
|
159
|
+
value.isDefault = facet.options.defaultValue ? value.value === facet.options.defaultValue.value : false;
|
|
160
|
+
} else if (facet.type === 'facets-list') {
|
|
161
|
+
const firstItem = facet.options.items.find(item => item.value === (Array.isArray(value.value) ? value.value[0] : value.value));
|
|
162
|
+
value.label = firstItem ? firstItem.label : facet.options.defaultValue.label;
|
|
163
|
+
if (value.value && value.value.length > 1) {
|
|
164
|
+
value.label = `${value.label} та ще ${value.value.length - 1}`;
|
|
165
|
+
}
|
|
166
|
+
value.isDefault = facet.options.defaultValue ? JSON.stringify(value.value) === JSON.stringify(facet.options.defaultValue.value) : false;
|
|
167
|
+
} else if (facet.type === 'amount') {
|
|
168
|
+
if (typeof value.value[0] !== 'number' || typeof value.value[1] !== 'number') {
|
|
169
|
+
value.value = [facet.options.min, facet.options.max];
|
|
170
|
+
}
|
|
171
|
+
value.isDefault = facet.options.defaultValue ? JSON.stringify(value.value) === JSON.stringify(facet.options.defaultValue.value) : false;
|
|
172
|
+
value.label = value.isDefault ? facet.options.defaultValue.label : `${formatMoney(value.value[0], null, 0)} - ${formatMoney(value.value[1], null, 0)}`;
|
|
173
|
+
} else if (facet.type === 'list') {
|
|
174
|
+
const item = facet.options.items.find(item => item.value === value.value);
|
|
175
|
+
value.isDefault = facet.options.defaultValue ? JSON.stringify(value.value) === JSON.stringify(facet.options.defaultValue.value) : false;
|
|
176
|
+
value.label = item ? item.label : facet.options.defaultValue.label;
|
|
177
|
+
} else if (facet.type === 'text') {
|
|
178
|
+
value.value = value.value.length ? value.value : undefined;
|
|
179
|
+
value.isDefault = !value.value;
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
</script>
|
|
@@ -79,6 +79,10 @@
|
|
|
79
79
|
</div>
|
|
80
80
|
</template>
|
|
81
81
|
<style lang="scss">
|
|
82
|
+
@import "bootstrap/scss/_functions.scss";
|
|
83
|
+
@import "bootstrap/scss/_variables.scss";
|
|
84
|
+
@import "bootstrap/scss/mixins/_breakpoints.scss";
|
|
85
|
+
|
|
82
86
|
.b-panel-list-container {
|
|
83
87
|
width: 100%;
|
|
84
88
|
height: 100%;
|
|
@@ -87,15 +91,18 @@
|
|
|
87
91
|
}
|
|
88
92
|
.b-panel-list {
|
|
89
93
|
align-items: stretch;
|
|
90
|
-
//background: var(--bs-body-bg);
|
|
94
|
+
//background: var(--bs-body-bg);
|
|
91
95
|
bottom: 0;
|
|
92
|
-
display: flex;
|
|
93
96
|
height: 100%;
|
|
94
97
|
left: 0;
|
|
95
98
|
overflow-x: auto;
|
|
96
99
|
position: absolute;
|
|
97
100
|
right: 0;
|
|
98
101
|
top: 0;
|
|
102
|
+
|
|
103
|
+
@include media-breakpoint-up(md) {
|
|
104
|
+
display: flex;
|
|
105
|
+
}
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
$an-time: .1s;
|
|
@@ -213,6 +220,7 @@ export default class PanelList extends Vue {
|
|
|
213
220
|
newStack[index].isCollapsed = false;
|
|
214
221
|
this.panelsStack = newStack;
|
|
215
222
|
this.ensureOnlyTwoOpenPanels(panel.id);
|
|
223
|
+
this.setPanelHash();
|
|
216
224
|
}
|
|
217
225
|
|
|
218
226
|
ensureOnlyTwoOpenPanels(keepOpenId: number) {
|
|
@@ -244,7 +252,7 @@ export default class PanelList extends Vue {
|
|
|
244
252
|
this.panelsStack = newStack;
|
|
245
253
|
}
|
|
246
254
|
|
|
247
|
-
internalOpenPanel(type: string, payload: any = {}, openIndex?: number) {
|
|
255
|
+
internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
|
|
248
256
|
if (!this.panels[type]) {
|
|
249
257
|
return;
|
|
250
258
|
}
|
|
@@ -315,13 +323,15 @@ export default class PanelList extends Vue {
|
|
|
315
323
|
newPanel.payload = value;
|
|
316
324
|
newPanel.title = this.panels[type].caption(this.$t.bind(this), value);
|
|
317
325
|
newPanel.icon = this.panels[type].icon ? this.panels[type].icon(this.$t.bind(this), payload) : null,
|
|
318
|
-
|
|
326
|
+
this.setPanelHash()
|
|
319
327
|
}
|
|
320
328
|
newStack.push(newPanel);
|
|
321
329
|
this.panelsStack = newStack;
|
|
322
330
|
this.ensureOnlyTwoOpenPanels(newPanel.id);
|
|
323
331
|
this.emitEvent('panel.open', newPanel);
|
|
324
|
-
|
|
332
|
+
if (!noEvents) {
|
|
333
|
+
this.emitEvent('panels.changed', this.panelsStack);
|
|
334
|
+
}
|
|
325
335
|
return res(newPanel);
|
|
326
336
|
})
|
|
327
337
|
});
|
|
@@ -365,6 +375,7 @@ export default class PanelList extends Vue {
|
|
|
365
375
|
p.isCollapsed = p !== panel;
|
|
366
376
|
}
|
|
367
377
|
this.panelsStack = newStack;
|
|
378
|
+
this.setPanelHash();
|
|
368
379
|
}
|
|
369
380
|
|
|
370
381
|
getPanels(type) {
|
|
@@ -377,7 +388,7 @@ export default class PanelList extends Vue {
|
|
|
377
388
|
|
|
378
389
|
getLink(stack: IPanel[]) {
|
|
379
390
|
return stack.map(panel => {
|
|
380
|
-
return `${panel.type}=${JSON.stringify(panel.payload || {})}`;
|
|
391
|
+
return `${panel.type}${panel.isCollapsed ? '' : '!'}=${JSON.stringify(panel.payload || {})}`;
|
|
381
392
|
}).join('&');
|
|
382
393
|
}
|
|
383
394
|
|
|
@@ -387,19 +398,33 @@ export default class PanelList extends Vue {
|
|
|
387
398
|
}
|
|
388
399
|
|
|
389
400
|
async parsePanelHash() {
|
|
390
|
-
const hash =
|
|
401
|
+
const {hash} = location;
|
|
391
402
|
if (hash) {
|
|
392
403
|
const panels = hash.slice(1).split('&').map(item => {
|
|
393
404
|
const [type, payload] = item.split('=');
|
|
405
|
+
const isCollapsed = !item.includes('!');
|
|
394
406
|
return {
|
|
395
|
-
type,
|
|
407
|
+
type: type.replace('!', ''),
|
|
408
|
+
isCollapsed,
|
|
396
409
|
payload: JSON.parse(decodeURIComponent(payload))
|
|
397
410
|
};
|
|
398
411
|
});
|
|
399
412
|
const newStack = [];
|
|
400
|
-
for (const
|
|
401
|
-
const
|
|
402
|
-
|
|
413
|
+
for (const panelIndex in panels) {
|
|
414
|
+
const panel = panels[panelIndex];
|
|
415
|
+
if (this.panelsStack[panelIndex] && this.panelsStack[panelIndex].type === panel.type) {
|
|
416
|
+
// reuse panel
|
|
417
|
+
this.panelsStack[panelIndex].payload = panel.payload;
|
|
418
|
+
this.panelsStack[panelIndex].isCollapsed = panel.isCollapsed;
|
|
419
|
+
newStack.push(this.panelsStack[panelIndex]);
|
|
420
|
+
} else {
|
|
421
|
+
const resPanel = await this.internalOpenPanel(panel.type, panel.payload, undefined, true);
|
|
422
|
+
if (resPanel) {
|
|
423
|
+
resPanel.isCollapsed = panel.isCollapsed;
|
|
424
|
+
resPanel.isAnimate = false;
|
|
425
|
+
newStack.push(resPanel);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
403
428
|
}
|
|
404
429
|
this.panelsStack = newStack;
|
|
405
430
|
this.emitEvent('panels.changed', this.panelsStack);
|