@itfin/components 1.4.33 → 1.4.36

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.33",
3
+ "version": "1.4.36",
4
4
  "author": "Vitalii Savchuk <esvit666@gmail.com>",
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -11,9 +11,9 @@
11
11
  <button
12
12
  v-for="(item, n) in daysList"
13
13
  :key="n"
14
- :disabled="!isEnabled(item.date())"
15
- :class="{'active': isSelected(item.date())}"
16
- @click="selectDate(item.date())"
14
+ :disabled="!isEnabled(item.date)"
15
+ :class="{'active': isSelected(item.date)}"
16
+ @click="selectDate(item.date)"
17
17
  class="btn btn-outline-primary btn-sm text-start">
18
18
  {{item.title}}
19
19
  </button>
@@ -30,31 +30,29 @@ export default @Component({
30
30
  name: 'itfDatePickerInline',
31
31
  })
32
32
  class itfDatePickerInline extends Vue {
33
- @Prop() value;
33
+ @Prop({ type: String }) value;
34
34
  @Prop({ type: String, default: 'yyyy-MM-dd' }) valueFormat;
35
35
  @Prop({ type: String, default: ITFSettings.defaultDisplayDateFormat }) displayFormat;
36
+ @Prop({ type: String, default: 'days', validator: (value) => ['days', 'months', 'years'].includes(value) }) startView;
37
+ @Prop({ type: String, default: 'days', validator: (value) => ['days', 'months', 'years'].includes(value) }) minView;
36
38
  @Prop({ type: Boolean, default: false }) onlyCalendar;
39
+ @Prop({ type: Boolean, default: false }) range;
37
40
  @Prop({ type: Object, default: () => ({}) }) customDays;
38
41
  @Prop({ type: [String, Date], default: '' }) minDate;
39
42
  @Prop({ type: [String, Date], default: '' }) maxDate;
40
- @Prop({ type: String, default: 'days' }) minView;
41
43
  @Prop({
42
44
  type: Array,
43
45
  default: function () {
44
- return [// { title: 'Today', date: () => [DateTime.local(), DateTime.local()] },
45
- { title: this.$t('components.thisWeek'), date: () => [DateTime.local().startOf('week'), DateTime.local().endOf('week')] },
46
- { title: this.$t('components.lastWeek'), date: () => [DateTime.local().minus({ week: 1 }).startOf('week'), DateTime.local().minus({ week: 1 }).endOf('week')] },
47
- { title: this.$t('components.thisMonth'), date: () => [DateTime.local().startOf('month'), DateTime.local().endOf('month')] },
48
- { title: this.$t('components.lastMonth'), date: () => [DateTime.local().minus({ months: 1 }).startOf('month'), DateTime.local().minus({ months: 1 }).endOf('month')] },
49
- { title: this.$t('components.thisQuarter'), date: () => [DateTime.local().startOf('quarter'), DateTime.local().endOf('quarter')] },
50
- { title: this.$t('components.lastQuarter'), date: () => [DateTime.local().minus({ quarter: 1 }).startOf('quarter'), DateTime.local().minus({ quarter: 1 }).endOf('quarter')] },
51
- { title: this.$t('components.thisYear'), date: () => [DateTime.local().startOf('year'), DateTime.local().endOf('year')] },
52
- { title: this.$t('components.lastYear'), date: () => [DateTime.local().minus({ year: 1 }).startOf('year'), DateTime.local().minus({ year: 1 }).endOf('year')] },
46
+ return [
47
+ { title: this.$t('components.today'), date: {} },
48
+ { title: this.$t('components.tomorrow'), date: { days: 1 } },
49
+ { title: this.$t('components.inAWeek'), date: { week: 1 } },
50
+ { title: this.$t('components.inAMonth'), date: { month: 1 } },
51
+ { title: this.$t('components.inAHalfYear'), date: { month: 6 } },
52
+ { title: this.$t('components.inAYear'), date: { year: 1 } },
53
53
  ];
54
54
  }
55
55
  }) daysList;
56
- @Prop(Boolean) compare;
57
- @Prop({ type: Array }) range; // for compare mode
58
56
 
59
57
  calendar = null;
60
58
 
@@ -62,17 +60,12 @@ class itfDatePickerInline extends Vue {
62
60
  this.createCalendar();
63
61
  }
64
62
 
65
- compareMode(value) {
66
- return this.calendar.compareMode(value);
67
- }
68
-
69
63
  @Watch('minDate')
70
- @Watch('maxDate')
71
- @Watch('minView')
72
64
  async createCalendar() {
73
65
  if (this.calendar) {
74
66
  this.destroyCalendar();
75
67
  }
68
+
76
69
  const [
77
70
  { default: AirDatepicker },
78
71
  { default: localeEn },
@@ -90,35 +83,28 @@ class itfDatePickerInline extends Vue {
90
83
  uk: localeUk,
91
84
  de: localeDe,
92
85
  };
93
- const opts = {
86
+ this.calendar = new AirDatepicker(this.$refs.calendar, {
94
87
  locale: locales[this.$i18n.locale] || locales.en,
95
88
  firstDay: 1,
96
89
  altFieldDateFormat: this.valueFormat,
90
+ range: this.range,
91
+ view: (this.valueAsLuxon && !this.minView) ? 'days' : this.startView,
92
+ minView: this.minView,
97
93
  minDate: this.minDate,
98
94
  maxDate: this.maxDate,
99
- view: this.minView,
100
- minView: this.minView,
101
- range: true,
102
- compareRange: true,
103
- dynamicRange: true,
104
- toggleSelected: function({ date, datepicker }) {
105
- if (datepicker.selectedDates.length > 1) {
106
- datepicker.selectDate(date);
107
- }
108
- return false;
109
- },
110
- selectedDates: this.valueAsLuxon
111
- ? [this.valueAsLuxon[0].toJSDate(), this.valueAsLuxon[1].toJSDate()]
112
- : [],
113
- onSelect: () => {
114
- if (!this.calendar.rangeDateTo) {
95
+ selectedDates: this.valueAsLuxon ? [this.valueAsLuxon.toJSDate()] : [],
96
+ onSelect: ({ date }) => {
97
+ if (this.range && !this.calendar.rangeDateTo) {
115
98
  return;
116
99
  }
117
- this.updateValue(
118
- DateTime.fromJSDate(this.calendar.rangeDateFrom).toFormat(this.displayFormat),
119
- DateTime.fromJSDate(this.calendar.rangeDateTo).toFormat(this.displayFormat),
120
- false,
121
- );
100
+ if (this.range) {
101
+ this.updateValue(
102
+ DateTime.fromJSDate(this.calendar.rangeDateFrom).toFormat(this.displayFormat),
103
+ false
104
+ );
105
+ } else {
106
+ this.updateValue(DateTime.fromJSDate(date).toFormat(this.displayFormat));
107
+ }
122
108
  },
123
109
  onRenderCell: ({ date }) => {
124
110
  const strDate = DateTime.fromJSDate(date).toFormat('yyyy-MM-dd');
@@ -127,22 +113,13 @@ class itfDatePickerInline extends Vue {
127
113
  }
128
114
  return {
129
115
  html: this.customDays[strDate].text || false,
130
- classes: this.customDays[strDate].class || false,
116
+ classes: this.customDays[strDate].class || false
131
117
  };
132
118
  },
133
- };
134
- if (this.compare) {
135
- opts.compareRange = true;
136
- }
137
-
138
- this.calendar = new AirDatepicker(this.$refs.calendar, opts);
119
+ });
139
120
  if (this.valueAsLuxon) {
140
- this.calendar.setViewDate(this.valueAsLuxon[0].toJSDate());
141
- }
142
- if (this.compare) {
143
- this.calendar.compareMode(true);
121
+ this.calendar.setViewDate(this.valueAsLuxon.toJSDate());
144
122
  }
145
- this.onRangeChange();
146
123
  }
147
124
 
148
125
  beforeDestroy() {
@@ -158,97 +135,59 @@ class itfDatePickerInline extends Vue {
158
135
  }
159
136
 
160
137
  get valueAsLuxon() {
161
- if (!this.value || this.value.length < 2 || !this.value[0] || !this.value[1]) {
138
+ if (!this.value) {
162
139
  return null;
163
140
  }
164
141
  if (this.valueFormat === 'ISO') {
165
- return [
166
- DateTime.fromISO(this.value[0]),
167
- DateTime.fromISO(this.value[1]),
168
- ];
142
+ return DateTime.fromISO(this.value);
143
+ }
144
+ return DateTime.fromFormat(this.value, this.valueFormat);
145
+ }
146
+
147
+ get displayValue() {
148
+ if (!this.valueAsLuxon) {
149
+ return '';
169
150
  }
170
- return [
171
- DateTime.fromFormat(this.value[0], this.valueFormat),
172
- DateTime.fromFormat(this.value[1], this.valueFormat),
173
- ];
151
+ return this.valueAsLuxon.toFormat(this.displayFormat);
174
152
  }
175
153
 
176
- updateValue(date1, date2, emitEmpty = false) {
177
- const val = date1 && DateTime.fromFormat(date1, this.displayFormat);
178
- const val2 = date2 && DateTime.fromFormat(date2, this.displayFormat);
179
- if (!val || !val.isValid || !val2 || !val2.isValid) {
154
+ updateValue(value, emitEmpty = false) {
155
+ const val = value && DateTime.fromFormat(value, this.displayFormat);
156
+ if (!val || !val.isValid) {
180
157
  if (emitEmpty) {
181
158
  this.$emit('input', null);
182
159
  }
183
160
  return;
184
161
  }
185
- if ((this.minDateLuxon && (val < this.minDateLuxon || val2 < this.minDateLuxon)) ||
186
- (this.maxDateLuxon && (val > this.maxDateLuxon || val2 > this.maxDateLuxon))
187
- ) {
188
- return;
189
- }
190
-
191
162
  if (this.valueFormat === 'ISO') {
192
- return this.$emit('input', [val.toISO(), val2.toISO()]);
163
+ return this.$emit('input', val.toISO());
193
164
  }
194
- this.$emit('input', [
195
- val.toFormat(this.valueFormat),
196
- val2.toFormat(this.valueFormat),
197
- ]);
165
+ this.$emit('input', val.toFormat(this.valueFormat));
198
166
  }
199
167
 
200
- get minDateLuxon() {
201
- return this.minDate && this.dateLuxon(this.minDate);
202
- }
203
- get maxDateLuxon() {
204
- return this.maxDate && this.dateLuxon(this.maxDate);
168
+ isSelected(date) {
169
+ if (!this.valueAsLuxon) {
170
+ return false;
171
+ }
172
+ return this.valueAsLuxon.hasSame(DateTime.local().plus(date), 'day');
205
173
  }
206
174
 
207
- dateLuxon(date) {
208
- return typeof date === 'string' ? DateTime.fromISO(date) : DateTime.fromJSDate(date);
175
+ get minDateLuxon() {
176
+ return this.minDate && (typeof this.minDate === 'string' ? DateTime.fromISO(this.minDate) : DateTime.fromJSDate(this.minDate));
209
177
  }
210
178
 
211
179
  isEnabled(date) {
212
- const lxDate1 = date[0].set({ hours: 0, minutes: 0, seconds: 0 });
213
- const lxDate2 = date[1].set({ hours: 23, minutes: 59, seconds: 59 });
214
-
215
- if (this.minDateLuxon) {
216
- if (this.maxDateLuxon) {
217
- return lxDate1 > this.minDateLuxon && lxDate2 > this.minDateLuxon && lxDate1 < this.maxDateLuxon && lxDate2 < this.maxDateLuxon;
218
- } else {
219
- return lxDate1 > this.minDateLuxon && lxDate2 > this.minDateLuxon;
220
- }
221
- } else if (this.maxDateLuxon) {
222
- return lxDate1 < this.maxDateLuxon && lxDate2 < this.maxDateLuxon;
223
- }
224
-
225
- return true;
226
- }
227
-
228
- isSelected(date) {
229
- if (!this.valueAsLuxon || this.valueAsLuxon.length < 2) {
230
- return false;
231
- }
232
- return this.valueAsLuxon[0].hasSame(date[0], 'day') &&
233
- this.valueAsLuxon[1].hasSame(date[1], 'day');
180
+ const lxDate = DateTime.local().plus(date);
181
+ return !(this.minDateLuxon && lxDate < this.minDateLuxon);
234
182
  }
235
183
 
236
184
  selectDate(date) {
237
- const lxDate1 = date[0].set({ hours: 0, minutes: 0, seconds: 0 });
238
- const lxDate2 = date[1].set({ hours: 23, minutes: 59, seconds: 59 });
239
- this.updateValue(lxDate1.toFormat(this.displayFormat), lxDate2.toFormat(this.displayFormat));
240
- this.calendar.selectDate([lxDate1.toJSDate(), lxDate2.toJSDate()], { silent: true });
241
- }
242
-
243
- @Watch('range')
244
- onRangeChange() {
245
- if (!this.calendar) {
185
+ let lxDate = DateTime.local().plus(date);
186
+ if (this.minDateLuxon && lxDate < this.minDateLuxon) {
246
187
  return;
247
188
  }
248
- this.calendar.selectCompareRange(this.range ? [
249
- DateTime.fromFormat(this.range[0], this.valueFormat).toJSDate(),
250
- DateTime.fromFormat(this.range[1], this.valueFormat).toJSDate(),
251
- ] : []);
189
+ this.calendar.selectDate(lxDate.toJSDate());
190
+ this.updateValue(lxDate.toFormat(this.displayFormat));
252
191
  }
253
192
 
254
193
  @Watch('value')
@@ -257,13 +196,7 @@ class itfDatePickerInline extends Vue {
257
196
  this.calendar && this.calendar.clear({ silent: true });
258
197
  return;
259
198
  }
260
- const date1 = this.calendar.rangeDateFrom && DateTime.fromJSDate(this.calendar.rangeDateFrom).toFormat('yyyy-MM-dd')
261
- const date2 = this.calendar.rangeDateTo && DateTime.fromJSDate(this.calendar.rangeDateTo).toFormat('yyyy-MM-dd')
262
- if (date1 !== this.valueAsLuxon[0].toFormat('yyyy-MM-dd') || date2 !== this.valueAsLuxon[1].toFormat('yyyy-MM-dd')) {
263
- this.$nextTick(() => {
264
- this.calendar.selectDate(this.valueAsLuxon && [this.valueAsLuxon[0].toJSDate(), this.valueAsLuxon[1].toJSDate()], { silent: true });
265
- });
266
- }
199
+ this.calendar.selectDate(this.valueAsLuxon.toJSDate(), { silent: true });
267
200
  }
268
201
  }
269
202
  </script>
@@ -0,0 +1,5 @@
1
+ <template><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M10 14L9 14C7.11438 14 6.17157 14 5.58579 13.4142C5 12.8284 5 11.8856 5 10L5 9C5 7.11438 5 6.17157 5.58579 5.58579C6.17157 5 7.11438 5 9 5L10 5C11.8856 5 12.8284 5 13.4142 5.58579C14 6.17157 14 7.11438 14 9L14 10C14 11.8856 14 12.8284 13.4142 13.4142C12.8284 14 11.8856 14 10 14Z" fill="currentColor"/>
3
+ <path d="M9.40039 14C9.40039 13.0742 9.39868 12.3195 9.47852 11.7256C9.56068 11.1144 9.73818 10.5841 10.1611 10.1611C10.5841 9.73818 11.1144 9.56068 11.7256 9.47852C12.3195 9.39868 13.0742 9.40039 14 9.40039L15 9.40039C15.9258 9.40039 16.6805 9.39868 17.2744 9.47852C17.8856 9.56068 18.4159 9.73818 18.8389 10.1611C19.2618 10.5841 19.4393 11.1144 19.5215 11.7256C19.6013 12.3195 19.5996 13.0742 19.5996 14L19.5996 15C19.5996 15.9258 19.6013 16.6805 19.5215 17.2744C19.4393 17.8856 19.2618 18.4159 18.8389 18.8389C18.4159 19.2618 17.8856 19.4393 17.2744 19.5215C16.6805 19.6013 15.9258 19.5996 15 19.5996L14 19.5996C13.0742 19.5996 12.3195 19.6013 11.7256 19.5215C11.1144 19.4393 10.5841 19.2618 10.1611 18.8389C9.73818 18.4159 9.56068 17.8856 9.47852 17.2744C9.39868 16.6805 9.40039 15.9258 9.40039 15L9.40039 14Z" fill="currentColor" stroke="white" stroke-width="1.2"/>
4
+ </svg>
5
+ </template>
@@ -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,20 +17,14 @@
17
17
  :icon="panel.icon"
18
18
  :payload="panel.payload"
19
19
  :expandable="panelsStack.length > 1"
20
- :isFullSize="isFullSize"
21
20
  :collapsed="panel.isCollapsed"
22
21
  :closeable="panel.isCloseable"
23
22
  :animate="panel.isAnimate"
24
23
  @open="openPanel($event[0], $event[1], n + 1)"
25
24
  @expand="expandPanel(panel)"
26
25
  @fullsize="fullsizePanel(panel)"
27
- @collapse="collapsePanel(panel)"
28
26
  @close="closePanel(panel)"
29
- @open-menu="$emit('open-menu', panel.type, panel.payload)"
30
27
  >
31
- <template #before-header>
32
- <slot name="before-header" :panel="panel" :index="n" :payload="panel.payload"></slot>
33
- </template>
34
28
  <slot
35
29
  :name="panel.type"
36
30
  :panel="panel"
@@ -40,9 +34,9 @@
40
34
  :close="() => closePanel(panel)"
41
35
  :expand="() => expandPanel(panel)"
42
36
  :fullsize="() => fullsizePanel(panel)">
43
- <component v-if="panel.components.default" :is="panel.components.default" :panel="panel" :payload="panel.payload" />
37
+ <component :is="panels[panel.type].default || panels[panel.type]" :panel="panel" :payload="panel.payload" />
44
38
  </slot>
45
- <template v-if="$scopedSlots[`${panel.type}.title`] || panel.components.title" #title>
39
+ <template v-if="$scopedSlots[`${panel.type}.title`] || panels[panel.type].title" #title>
46
40
  <slot
47
41
  :name="`${panel.type}.title`"
48
42
  :panel="panel"
@@ -52,10 +46,10 @@
52
46
  :close="() => closePanel(panel)"
53
47
  :expand="() => expandPanel(panel)"
54
48
  :fullsize="() => fullsizePanel(panel)">
55
- <component v-if="panel.components.title" :is="panel.components.title" :panel="panel" :payload="panel.payload" />
49
+ <component v-if="panels[panel.type].title" :is="panels[panel.type].title" :panel="panel" :payload="panel.payload" />
56
50
  </slot>
57
51
  </template>
58
- <template v-if="$scopedSlots[`${panel.type}.buttons`] || panel.components.buttons" #buttons>
52
+ <template v-if="$scopedSlots[`${panel.type}.buttons`] || panels[panel.type].buttons" #buttons>
59
53
  <slot
60
54
  :name="`${panel.type}.buttons`"
61
55
  :panel="panel"
@@ -65,10 +59,10 @@
65
59
  :close="() => closePanel(panel)"
66
60
  :expand="() => expandPanel(panel)"
67
61
  :fullsize="() => fullsizePanel(panel)">
68
- <component v-if="panel.components.buttons" :is="panel.components.buttons" :panel="panel" :payload="panel.payload" />
62
+ <component v-if="panels[panel.type].buttons" :is="panels[panel.type].buttons" :panel="panel" :payload="panel.payload" />
69
63
  </slot>
70
64
  </template>
71
- <template v-if="$scopedSlots[`${panel.type}.header`] || panel.components.header" #header>
65
+ <template v-if="$scopedSlots[`${panel.type}.header`] || panels[panel.type].header" #header>
72
66
  <slot
73
67
  :name="`${panel.type}.header`"
74
68
  :panel="panel"
@@ -78,7 +72,7 @@
78
72
  :close="() => closePanel(panel)"
79
73
  :expand="() => expandPanel(panel)"
80
74
  :fullsize="() => fullsizePanel(panel)">
81
- <component v-if="panel.components.header" :is="panel.components.header" :panel="panel" :payload="panel.payload" />
75
+ <component v-if="panels[panel.type].header" :is="panels[panel.type].header" :panel="panel" :payload="panel.payload" />
82
76
  </slot>
83
77
  </template>
84
78
  </panel>
@@ -154,6 +148,7 @@ $double-an-time: $an-time * 2;
154
148
  //transition: opacity $an-time linear;
155
149
  }
156
150
  }
151
+
157
152
  //.slide-enter-active > div {
158
153
  // opacity: 0;
159
154
  //}
@@ -164,10 +159,8 @@ $double-an-time: $an-time * 2;
164
159
  </style>
165
160
  <script lang="ts">
166
161
  import { Vue, Component, Prop } from 'vue-property-decorator';
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";
162
+ import Panel from './Panel';
163
+ import {hashToStack, stackToHash} from "./helpers";
171
164
 
172
165
  interface VisualOptions {
173
166
  title: string;
@@ -202,7 +195,6 @@ export interface IPanel {
202
195
 
203
196
  @Component({
204
197
  components: {
205
- itfIcon,
206
198
  Panel
207
199
  },
208
200
  directives: {
@@ -216,20 +208,15 @@ export interface IPanel {
216
208
  export default class PanelList extends Vue {
217
209
  @Prop() firstPanel: IPanel;
218
210
  @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;
222
211
 
223
212
  panelsStack:IPanel[] = [];
224
213
 
225
214
  nextId:number = 0;
226
215
 
227
216
  created() {
228
- setRootPanelList(this);
229
217
  if (this.firstPanel) {
230
218
  this.internalOpenPanel(this.firstPanel.type, this.firstPanel.payload);
231
219
  }
232
- console.info('created');
233
220
  this.parsePanelHash(); // щоб панелі змінювались при перезавантаженні
234
221
  window.addEventListener('popstate', this.handlePopState); // щоб панелі змінювались при навігації
235
222
  }
@@ -280,30 +267,18 @@ export default class PanelList extends Vue {
280
267
  this.panelsStack = newStack;
281
268
  }
282
269
 
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;
270
+ internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
271
+ if (!this.panels[type]) {
272
+ return;
292
273
  }
293
- if (typeof panel.caption !== 'function') {
274
+ if (typeof this.panels[type].caption !== 'function') {
294
275
  throw new Error('Panel component must have a "caption" function');
295
276
  }
296
277
  const newPanel:any = {
297
278
  id: this.nextId++,
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
- },
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,
307
282
  type,
308
283
  payload,
309
284
  isCollapsed: false,
@@ -314,7 +289,7 @@ export default class PanelList extends Vue {
314
289
  newPanel.isCloseable = false;
315
290
  }
316
291
  let newStack = [...this.panelsStack];
317
- if (panel.permanentExpanded && newStack.length) {
292
+ if (this.panels[type].permanentExpanded && newStack.length) {
318
293
  for (const panel of newStack) {
319
294
  panel.isCollapsed = true;
320
295
  }
@@ -324,32 +299,25 @@ export default class PanelList extends Vue {
324
299
  isAnimation = newStack.length === openIndex;
325
300
  newStack = newStack.slice(0, openIndex);
326
301
  }
327
- if (newStack.length > 0 && !newStack.find(p => !p.isCollapsed)) {
328
- // якщо немає відкритих панелей, то перша панель має бути розгорнута
329
- newStack[0].isCollapsed = false;
330
- }
331
302
  this.panelsStack = newStack;
332
303
  return new Promise(res => {
333
304
  this.$nextTick(() => { // щоб панелі змінювались при редагуванні
334
305
  const n = newStack.length;
335
306
  newPanel.isAnimate = isAnimation;
336
- newPanel.permanentExpanded = !!panel.permanentExpanded;
307
+ newPanel.permanentExpanded = !!this.panels[type].permanentExpanded;
337
308
  newPanel.emit = (event, ...args) => this.emitEvent(event, ...args);
338
- newPanel.open = (type, payload, index?:number) => this.openPanel(type, payload, index ?? n + 1);
309
+ newPanel.open = (type, payload) => this.openPanel(type, payload, n + 1);
339
310
  newPanel.close = () => this.closePanel(newPanel);
340
311
  newPanel.expand = () => this.expandPanel(newPanel);
341
312
  newPanel.getTitle = () => newPanel.title;
342
313
  newPanel.getIcon = () => newPanel.icon;
343
- newPanel.setTitle = (title: string) => { newPanel.title = title; this.updateTitle() };
314
+ newPanel.setTitle = (title: string) => { newPanel.title = title; };
344
315
  newPanel.setIcon = (icon: string) => { newPanel.icon = icon; };
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);
316
+ newPanel.on = (eventName, func: (event: string, ...args: any[]) => any) => {
317
+ if (!newPanel.__events[eventName]) {
318
+ newPanel.__events[eventName] = [];
352
319
  }
320
+ newPanel.__events[eventName].push(func);
353
321
  };
354
322
  newPanel.off = (eventName, func: (event: string, ...args: any[]) => any) => {
355
323
  if (newPanel.__events[eventName]) {
@@ -369,7 +337,9 @@ export default class PanelList extends Vue {
369
337
  newPanel.getPayload = () => newPanel.payload;
370
338
  newPanel.setPayload = (value: any) => {
371
339
  newPanel.payload = value;
372
- this.setPanelHash();
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()
373
343
  }
374
344
  newStack.push(newPanel);
375
345
  this.panelsStack = newStack;
@@ -383,19 +353,12 @@ export default class PanelList extends Vue {
383
353
  });
384
354
  }
385
355
 
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
-
392
356
  async openPanel(type: string, payload: any, openIndex?: number) {
393
357
  await this.internalOpenPanel(type, payload, openIndex);
394
- this.setPanelHash();
358
+ this.setPanelHash()
395
359
  }
396
360
 
397
361
  emitEvent(event: string, ...args: any[]) {
398
- emitGlobalEvent(event, ...args);
399
362
  for (const panel of this.panelsStack) {
400
363
  if (panel.__events[event]) {
401
364
  for (const func of panel.__events[event]) {
@@ -425,40 +388,12 @@ export default class PanelList extends Vue {
425
388
  fullsizePanel(panel: IPanel) {
426
389
  const newStack = [...this.panelsStack];
427
390
  for (const p of newStack) {
428
- p.isLastOpened = !p.isCollapsed && p !== panel;
429
391
  p.isCollapsed = p !== panel;
430
392
  }
431
393
  this.panelsStack = newStack;
432
394
  this.setPanelHash();
433
395
  }
434
396
 
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
-
462
397
  getPanels(type) {
463
398
  return this.panelsStack.filter(panel => panel.type === type);
464
399
  }
@@ -468,19 +403,14 @@ export default class PanelList extends Vue {
468
403
  }
469
404
 
470
405
  setPanelHash() {
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();
406
+ const hash = stackToHash(this.panelsStack).replace(/^#/, '');
407
+ this.$router.push({ hash });
478
408
  }
479
409
 
480
410
  async parsePanelHash() {
481
- const hash = this.routeType === 'path' ? location.pathname : location.hash;
411
+ const {hash} = location;
482
412
  if (hash) {
483
- const panels = hashToStack(hash, this.routePrefix);
413
+ const panels = hashToStack(hash);
484
414
  const newStack = [];
485
415
  this.panelsStack = [];
486
416
  for (const panelIndex in panels) {
@@ -500,14 +430,11 @@ export default class PanelList extends Vue {
500
430
  }
501
431
  }
502
432
  this.panelsStack = newStack;
503
- console.info('set', newStack);
504
433
  this.emitEvent('panels.changed', this.panelsStack);
505
- this.updateTitle();
506
434
  }
507
435
  }
508
436
 
509
437
  handlePopState() {
510
- console.info('handlePopState')
511
438
  this.parsePanelHash();
512
439
  // виправляє проблему відкритої панелі при натисканні кнопки "назад" до першої панелі
513
440
  if (this.panelsStack.length === 2) {
@@ -1,57 +1,33 @@
1
- import JSON5 from 'json5'
2
- import {isPathType} from "./index";
3
-
4
1
  export interface IPanel {
5
2
  type: string;
6
3
  payload?: any;
7
4
  isCollapsed?: boolean;
8
5
  }
9
6
 
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}`;
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}`;
23
12
  }
24
13
 
25
14
 
26
- export function hashToStack(hash: string|undefined, prefix: string = ''): IPanel[] {
15
+ export function hashToStack(hash: string|undefined): IPanel[] {
27
16
  let stack:IPanel[] = [];
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);
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('!');
39
23
  let payloadObj:any = {};
40
24
  try {
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);
25
+ payloadObj = JSON.parse(decodeURIComponent(payload));
49
26
  } catch (e) {
50
27
  // ignore
51
- console.warn(`Error parsing payload for type ${type}:`, payload, e);
52
28
  }
53
29
  return {
54
- type: type.replace(COLLAPSE_SYMBOL, ''),
30
+ type: type.replace('!', ''),
55
31
  isCollapsed,
56
32
  payload: payloadObj
57
33
  };