@itfin/components 1.4.27 → 1.4.30
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/libs/air-datepicker/air-datepicker.js +1 -1
- package/package.json +1 -1
- package/src/components/datepicker/DatePickerInline.vue +129 -62
- package/src/components/filter/FilterPanel.vue +37 -16
- package/src/components/icon/components/nomi-chart-funnel.vue +6 -0
- package/src/components/icon/components/nomi-chart-kpi.vue +8 -0
- package/src/components/icon/components/nomi-control-panel.vue +8 -0
- package/src/components/panels/Panel.vue +44 -7
- package/src/components/panels/PanelList.vue +106 -33
- package/src/components/panels/helpers.ts +38 -14
- package/src/components/panels/index.ts +43 -0
- package/src/components/view/View.vue +15 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="itf-filter-panel d-flex flex-column
|
|
2
|
+
<div class="itf-filter-panel d-flex flex-column align-items-start" :class="{'gap-3': !filtersOnly}">
|
|
3
3
|
<div v-if="!filtersOnly" class="d-flex gap-2 justify-content-between w-100">
|
|
4
4
|
<slot name="search">
|
|
5
5
|
<div>
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
class="itf-filter-panel__badge"
|
|
36
36
|
:ref="'item-' + n"
|
|
37
37
|
v-model="filter[facet.name]"
|
|
38
|
-
:is-default="filter[facet.name].isDefault"
|
|
39
|
-
:text="filter[facet.name].label"
|
|
38
|
+
:is-default="filter[facet.name] && filter[facet.name].isDefault"
|
|
39
|
+
:text="filter[facet.name] && filter[facet.name].label"
|
|
40
40
|
:type="facet.type"
|
|
41
41
|
:icon="facet.icon"
|
|
42
42
|
:options="facet.options"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
</div>
|
|
53
53
|
<slot name="after-filters"></slot>
|
|
54
54
|
</div>
|
|
55
|
-
<div v-if="loading">
|
|
55
|
+
<div v-if="loading && !visibleFilters.length">
|
|
56
56
|
<span class="itf-spinner"></span>
|
|
57
57
|
{{$t('loading')}}
|
|
58
58
|
</div>
|
|
@@ -177,6 +177,8 @@ class FilterPanel extends Vue {
|
|
|
177
177
|
(entries) => {
|
|
178
178
|
entries.forEach(entry => {
|
|
179
179
|
const index = parseInt(entry.target.dataset.index);
|
|
180
|
+
const filter = this.filters[index];
|
|
181
|
+
const value = this.filter[filter.name];
|
|
180
182
|
if (entry.isIntersecting) {
|
|
181
183
|
this.visibleItems.add(index); // Додаємо, якщо елемент у полі зору
|
|
182
184
|
} else {
|
|
@@ -206,7 +208,7 @@ class FilterPanel extends Vue {
|
|
|
206
208
|
|
|
207
209
|
get visibleFilters() {
|
|
208
210
|
if (this.mini) {
|
|
209
|
-
return sortBy(this.filters, (f) => this.filter[f.name].isDefault).filter(f => !f.options?.hidden)
|
|
211
|
+
return sortBy(this.filters, (f) => this.filter[f.name].isDefault).filter(f => !f.options?.hidden);
|
|
210
212
|
}
|
|
211
213
|
return this.filters.filter(f => !f.options?.hidden);
|
|
212
214
|
}
|
|
@@ -233,15 +235,7 @@ class FilterPanel extends Vue {
|
|
|
233
235
|
|
|
234
236
|
this.filters = this.staticFilters ?? [];
|
|
235
237
|
if (this.endpoint) {
|
|
236
|
-
this.
|
|
237
|
-
await this.$try(async () => {
|
|
238
|
-
const payload = this.panel ? this.panel.getPayload() : {};
|
|
239
|
-
const {filters, tableSchema} = await this.$axios.$get(this.endpoint, { params: payload });
|
|
240
|
-
this.filters = filters;
|
|
241
|
-
this.$emit('set-table-schema', tableSchema);
|
|
242
|
-
this.loadFiltersValue();
|
|
243
|
-
});
|
|
244
|
-
this.loading = false;
|
|
238
|
+
this.loadData();
|
|
245
239
|
} else {
|
|
246
240
|
this.loadFiltersValue();
|
|
247
241
|
}
|
|
@@ -250,6 +244,23 @@ class FilterPanel extends Vue {
|
|
|
250
244
|
}
|
|
251
245
|
}
|
|
252
246
|
|
|
247
|
+
async loadData() {
|
|
248
|
+
this.loading = true;
|
|
249
|
+
await this.$try(async () => {
|
|
250
|
+
const payload = this.panel ? this.panel.getPayload() : {};
|
|
251
|
+
const {filters, tableSchema} = await this.$axios.$get(this.endpoint, {
|
|
252
|
+
preventRaceCondition: true,
|
|
253
|
+
params: payload
|
|
254
|
+
});
|
|
255
|
+
this.filters = filters;
|
|
256
|
+
this.loading = false;
|
|
257
|
+
this.$emit('set-table-schema', tableSchema);
|
|
258
|
+
this.loadFiltersValue();
|
|
259
|
+
}, () => {
|
|
260
|
+
this.loading = false;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
253
264
|
toggleFilters() {
|
|
254
265
|
this.showFilters = !this.showFilters;
|
|
255
266
|
if (this.stateName) {
|
|
@@ -269,7 +280,7 @@ class FilterPanel extends Vue {
|
|
|
269
280
|
filterValue.to = payload.to;
|
|
270
281
|
} else {
|
|
271
282
|
filter[item.name] = payload[item.name] ? this.formatValue(item, { value: payload[item.name] }) : { isDefault: true, ...item.options.defaultValue };
|
|
272
|
-
filterValue[item.name] = payload[item.name]
|
|
283
|
+
filterValue[item.name] = payload[item.name];
|
|
273
284
|
}
|
|
274
285
|
}
|
|
275
286
|
}
|
|
@@ -282,6 +293,7 @@ class FilterPanel extends Vue {
|
|
|
282
293
|
this.filter = filter;
|
|
283
294
|
this.filterValue = filterValue;
|
|
284
295
|
this.$emit('input', this.filterValue);
|
|
296
|
+
this.$emit('loaded', this.filterValue);
|
|
285
297
|
this.initObserver();
|
|
286
298
|
}
|
|
287
299
|
}
|
|
@@ -318,6 +330,7 @@ class FilterPanel extends Vue {
|
|
|
318
330
|
this.panel.setPayload({ ...payload, ...this.filterValue });
|
|
319
331
|
}
|
|
320
332
|
this.$emit('input', this.filterValue);
|
|
333
|
+
this.$emit('change', this.filterValue);
|
|
321
334
|
}
|
|
322
335
|
|
|
323
336
|
get daysList() {
|
|
@@ -363,7 +376,11 @@ class FilterPanel extends Vue {
|
|
|
363
376
|
}
|
|
364
377
|
} else if (facet.type === 'date') {
|
|
365
378
|
const date = DateTime.fromISO(value.value);
|
|
366
|
-
value.label = (date.isValid ?
|
|
379
|
+
value.label = (date.isValid ? date : DateTime.fromISO(facet.options.defaultValue.value ?? DateTime.now().toISO())).toFormat('dd MMM yyyy');
|
|
380
|
+
value.isDefault = facet.options.defaultValue ? value.value === facet.options.defaultValue.value : false;
|
|
381
|
+
} else if (facet.type === 'month') {
|
|
382
|
+
const date = DateTime.fromISO(value.value);
|
|
383
|
+
value.label = capitalizeFirstLetter((date.isValid ? date : DateTime.fromISO(facet.options.defaultValue.value)).toFormat('LLLL yyyy'));
|
|
367
384
|
value.isDefault = facet.options.defaultValue ? value.value === facet.options.defaultValue.value : false;
|
|
368
385
|
} else if (facet.type === 'facets-list') {
|
|
369
386
|
const firstItem = facet.options.items.find(item => item.value === (Array.isArray(value.value) ? value.value[0] : value.value));
|
|
@@ -402,6 +419,10 @@ class FilterPanel extends Vue {
|
|
|
402
419
|
}
|
|
403
420
|
value.hidden = facet.options?.hidden ?? false;
|
|
404
421
|
return value;
|
|
422
|
+
|
|
423
|
+
function capitalizeFirstLetter(string) {
|
|
424
|
+
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
425
|
+
}
|
|
405
426
|
}
|
|
406
427
|
}
|
|
407
428
|
</script>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<template><svg width="24" height="24" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
3
|
+
d="M10.809 11C9.18 11 8 12.334 8 13.81v2.798c0 1.476 1.181 2.81 2.809 2.81H53.19c1.628 0 2.809-1.334 2.809-2.81V13.81c0-1.476-1.181-2.81-2.809-2.81H10.81Zm5.436 11.194c-1.628 0-2.809 1.334-2.809 2.81v2.798c0 1.476 1.181 2.81 2.809 2.81h31.51c1.628 0 2.809-1.334 2.809-2.81v-2.798c0-1.476-1.181-2.81-2.809-2.81h-31.51Zm5.436 11.194c-1.627 0-2.809 1.334-2.809 2.81v2.798c0 1.476 1.182 2.81 2.81 2.81h20.637c1.627 0 2.808-1.334 2.808-2.81v-2.798c0-1.476-1.18-2.81-2.808-2.81H21.68Zm5.436 11.194c-1.627 0-2.808 1.334-2.808 2.81v2.798c0 1.476 1.18 2.81 2.808 2.81h9.765c1.628 0 2.81-1.334 2.81-2.81v-2.798c0-1.476-1.182-2.81-2.81-2.81h-9.764Z"
|
|
4
|
+
fill="currentColor"></path>
|
|
5
|
+
</svg>
|
|
6
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<template><svg width="24" height="24" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M28.761 40.248h-3.974l-4.324-6.954-1.48 1.06v5.894h-3.5V23.742h3.5v7.553l1.378-1.942 4.471-5.611h3.884l-5.758 7.305 5.803 9.201ZM34.339 31.51h1.151c1.077 0 1.882-.211 2.416-.633.535-.429.802-1.05.802-1.862 0-.82-.226-1.427-.677-1.818-.444-.392-1.144-.587-2.1-.587h-1.592v4.9Zm7.903-2.62c0 1.777-.557 3.135-1.671 4.076-1.106.941-2.683 1.411-4.73 1.411h-1.502v5.871h-3.5V23.742h5.272c2.003 0 3.523.433 4.562 1.298 1.046.858 1.569 2.142 1.569 3.85ZM45.358 40.248V23.742h3.5v16.506h-3.5Z"
|
|
3
|
+
fill="currentColor"></path>
|
|
4
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
|
5
|
+
d="M9.29 12.903c0-.855.743-1.548 1.66-1.548h43.133c.916 0 1.66.693 1.66 1.548 0 .855-.744 1.549-1.66 1.549H10.95c-.917 0-1.66-.694-1.66-1.549ZM9.29 51.097c0-.855.743-1.549 1.66-1.549h43.133c.916 0 1.66.694 1.66 1.549s-.744 1.548-1.66 1.548H10.95c-.917 0-1.66-.693-1.66-1.548Z"
|
|
6
|
+
fill="currentColor"></path>
|
|
7
|
+
</svg>
|
|
8
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<template><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M19 9L9 9" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M13 15H5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M19 15L17 15" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<circle cx="15" cy="15" r="2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
<circle cx="7" cy="9" r="2" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
7
|
+
</svg>
|
|
8
|
+
</template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div tabindex="-1" :class="{'b-panel': !nocard, 'b-panel__collapsed': collapsed, 'b-panel__active': !collapsed}">
|
|
3
|
+
<slot name="before-header"></slot>
|
|
3
4
|
<div v-if="collapsed && !nocard" class="b-panel__expand" @click.stop.prevent="expandPanel">
|
|
4
5
|
<itf-button v-if="closeable" icon small class="b-panel__expand_button" @click="closePanel">
|
|
5
6
|
<itf-icon name="cross" />
|
|
@@ -12,19 +13,24 @@
|
|
|
12
13
|
</div>
|
|
13
14
|
<div v-if="!nocard" v-show="!collapsed" class="b-panel-header px-3 pt-3 pb-2">
|
|
14
15
|
<slot name="header">
|
|
15
|
-
<div class="d-flex gap-3 align-items-center">
|
|
16
|
-
<itf-button
|
|
17
|
-
<itf-icon name="
|
|
16
|
+
<div class="d-flex gap-3 align-items-center" style="min-width: 0">
|
|
17
|
+
<itf-button icon default class="d-md-none open-menu-button" @click="$emit('open-menu')">
|
|
18
|
+
<itf-icon name="menu" new />
|
|
18
19
|
</itf-button>
|
|
19
20
|
<slot name="title">
|
|
20
|
-
<div class="b-panel__title fw-bold mb-0 h2" v-text="title"></div>
|
|
21
|
+
<div style="min-width: 0" class="b-panel__title fw-bold mb-0 h2 text-truncate" v-text="title"></div>
|
|
21
22
|
</slot>
|
|
22
23
|
</div>
|
|
23
24
|
<div class="d-flex gap-1">
|
|
24
25
|
<slot name="buttons"></slot>
|
|
25
|
-
<
|
|
26
|
-
<itf-icon
|
|
27
|
-
|
|
26
|
+
<template v-if="expandable">
|
|
27
|
+
<itf-button v-if="!isFullSize" icon default class="b-panel__expand_button d-none d-md-block" @click="fullsizePanel">
|
|
28
|
+
<itf-icon new name="expand" />
|
|
29
|
+
</itf-button>
|
|
30
|
+
<itf-button v-else icon default class="b-panel__expand_button d-none d-md-block" @click="collapsePanel">
|
|
31
|
+
<itf-icon new name="collapse" />
|
|
32
|
+
</itf-button>
|
|
33
|
+
</template>
|
|
28
34
|
<itf-button v-if="closeable" icon default class="b-panel__expand_button" @click="closePanel">
|
|
29
35
|
<itf-icon new name="close" />
|
|
30
36
|
</itf-button>
|
|
@@ -37,10 +43,36 @@
|
|
|
37
43
|
</div>
|
|
38
44
|
</template>
|
|
39
45
|
<style lang="scss">
|
|
46
|
+
@keyframes bellRing {
|
|
47
|
+
0% {
|
|
48
|
+
transform: rotate(0deg);
|
|
49
|
+
}
|
|
50
|
+
10% {
|
|
51
|
+
transform: rotate(30deg);
|
|
52
|
+
}
|
|
53
|
+
20% {
|
|
54
|
+
transform: rotate(0deg);
|
|
55
|
+
}
|
|
56
|
+
30% {
|
|
57
|
+
transform: rotate(30deg);
|
|
58
|
+
}
|
|
59
|
+
40% {
|
|
60
|
+
transform: rotate(0deg);
|
|
61
|
+
}
|
|
62
|
+
100% {
|
|
63
|
+
transform: rotate(0deg);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
.b-panel {
|
|
41
68
|
--b-panel-bg: var(--bs-body-bg);
|
|
42
69
|
--b-panel-color: var(--bs-body-color);
|
|
43
70
|
--b-panel-box-shadow: 0px 2px 6px 0px rgba(21,23,25,.05);
|
|
71
|
+
--b-panel-white: #ffffff;
|
|
72
|
+
--b-panel-info: #5981c0;
|
|
73
|
+
--b-panel-success: #10834e;
|
|
74
|
+
--b-panel-warning: #cda277;
|
|
75
|
+
--b-panel-danger: #A90B00;
|
|
44
76
|
|
|
45
77
|
margin-left: 8px;
|
|
46
78
|
|
|
@@ -158,6 +190,7 @@ class Panel extends Vue {
|
|
|
158
190
|
@Prop() panel;
|
|
159
191
|
@Prop(Boolean) collapsed;
|
|
160
192
|
@Prop(Boolean) closeable;
|
|
193
|
+
@Prop(Boolean) isFullSize;
|
|
161
194
|
@Prop(Boolean) expandable;
|
|
162
195
|
@Prop(Boolean) animate;
|
|
163
196
|
@Prop(Boolean) nocard;
|
|
@@ -170,6 +203,10 @@ class Panel extends Vue {
|
|
|
170
203
|
this.$emit('expand');
|
|
171
204
|
}
|
|
172
205
|
|
|
206
|
+
collapsePanel() {
|
|
207
|
+
this.$emit('collapse');
|
|
208
|
+
}
|
|
209
|
+
|
|
173
210
|
fullsizePanel() {
|
|
174
211
|
this.$emit('fullsize');
|
|
175
212
|
}
|
|
@@ -17,14 +17,20 @@
|
|
|
17
17
|
:icon="panel.icon"
|
|
18
18
|
:payload="panel.payload"
|
|
19
19
|
:expandable="panelsStack.length > 1"
|
|
20
|
+
:isFullSize="isFullSize"
|
|
20
21
|
:collapsed="panel.isCollapsed"
|
|
21
22
|
:closeable="panel.isCloseable"
|
|
22
23
|
:animate="panel.isAnimate"
|
|
23
24
|
@open="openPanel($event[0], $event[1], n + 1)"
|
|
24
25
|
@expand="expandPanel(panel)"
|
|
25
26
|
@fullsize="fullsizePanel(panel)"
|
|
27
|
+
@collapse="collapsePanel(panel)"
|
|
26
28
|
@close="closePanel(panel)"
|
|
29
|
+
@open-menu="$emit('open-menu', panel.type, panel.payload)"
|
|
27
30
|
>
|
|
31
|
+
<template #before-header>
|
|
32
|
+
<slot name="before-header" :panel="panel" :index="n" :payload="panel.payload"></slot>
|
|
33
|
+
</template>
|
|
28
34
|
<slot
|
|
29
35
|
:name="panel.type"
|
|
30
36
|
:panel="panel"
|
|
@@ -34,9 +40,9 @@
|
|
|
34
40
|
:close="() => closePanel(panel)"
|
|
35
41
|
:expand="() => expandPanel(panel)"
|
|
36
42
|
:fullsize="() => fullsizePanel(panel)">
|
|
37
|
-
<component
|
|
43
|
+
<component v-if="panel.components.default" :is="panel.components.default" :panel="panel" :payload="panel.payload" />
|
|
38
44
|
</slot>
|
|
39
|
-
<template v-if="$scopedSlots[`${panel.type}.title`] ||
|
|
45
|
+
<template v-if="$scopedSlots[`${panel.type}.title`] || panel.components.title" #title>
|
|
40
46
|
<slot
|
|
41
47
|
:name="`${panel.type}.title`"
|
|
42
48
|
:panel="panel"
|
|
@@ -46,10 +52,10 @@
|
|
|
46
52
|
:close="() => closePanel(panel)"
|
|
47
53
|
:expand="() => expandPanel(panel)"
|
|
48
54
|
:fullsize="() => fullsizePanel(panel)">
|
|
49
|
-
<component v-if="
|
|
55
|
+
<component v-if="panel.components.title" :is="panel.components.title" :panel="panel" :payload="panel.payload" />
|
|
50
56
|
</slot>
|
|
51
57
|
</template>
|
|
52
|
-
<template v-if="$scopedSlots[`${panel.type}.buttons`] ||
|
|
58
|
+
<template v-if="$scopedSlots[`${panel.type}.buttons`] || panel.components.buttons" #buttons>
|
|
53
59
|
<slot
|
|
54
60
|
:name="`${panel.type}.buttons`"
|
|
55
61
|
:panel="panel"
|
|
@@ -59,10 +65,10 @@
|
|
|
59
65
|
:close="() => closePanel(panel)"
|
|
60
66
|
:expand="() => expandPanel(panel)"
|
|
61
67
|
:fullsize="() => fullsizePanel(panel)">
|
|
62
|
-
<component v-if="
|
|
68
|
+
<component v-if="panel.components.buttons" :is="panel.components.buttons" :panel="panel" :payload="panel.payload" />
|
|
63
69
|
</slot>
|
|
64
70
|
</template>
|
|
65
|
-
<template v-if="$scopedSlots[`${panel.type}.header`] ||
|
|
71
|
+
<template v-if="$scopedSlots[`${panel.type}.header`] || panel.components.header" #header>
|
|
66
72
|
<slot
|
|
67
73
|
:name="`${panel.type}.header`"
|
|
68
74
|
:panel="panel"
|
|
@@ -72,7 +78,7 @@
|
|
|
72
78
|
:close="() => closePanel(panel)"
|
|
73
79
|
:expand="() => expandPanel(panel)"
|
|
74
80
|
:fullsize="() => fullsizePanel(panel)">
|
|
75
|
-
<component v-if="
|
|
81
|
+
<component v-if="panel.components.header" :is="panel.components.header" :panel="panel" :payload="panel.payload" />
|
|
76
82
|
</slot>
|
|
77
83
|
</template>
|
|
78
84
|
</panel>
|
|
@@ -148,7 +154,6 @@ $double-an-time: $an-time * 2;
|
|
|
148
154
|
//transition: opacity $an-time linear;
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
|
-
|
|
152
157
|
//.slide-enter-active > div {
|
|
153
158
|
// opacity: 0;
|
|
154
159
|
//}
|
|
@@ -159,8 +164,10 @@ $double-an-time: $an-time * 2;
|
|
|
159
164
|
</style>
|
|
160
165
|
<script lang="ts">
|
|
161
166
|
import { Vue, Component, Prop } from 'vue-property-decorator';
|
|
162
|
-
import
|
|
163
|
-
import
|
|
167
|
+
import itfIcon from '../icon/Icon.vue';
|
|
168
|
+
import Panel from './Panel.vue';
|
|
169
|
+
import {hashToStack, stackToHash} from "@itfin/components/src/components/panels/helpers";
|
|
170
|
+
import {emitGlobalEvent, setRootPanelList} from "@itfin/components/src/components/panels";
|
|
164
171
|
|
|
165
172
|
interface VisualOptions {
|
|
166
173
|
title: string;
|
|
@@ -195,6 +202,7 @@ export interface IPanel {
|
|
|
195
202
|
|
|
196
203
|
@Component({
|
|
197
204
|
components: {
|
|
205
|
+
itfIcon,
|
|
198
206
|
Panel
|
|
199
207
|
},
|
|
200
208
|
directives: {
|
|
@@ -208,15 +216,20 @@ export interface IPanel {
|
|
|
208
216
|
export default class PanelList extends Vue {
|
|
209
217
|
@Prop() firstPanel: IPanel;
|
|
210
218
|
@Prop() panels: Record<string, Component>;
|
|
219
|
+
@Prop({ default: () => {} }) searchPanel: (type: string) => boolean;
|
|
220
|
+
@Prop({ type: String, default: 'path' }) routeType: string;
|
|
221
|
+
@Prop({ type: String, default: '' }) routePrefix: string;
|
|
211
222
|
|
|
212
223
|
panelsStack:IPanel[] = [];
|
|
213
224
|
|
|
214
225
|
nextId:number = 0;
|
|
215
226
|
|
|
216
227
|
created() {
|
|
228
|
+
setRootPanelList(this);
|
|
217
229
|
if (this.firstPanel) {
|
|
218
230
|
this.internalOpenPanel(this.firstPanel.type, this.firstPanel.payload);
|
|
219
231
|
}
|
|
232
|
+
console.info('created');
|
|
220
233
|
this.parsePanelHash(); // щоб панелі змінювались при перезавантаженні
|
|
221
234
|
window.addEventListener('popstate', this.handlePopState); // щоб панелі змінювались при навігації
|
|
222
235
|
}
|
|
@@ -267,18 +280,30 @@ export default class PanelList extends Vue {
|
|
|
267
280
|
this.panelsStack = newStack;
|
|
268
281
|
}
|
|
269
282
|
|
|
270
|
-
internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
|
|
271
|
-
|
|
272
|
-
|
|
283
|
+
async internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
|
|
284
|
+
let panel = this.panels[type];
|
|
285
|
+
if (!panel) {
|
|
286
|
+
panel = await this.searchPanel(type, this.panels);
|
|
287
|
+
if (!panel) {
|
|
288
|
+
console.error(`Panel type "${type}" not found`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
panel.type = type;
|
|
273
292
|
}
|
|
274
|
-
if (typeof
|
|
293
|
+
if (typeof panel.caption !== 'function') {
|
|
275
294
|
throw new Error('Panel component must have a "caption" function');
|
|
276
295
|
}
|
|
277
296
|
const newPanel:any = {
|
|
278
297
|
id: this.nextId++,
|
|
279
|
-
nocard:
|
|
280
|
-
title:
|
|
281
|
-
icon:
|
|
298
|
+
nocard: panel.nocard,
|
|
299
|
+
title: panel.caption(this.$t.bind(this), payload),
|
|
300
|
+
icon: panel.icon ? panel.icon(this.$t.bind(this), payload) : null,
|
|
301
|
+
components: {
|
|
302
|
+
default: panel.default ?? undefined,
|
|
303
|
+
buttons: panel.buttons ?? undefined,
|
|
304
|
+
header: panel.header ?? undefined,
|
|
305
|
+
title: panel.title ?? undefined,
|
|
306
|
+
},
|
|
282
307
|
type,
|
|
283
308
|
payload,
|
|
284
309
|
isCollapsed: false,
|
|
@@ -289,7 +314,7 @@ export default class PanelList extends Vue {
|
|
|
289
314
|
newPanel.isCloseable = false;
|
|
290
315
|
}
|
|
291
316
|
let newStack = [...this.panelsStack];
|
|
292
|
-
if (
|
|
317
|
+
if (panel.permanentExpanded && newStack.length) {
|
|
293
318
|
for (const panel of newStack) {
|
|
294
319
|
panel.isCollapsed = true;
|
|
295
320
|
}
|
|
@@ -299,25 +324,32 @@ export default class PanelList extends Vue {
|
|
|
299
324
|
isAnimation = newStack.length === openIndex;
|
|
300
325
|
newStack = newStack.slice(0, openIndex);
|
|
301
326
|
}
|
|
327
|
+
if (newStack.length > 0 && !newStack.find(p => !p.isCollapsed)) {
|
|
328
|
+
// якщо немає відкритих панелей, то перша панель має бути розгорнута
|
|
329
|
+
newStack[0].isCollapsed = false;
|
|
330
|
+
}
|
|
302
331
|
this.panelsStack = newStack;
|
|
303
332
|
return new Promise(res => {
|
|
304
333
|
this.$nextTick(() => { // щоб панелі змінювались при редагуванні
|
|
305
334
|
const n = newStack.length;
|
|
306
335
|
newPanel.isAnimate = isAnimation;
|
|
307
|
-
newPanel.permanentExpanded = !!
|
|
336
|
+
newPanel.permanentExpanded = !!panel.permanentExpanded;
|
|
308
337
|
newPanel.emit = (event, ...args) => this.emitEvent(event, ...args);
|
|
309
|
-
newPanel.open = (type, payload) => this.openPanel(type, payload, n + 1);
|
|
338
|
+
newPanel.open = (type, payload, index?:number) => this.openPanel(type, payload, index ?? n + 1);
|
|
310
339
|
newPanel.close = () => this.closePanel(newPanel);
|
|
311
340
|
newPanel.expand = () => this.expandPanel(newPanel);
|
|
312
341
|
newPanel.getTitle = () => newPanel.title;
|
|
313
342
|
newPanel.getIcon = () => newPanel.icon;
|
|
314
|
-
newPanel.setTitle = (title: string) => { newPanel.title = title; };
|
|
343
|
+
newPanel.setTitle = (title: string) => { newPanel.title = title; this.updateTitle() };
|
|
315
344
|
newPanel.setIcon = (icon: string) => { newPanel.icon = icon; };
|
|
316
|
-
newPanel.on = (eventName, func: (event: string, ...args: any[]) => any) => {
|
|
317
|
-
|
|
318
|
-
|
|
345
|
+
newPanel.on = (eventName: string|string[], func: (event: string, ...args: any[]) => any) => {
|
|
346
|
+
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
|
|
347
|
+
for (const evName of eventNames) {
|
|
348
|
+
if (!newPanel.__events[evName]) {
|
|
349
|
+
newPanel.__events[evName] = [];
|
|
350
|
+
}
|
|
351
|
+
newPanel.__events[evName].push(func);
|
|
319
352
|
}
|
|
320
|
-
newPanel.__events[eventName].push(func);
|
|
321
353
|
};
|
|
322
354
|
newPanel.off = (eventName, func: (event: string, ...args: any[]) => any) => {
|
|
323
355
|
if (newPanel.__events[eventName]) {
|
|
@@ -337,9 +369,7 @@ export default class PanelList extends Vue {
|
|
|
337
369
|
newPanel.getPayload = () => newPanel.payload;
|
|
338
370
|
newPanel.setPayload = (value: any) => {
|
|
339
371
|
newPanel.payload = value;
|
|
340
|
-
|
|
341
|
-
newPanel.icon = this.panels[type].icon ? this.panels[type].icon(this.$t.bind(this), payload) : null,
|
|
342
|
-
this.setPanelHash()
|
|
372
|
+
this.setPanelHash();
|
|
343
373
|
}
|
|
344
374
|
newStack.push(newPanel);
|
|
345
375
|
this.panelsStack = newStack;
|
|
@@ -353,12 +383,19 @@ export default class PanelList extends Vue {
|
|
|
353
383
|
});
|
|
354
384
|
}
|
|
355
385
|
|
|
386
|
+
updateTitle() {
|
|
387
|
+
const titles = this.panelsStack.map(p => p.getTitle()).filter(Boolean).reverse();
|
|
388
|
+
this.$root.$options.head.titleChunk = titles.join(' / ');
|
|
389
|
+
this.$meta().refresh();
|
|
390
|
+
}
|
|
391
|
+
|
|
356
392
|
async openPanel(type: string, payload: any, openIndex?: number) {
|
|
357
393
|
await this.internalOpenPanel(type, payload, openIndex);
|
|
358
|
-
this.setPanelHash()
|
|
394
|
+
this.setPanelHash();
|
|
359
395
|
}
|
|
360
396
|
|
|
361
397
|
emitEvent(event: string, ...args: any[]) {
|
|
398
|
+
emitGlobalEvent(event, ...args);
|
|
362
399
|
for (const panel of this.panelsStack) {
|
|
363
400
|
if (panel.__events[event]) {
|
|
364
401
|
for (const func of panel.__events[event]) {
|
|
@@ -388,12 +425,40 @@ export default class PanelList extends Vue {
|
|
|
388
425
|
fullsizePanel(panel: IPanel) {
|
|
389
426
|
const newStack = [...this.panelsStack];
|
|
390
427
|
for (const p of newStack) {
|
|
428
|
+
p.isLastOpened = !p.isCollapsed && p !== panel;
|
|
391
429
|
p.isCollapsed = p !== panel;
|
|
392
430
|
}
|
|
393
431
|
this.panelsStack = newStack;
|
|
394
432
|
this.setPanelHash();
|
|
395
433
|
}
|
|
396
434
|
|
|
435
|
+
get isFullSize() {
|
|
436
|
+
return this.panelsStack.filter(p => !p.isCollapsed).length === 1;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
expandPanel(panel: IPanel) {
|
|
440
|
+
const newStack = [...this.panelsStack];
|
|
441
|
+
const index = newStack.findIndex(p => p.id === panel.id);
|
|
442
|
+
newStack[index].isCollapsed = false;
|
|
443
|
+
this.panelsStack = newStack;
|
|
444
|
+
this.ensureOnlyTwoOpenPanels(panel.id);
|
|
445
|
+
this.setPanelHash();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
collapsePanel(panel: IPanel) {
|
|
449
|
+
const newStack = [...this.panelsStack];
|
|
450
|
+
const currenctIndex = newStack.findIndex(p => p.id === panel.id);
|
|
451
|
+
const lastOpenedIndex = newStack.findIndex(p => p.isLastOpened);
|
|
452
|
+
if (lastOpenedIndex !== -1) { // якщо зебрежена остання відкрита панель
|
|
453
|
+
newStack[lastOpenedIndex].isCollapsed = false
|
|
454
|
+
} else if (newStack[currenctIndex-1]) { // якщо після оновлення сторінки відсутнє значення "остання відкрита", то відкриваємо ту, що зліва
|
|
455
|
+
newStack[currenctIndex-1].isCollapsed = false;
|
|
456
|
+
}
|
|
457
|
+
this.panelsStack = newStack;
|
|
458
|
+
this.ensureOnlyTwoOpenPanels(panel.id);
|
|
459
|
+
this.setPanelHash();
|
|
460
|
+
}
|
|
461
|
+
|
|
397
462
|
getPanels(type) {
|
|
398
463
|
return this.panelsStack.filter(panel => panel.type === type);
|
|
399
464
|
}
|
|
@@ -403,14 +468,19 @@ export default class PanelList extends Vue {
|
|
|
403
468
|
}
|
|
404
469
|
|
|
405
470
|
setPanelHash() {
|
|
406
|
-
const hash = stackToHash(this.panelsStack).replace(/^#/, '');
|
|
407
|
-
this
|
|
471
|
+
const hash = stackToHash(this.panelsStack, this.routePrefix).replace(/^#/, '');
|
|
472
|
+
if (this.routeType === 'path') {
|
|
473
|
+
this.$router.push({ path: hash });
|
|
474
|
+
} else {
|
|
475
|
+
this.$router.push({ hash });
|
|
476
|
+
}
|
|
477
|
+
this.updateTitle();
|
|
408
478
|
}
|
|
409
479
|
|
|
410
480
|
async parsePanelHash() {
|
|
411
|
-
const
|
|
481
|
+
const hash = this.routeType === 'path' ? location.pathname : location.hash;
|
|
412
482
|
if (hash) {
|
|
413
|
-
const panels = hashToStack(hash);
|
|
483
|
+
const panels = hashToStack(hash, this.routePrefix);
|
|
414
484
|
const newStack = [];
|
|
415
485
|
this.panelsStack = [];
|
|
416
486
|
for (const panelIndex in panels) {
|
|
@@ -430,11 +500,14 @@ export default class PanelList extends Vue {
|
|
|
430
500
|
}
|
|
431
501
|
}
|
|
432
502
|
this.panelsStack = newStack;
|
|
503
|
+
console.info('set', newStack);
|
|
433
504
|
this.emitEvent('panels.changed', this.panelsStack);
|
|
505
|
+
this.updateTitle();
|
|
434
506
|
}
|
|
435
507
|
}
|
|
436
508
|
|
|
437
509
|
handlePopState() {
|
|
510
|
+
console.info('handlePopState')
|
|
438
511
|
this.parsePanelHash();
|
|
439
512
|
// виправляє проблему відкритої панелі при натисканні кнопки "назад" до першої панелі
|
|
440
513
|
if (this.panelsStack.length === 2) {
|
|
@@ -1,33 +1,57 @@
|
|
|
1
|
+
import JSON5 from 'json5'
|
|
2
|
+
import {isPathType} from "./index";
|
|
3
|
+
|
|
1
4
|
export interface IPanel {
|
|
2
5
|
type: string;
|
|
3
6
|
payload?: any;
|
|
4
7
|
isCollapsed?: boolean;
|
|
5
8
|
}
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const COLLAPSE_SYMBOL = '~'
|
|
11
|
+
const PARAMS_SYMBOL = ';'
|
|
12
|
+
|
|
13
|
+
export function stackToHash(stack: IPanel[], prefix: string = '') {
|
|
14
|
+
let hash = stack.map(panel => {
|
|
15
|
+
let json = JSON5.stringify(panel.payload || {});
|
|
16
|
+
json = json.substring(1, json.length - 1); // Remove the outer {}
|
|
17
|
+
return `${panel.type}${panel.isCollapsed ? COLLAPSE_SYMBOL : ''}${json ? PARAMS_SYMBOL : ''}${json}`;
|
|
18
|
+
}).join(isPathType() ? '/' : '&');
|
|
19
|
+
if (prefix) {
|
|
20
|
+
hash = `${prefix}/${hash}`;
|
|
21
|
+
}
|
|
22
|
+
return isPathType() ? `/${hash}` : `#${hash}`;
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
|
|
15
|
-
export function hashToStack(hash: string|undefined): IPanel[] {
|
|
26
|
+
export function hashToStack(hash: string|undefined, prefix: string = ''): IPanel[] {
|
|
16
27
|
let stack:IPanel[] = [];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
let str = hash.replace(isPathType() ? /^\// : /^#/, '');
|
|
29
|
+
if (str && prefix) {
|
|
30
|
+
str = str.replace(new RegExp(`^${prefix}\/?`), '');
|
|
31
|
+
}
|
|
32
|
+
if (str) {
|
|
33
|
+
stack = str.split(isPathType() ? '/' : '&').map(item => {
|
|
34
|
+
if (!item.includes(PARAMS_SYMBOL)) {
|
|
35
|
+
return { type: item.replace(COLLAPSE_SYMBOL, ''), isCollapsed: item.includes(COLLAPSE_SYMBOL), payload: {} };
|
|
36
|
+
}
|
|
37
|
+
const [type, payload] = item.split(PARAMS_SYMBOL);
|
|
38
|
+
const isCollapsed = type.includes(COLLAPSE_SYMBOL);
|
|
23
39
|
let payloadObj:any = {};
|
|
24
40
|
try {
|
|
25
|
-
|
|
41
|
+
let json = decodeURIComponent(payload);
|
|
42
|
+
if (!json.startsWith('{')) {
|
|
43
|
+
json = `{${json}`; // Ensure it starts with a '{' to be valid JSON
|
|
44
|
+
}
|
|
45
|
+
if (!json.endsWith('}')) {
|
|
46
|
+
json += '}'; // Ensure it ends with a '}' to be valid JSON
|
|
47
|
+
}
|
|
48
|
+
payloadObj = JSON5.parse(json);
|
|
26
49
|
} catch (e) {
|
|
27
50
|
// ignore
|
|
51
|
+
console.warn(`Error parsing payload for type ${type}:`, payload, e);
|
|
28
52
|
}
|
|
29
53
|
return {
|
|
30
|
-
type: type.replace(
|
|
54
|
+
type: type.replace(COLLAPSE_SYMBOL, ''),
|
|
31
55
|
isCollapsed,
|
|
32
56
|
payload: payloadObj
|
|
33
57
|
};
|