@itfin/components 1.4.26 → 1.4.28

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": "1.4.26",
3
+ "version": "1.4.28",
4
4
  "author": "Vitalii Savchuk <esvit666@gmail.com>",
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -74,9 +74,12 @@ class itfApp extends Vue {
74
74
  try {
75
75
  await func();
76
76
  } catch (err) {
77
- this.showError(err.message);
78
- if (errFunc) {
79
- errFunc(err);
77
+ if (err.code !== 'ERR_CANCELED') {
78
+ console.error(err);
79
+ this.showError(err.message);
80
+ if (errFunc) {
81
+ errFunc(err);
82
+ }
80
83
  }
81
84
  }
82
85
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div class="itf-checkbox form-check" :class="{ 'form-switch': this.switch, 'itf-checkbox__large': large, 'itf-checkbox__medium': medium }">
4
4
  <input class="form-check-input" ref="input" :id="id" type="checkbox" name="checkbox" v-model="isChecked" :disabled="isDisabled" />
5
- <label :for="id" class="form-check-label">
5
+ <label :for="id" class="form-check-label w-100" :class="{ 'disabled': isDisabled }">
6
6
  <slot name="label">
7
7
  {{label}}
8
8
  <slot name="icon"></slot>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="itf-radio-box form-check card" :class="{ 'itf-radio__large': large, 'itf-radio__medium': medium, 'active': isChecked, 'right': right, 'left': !right }">
2
+ <div class="itf-radio-box form-check card" :class="{ 'disabled': disabled, 'itf-radio__large': large, 'itf-radio__medium': medium, 'active': isChecked, 'right': right, 'left': !right }">
3
3
  <input class="form-check-input" :id="id" type="radio" :name="radioName" v-model="isChecked" :value="true" :disabled="disabled" />
4
4
  <label :for="id" slot="label" class="form-check-label card-body">
5
5
 
@@ -18,6 +18,17 @@
18
18
  position: relative;
19
19
  cursor: pointer;
20
20
 
21
+ &:not(.disabled) {
22
+ cursor: pointer;
23
+
24
+ .form-check-label {
25
+ cursor: pointer;
26
+ }
27
+ }
28
+
29
+ .form-check-label {
30
+ cursor: not-allowed;
31
+ }
21
32
  &.left {
22
33
  padding: 0 0 0 2.5rem;
23
34
 
@@ -38,13 +49,9 @@
38
49
  &.active {
39
50
  background-color: rgba(var(--bs-primary-rgb),.1) !important;
40
51
  }
41
- &:hover {
52
+ &:hover:not(.disabled) {
42
53
  background-color: rgba(0,0,0,.05);
43
54
  }
44
-
45
- .form-check-label {
46
- cursor: pointer;
47
- }
48
55
  }
49
56
  </style>
50
57
  <script>
@@ -269,7 +269,7 @@ class FilterPanel extends Vue {
269
269
  filterValue.to = payload.to;
270
270
  } else {
271
271
  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];
272
+ filterValue[item.name] = payload[item.name] ?? filter[item.name].value;
273
273
  }
274
274
  }
275
275
  }
@@ -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
  }
@@ -1,19 +1,19 @@
1
1
  <template>
2
- <div v-loading="loading" class="px-3 pt-2 h-100">
2
+ <div v-loading="loading" class="px-3 pt-2 h-100 d-flex flex-column">
3
3
  <itf-form
4
4
  ref="editForm"
5
- class="d-flex flex-column justify-content-between h-100"
5
+ class="d-flex flex-column justify-content-between flex-grow-1"
6
6
  @keydown.native.shift.enter.stop.prevent="onSaveClick"
7
7
  @keydown.native.esc.stop.prevent="$emit('cancel')"
8
8
  >
9
9
  <slot></slot>
10
10
  <div class="py-3 justify-content-end d-flex align-items-center sticky-container">
11
11
  <div v-if="!hideFooter">
12
- <itf-button v-tooltip.delay="'Hot key: Esc'" secondary :loading="loading" :disabled="loading" @click="$emit('cancel')">
13
- <span>{{ $t('components.modal.cancel') }}</span>
12
+ <itf-button v-tooltip.delay="'Hot key: Esc'" secondary squircle :loading="loading" :disabled="loading" @click="$emit('cancel')">
13
+ <span>{{ cancelBtnText }}</span>
14
14
  </itf-button>
15
- <itf-button v-tooltip.delay="'Hot key: Shift + Enter'" primary :loading="loading" :disabled="loading" @click="onSaveClick">
16
- <span>{{ $t('components.modal.save') }}</span>
15
+ <itf-button v-tooltip.delay="'Hot key: Shift + Enter'" primary squircle :loading="loading" :disabled="loading" @click="onSaveClick">
16
+ <span>{{ saveBtnText }}</span>
17
17
  </itf-button>
18
18
  </div>
19
19
  </div>
@@ -51,6 +51,8 @@ import itfButton from '../button/Button.vue';
51
51
  export default class PanelItemEdit extends Vue {
52
52
  @Prop(Boolean) loading;
53
53
  @Prop(Boolean) hideFooter;
54
+ @Prop({ type: String, default: function() { return this.$t('components.modal.save') } }) saveBtnText;
55
+ @Prop({ type: String, default: function() { return this.$t('components.modal.cancel') } }) cancelBtnText;
54
56
 
55
57
  onSaveClick() {
56
58
  if (this.$refs.editForm && !this.$refs.editForm.doValidation()) {
@@ -5,6 +5,7 @@
5
5
  import { Vue, Component, Inject, Prop } from 'vue-property-decorator';
6
6
  import { IPanel } from './PanelList.vue';
7
7
  import {stackToHash} from "@itfin/components/src/components/panels/helpers";
8
+ import {getRootPanelList} from "@itfin/components/src/components/panels";
8
9
 
9
10
  @Component({
10
11
  components: {
@@ -15,7 +16,6 @@ import {stackToHash} from "@itfin/components/src/components/panels/helpers";
15
16
  }
16
17
  })
17
18
  export default class PanelLink extends Vue {
18
- @Inject({ default: null }) panelList;
19
19
  @Inject({ default: null }) currentPanel;
20
20
 
21
21
  @Prop(Boolean) global: boolean;
@@ -25,6 +25,7 @@ export default class PanelLink extends Vue {
25
25
  @Prop() list;
26
26
  @Prop({ type: String, default: 'active' }) activeClass: string;
27
27
  @Prop(Boolean) append: boolean;
28
+ @Prop(Boolean) replace: boolean;
28
29
 
29
30
  get on() {
30
31
  const handlers = {};
@@ -35,7 +36,7 @@ export default class PanelLink extends Vue {
35
36
  }
36
37
 
37
38
  get activeList() {
38
- return this.list ?? this.panelList;
39
+ return this.list ?? getRootPanelList();
39
40
  }
40
41
 
41
42
  get isActive() {
@@ -51,6 +52,9 @@ export default class PanelLink extends Vue {
51
52
  if (!this.append) {
52
53
  stack = stack.splice(0, this.currentPanel?.index + 1);
53
54
  }
55
+ if (this.replace) {
56
+ stack = [];
57
+ }
54
58
  const hash = stackToHash([
55
59
  ...stack,
56
60
  {
@@ -62,13 +66,18 @@ export default class PanelLink extends Vue {
62
66
  }
63
67
 
64
68
  onClick(e) {
65
- console.info(this.activeList);
69
+ e.preventDefault();
70
+ e.stopPropagation();
71
+ const index = this.replace ? 0 : (this.append ? undefined : this.currentPanel?.index + 1);
72
+ this.$emit('open', {
73
+ panel: this.panel,
74
+ payload: this.payload || {},
75
+ index
76
+ });
66
77
  if (!this.activeList) {
67
78
  return;
68
79
  }
69
- e.preventDefault();
70
- e.stopPropagation();
71
- this.activeList.openPanel(this.panel, this.payload || {}, this.append ? undefined : this.currentPanel?.index + 1);
80
+ this.activeList.openPanel(this.panel, this.payload || {}, index);
72
81
  }
73
82
  }
74
83
  </script>
@@ -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,12 +216,15 @@ 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;
211
221
 
212
222
  panelsStack:IPanel[] = [];
213
223
 
214
224
  nextId:number = 0;
215
225
 
216
226
  created() {
227
+ setRootPanelList(this);
217
228
  if (this.firstPanel) {
218
229
  this.internalOpenPanel(this.firstPanel.type, this.firstPanel.payload);
219
230
  }
@@ -267,18 +278,30 @@ export default class PanelList extends Vue {
267
278
  this.panelsStack = newStack;
268
279
  }
269
280
 
270
- internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
271
- if (!this.panels[type]) {
272
- return;
281
+ async internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
282
+ let panel = this.panels[type];
283
+ if (!panel) {
284
+ panel = await this.searchPanel(type, this.panels);
285
+ if (!panel) {
286
+ console.error(`Panel type "${type}" not found`);
287
+ return;
288
+ }
289
+ panel.type = type;
273
290
  }
274
- if (typeof this.panels[type].caption !== 'function') {
291
+ if (typeof panel.caption !== 'function') {
275
292
  throw new Error('Panel component must have a "caption" function');
276
293
  }
277
294
  const newPanel:any = {
278
295
  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,
296
+ nocard: panel.nocard,
297
+ title: panel.caption(this.$t.bind(this), payload),
298
+ icon: panel.icon ? panel.icon(this.$t.bind(this), payload) : null,
299
+ components: {
300
+ default: panel.default ?? undefined,
301
+ buttons: panel.buttons ?? undefined,
302
+ header: panel.header ?? undefined,
303
+ title: panel.title ?? undefined,
304
+ },
282
305
  type,
283
306
  payload,
284
307
  isCollapsed: false,
@@ -289,7 +312,7 @@ export default class PanelList extends Vue {
289
312
  newPanel.isCloseable = false;
290
313
  }
291
314
  let newStack = [...this.panelsStack];
292
- if (this.panels[type].permanentExpanded && newStack.length) {
315
+ if (panel.permanentExpanded && newStack.length) {
293
316
  for (const panel of newStack) {
294
317
  panel.isCollapsed = true;
295
318
  }
@@ -299,25 +322,32 @@ export default class PanelList extends Vue {
299
322
  isAnimation = newStack.length === openIndex;
300
323
  newStack = newStack.slice(0, openIndex);
301
324
  }
325
+ if (newStack.length > 0 && !newStack.find(p => !p.isCollapsed)) {
326
+ // якщо немає відкритих панелей, то перша панель має бути розгорнута
327
+ newStack[0].isCollapsed = false;
328
+ }
302
329
  this.panelsStack = newStack;
303
330
  return new Promise(res => {
304
331
  this.$nextTick(() => { // щоб панелі змінювались при редагуванні
305
332
  const n = newStack.length;
306
333
  newPanel.isAnimate = isAnimation;
307
- newPanel.permanentExpanded = !!this.panels[type].permanentExpanded;
334
+ newPanel.permanentExpanded = !!panel.permanentExpanded;
308
335
  newPanel.emit = (event, ...args) => this.emitEvent(event, ...args);
309
- newPanel.open = (type, payload) => this.openPanel(type, payload, n + 1);
336
+ newPanel.open = (type, payload, index?:number) => this.openPanel(type, payload, index ?? n + 1);
310
337
  newPanel.close = () => this.closePanel(newPanel);
311
338
  newPanel.expand = () => this.expandPanel(newPanel);
312
339
  newPanel.getTitle = () => newPanel.title;
313
340
  newPanel.getIcon = () => newPanel.icon;
314
- newPanel.setTitle = (title: string) => { newPanel.title = title; };
341
+ newPanel.setTitle = (title: string) => { newPanel.title = title; this.updateTitle() };
315
342
  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] = [];
343
+ newPanel.on = (eventName: string|string[], func: (event: string, ...args: any[]) => any) => {
344
+ const eventNames = Array.isArray(eventName) ? eventName : [eventName];
345
+ for (const evName of eventNames) {
346
+ if (!newPanel.__events[evName]) {
347
+ newPanel.__events[evName] = [];
348
+ }
349
+ newPanel.__events[evName].push(func);
319
350
  }
320
- newPanel.__events[eventName].push(func);
321
351
  };
322
352
  newPanel.off = (eventName, func: (event: string, ...args: any[]) => any) => {
323
353
  if (newPanel.__events[eventName]) {
@@ -337,9 +367,7 @@ export default class PanelList extends Vue {
337
367
  newPanel.getPayload = () => newPanel.payload;
338
368
  newPanel.setPayload = (value: any) => {
339
369
  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()
370
+ this.setPanelHash();
343
371
  }
344
372
  newStack.push(newPanel);
345
373
  this.panelsStack = newStack;
@@ -353,12 +381,19 @@ export default class PanelList extends Vue {
353
381
  });
354
382
  }
355
383
 
384
+ updateTitle() {
385
+ const titles = this.panelsStack.map(p => p.getTitle()).filter(Boolean).reverse();
386
+ this.$root.$options.head.titleChunk = titles.join(' / ');
387
+ this.$meta().refresh();
388
+ }
389
+
356
390
  async openPanel(type: string, payload: any, openIndex?: number) {
357
391
  await this.internalOpenPanel(type, payload, openIndex);
358
- this.setPanelHash()
392
+ this.setPanelHash();
359
393
  }
360
394
 
361
395
  emitEvent(event: string, ...args: any[]) {
396
+ emitGlobalEvent(event, ...args);
362
397
  for (const panel of this.panelsStack) {
363
398
  if (panel.__events[event]) {
364
399
  for (const func of panel.__events[event]) {
@@ -388,12 +423,40 @@ export default class PanelList extends Vue {
388
423
  fullsizePanel(panel: IPanel) {
389
424
  const newStack = [...this.panelsStack];
390
425
  for (const p of newStack) {
426
+ p.isLastOpened = !p.isCollapsed && p !== panel;
391
427
  p.isCollapsed = p !== panel;
392
428
  }
393
429
  this.panelsStack = newStack;
394
430
  this.setPanelHash();
395
431
  }
396
432
 
433
+ get isFullSize() {
434
+ return this.panelsStack.filter(p => !p.isCollapsed).length === 1;
435
+ }
436
+
437
+ expandPanel(panel: IPanel) {
438
+ const newStack = [...this.panelsStack];
439
+ const index = newStack.findIndex(p => p.id === panel.id);
440
+ newStack[index].isCollapsed = false;
441
+ this.panelsStack = newStack;
442
+ this.ensureOnlyTwoOpenPanels(panel.id);
443
+ this.setPanelHash();
444
+ }
445
+
446
+ collapsePanel(panel: IPanel) {
447
+ const newStack = [...this.panelsStack];
448
+ const currenctIndex = newStack.findIndex(p => p.id === panel.id);
449
+ const lastOpenedIndex = newStack.findIndex(p => p.isLastOpened);
450
+ if (lastOpenedIndex !== -1) { // якщо зебрежена остання відкрита панель
451
+ newStack[lastOpenedIndex].isCollapsed = false
452
+ } else if (newStack[currenctIndex-1]) { // якщо після оновлення сторінки відсутнє значення "остання відкрита", то відкриваємо ту, що зліва
453
+ newStack[currenctIndex-1].isCollapsed = false;
454
+ }
455
+ this.panelsStack = newStack;
456
+ this.ensureOnlyTwoOpenPanels(panel.id);
457
+ this.setPanelHash();
458
+ }
459
+
397
460
  getPanels(type) {
398
461
  return this.panelsStack.filter(panel => panel.type === type);
399
462
  }
@@ -403,14 +466,19 @@ export default class PanelList extends Vue {
403
466
  }
404
467
 
405
468
  setPanelHash() {
406
- const hash = stackToHash(this.panelsStack).replace(/^#/, '');
407
- this.$router.push({ hash });
469
+ const hash = stackToHash(this.panelsStack, this.routeType === 'path').replace(/^#/, '');
470
+ if (this.routeType === 'path') {
471
+ this.$router.push({ path: `/${hash}` });
472
+ } else {
473
+ this.$router.push({ hash });
474
+ }
475
+ this.updateTitle();
408
476
  }
409
477
 
410
478
  async parsePanelHash() {
411
- const {hash} = location;
479
+ const hash = this.routeType === 'path' ? location.pathname : location.hash;
412
480
  if (hash) {
413
- const panels = hashToStack(hash);
481
+ const panels = hashToStack(hash, this.routeType === 'path');
414
482
  const newStack = [];
415
483
  this.panelsStack = [];
416
484
  for (const panelIndex in panels) {
@@ -431,6 +499,7 @@ export default class PanelList extends Vue {
431
499
  }
432
500
  this.panelsStack = newStack;
433
501
  this.emitEvent('panels.changed', this.panelsStack);
502
+ this.updateTitle();
434
503
  }
435
504
  }
436
505
 
@@ -1,33 +1,51 @@
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
 
10
+ const COLLAPSE_SYMBOL = '~'
11
+ const PARAMS_SYMBOL = ';'
12
+
7
13
  export function stackToHash(stack: IPanel[]) {
8
14
  const hash = stack.map(panel => {
9
- return `${panel.type}${panel.isCollapsed ? '' : '!'}=${JSON.stringify(panel.payload || {})}`;
10
- }).join('&');
11
- return `#${hash}`;
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
+ return isPathType() ? `/${hash}` : `#${hash}`;
12
20
  }
13
21
 
14
22
 
15
23
  export function hashToStack(hash: string|undefined): IPanel[] {
16
24
  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('!');
25
+ const str = hash.replace(isPathType() ? /^\// : /^#/, '');
26
+ if (str) {
27
+ stack = str.split(isPathType() ? '/' : '&').map(item => {
28
+ if (!item.includes(PARAMS_SYMBOL)) {
29
+ return { type: item.replace(COLLAPSE_SYMBOL, ''), isCollapsed: item.includes(COLLAPSE_SYMBOL), payload: {} };
30
+ }
31
+ const [type, payload] = item.split(PARAMS_SYMBOL);
32
+ const isCollapsed = type.includes(COLLAPSE_SYMBOL);
23
33
  let payloadObj:any = {};
24
34
  try {
25
- payloadObj = JSON.parse(decodeURIComponent(payload));
35
+ let json = decodeURIComponent(payload);
36
+ if (!json.startsWith('{')) {
37
+ json = `{${json}`; // Ensure it starts with a '{' to be valid JSON
38
+ }
39
+ if (!json.endsWith('}')) {
40
+ json += '}'; // Ensure it ends with a '}' to be valid JSON
41
+ }
42
+ payloadObj = JSON5.parse(json);
26
43
  } catch (e) {
27
44
  // ignore
45
+ console.warn(`Error parsing payload for type ${type}:`, payload, e);
28
46
  }
29
47
  return {
30
- type: type.replace('!', ''),
48
+ type: type.replace(COLLAPSE_SYMBOL, ''),
31
49
  isCollapsed,
32
50
  payload: payloadObj
33
51
  };