@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 +1 -1
- package/src/components/datepicker/DatePickerInline.vue +62 -129
- package/src/components/icon/components/nomi-duplicate.vue +5 -0
- package/src/components/panels/PanelLink.vue +15 -6
- package/src/components/panels/PanelList.vue +33 -106
- package/src/components/panels/helpers.ts +14 -38
package/package.json
CHANGED
|
@@ -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 [
|
|
45
|
-
{ title: this.$t('components.
|
|
46
|
-
{ title: this.$t('components.
|
|
47
|
-
{ title: this.$t('components.
|
|
48
|
-
{ title: this.$t('components.
|
|
49
|
-
{ title: this.$t('components.
|
|
50
|
-
{ title: this.$t('components.
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
|
138
|
+
if (!this.value) {
|
|
162
139
|
return null;
|
|
163
140
|
}
|
|
164
141
|
if (this.valueFormat === 'ISO') {
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
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(
|
|
177
|
-
const val =
|
|
178
|
-
|
|
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',
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return this.
|
|
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
|
-
|
|
208
|
-
return typeof
|
|
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
|
|
213
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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.
|
|
249
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
168
|
-
import
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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:
|
|
299
|
-
title:
|
|
300
|
-
icon:
|
|
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 (
|
|
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 = !!
|
|
307
|
+
newPanel.permanentExpanded = !!this.panels[type].permanentExpanded;
|
|
337
308
|
newPanel.emit = (event, ...args) => this.emitEvent(event, ...args);
|
|
338
|
-
newPanel.open = (type, payload
|
|
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;
|
|
314
|
+
newPanel.setTitle = (title: string) => { newPanel.title = title; };
|
|
344
315
|
newPanel.setIcon = (icon: string) => { newPanel.icon = icon; };
|
|
345
|
-
newPanel.on = (eventName
|
|
346
|
-
|
|
347
|
-
|
|
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.
|
|
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
|
|
472
|
-
|
|
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 =
|
|
411
|
+
const {hash} = location;
|
|
482
412
|
if (hash) {
|
|
483
|
-
const panels = hashToStack(hash
|
|
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
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
15
|
+
export function hashToStack(hash: string|undefined): IPanel[] {
|
|
27
16
|
let stack:IPanel[] = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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(
|
|
30
|
+
type: type.replace('!', ''),
|
|
55
31
|
isCollapsed,
|
|
56
32
|
payload: payloadObj
|
|
57
33
|
};
|