@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.
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="itf-filter-panel d-flex flex-column gap-3 align-items-start">
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).slice(0, 2);
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.loading = true;
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] ?? filter[item.name].value;
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 ? value.value : DateTime.fromISO(facet.options.defaultValue.value)).toFormat('dd MMM yyyy');
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 v-if="closeable" icon default class="d-md-none" @click="closePanel">
17
- <itf-icon name="chevron_left" />
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
- <itf-button v-if="expandable" icon default class="b-panel__expand_button d-none d-md-block" @click="fullsizePanel">
26
- <itf-icon new name="expand" />
27
- </itf-button>
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 :is="panels[panel.type].default || panels[panel.type]" :panel="panel" :payload="panel.payload" />
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`] || panels[panel.type].title" #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="panels[panel.type].title" :is="panels[panel.type].title" :panel="panel" :payload="panel.payload" />
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`] || panels[panel.type].buttons" #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="panels[panel.type].buttons" :is="panels[panel.type].buttons" :panel="panel" :payload="panel.payload" />
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`] || panels[panel.type].header" #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="panels[panel.type].header" :is="panels[panel.type].header" :panel="panel" :payload="panel.payload" />
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 Panel from './Panel';
163
- import {hashToStack, stackToHash} from "./helpers";
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
- if (!this.panels[type]) {
272
- return;
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 this.panels[type].caption !== 'function') {
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: this.panels[type].nocard,
280
- title: this.panels[type].caption(this.$t.bind(this), payload),
281
- icon: this.panels[type].icon ? this.panels[type].icon(this.$t.bind(this), payload) : null,
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 (this.panels[type].permanentExpanded && newStack.length) {
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 = !!this.panels[type].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
- if (!newPanel.__events[eventName]) {
318
- newPanel.__events[eventName] = [];
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
- newPanel.title = this.panels[type].caption(this.$t.bind(this), value);
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.$router.push({ hash });
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 {hash} = location;
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
- export function stackToHash(stack: IPanel[]) {
8
- const hash = stack.map(panel => {
9
- return `${panel.type}${panel.isCollapsed ? '' : '!'}=${JSON.stringify(panel.payload || {})}`;
10
- }).join('&');
11
- return `#${hash}`;
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
- if (hash) {
18
- const str = hash.replace(/^#/, '');
19
-
20
- stack = str.split('&').map(item => {
21
- const [type, payload] = item.split('=');
22
- const isCollapsed = !type.includes('!');
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
- payloadObj = JSON.parse(decodeURIComponent(payload));
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
  };