@itfin/components 1.4.36 → 1.5.0

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.
Files changed (204) hide show
  1. package/package.json +17 -20
  2. package/src/ITFSettings.js +6 -0
  3. package/src/assets/scss/_css_variables.scss +2 -7
  4. package/src/assets/scss/_dark-theme.scss +2 -12
  5. package/src/assets/scss/_variables.scss +34 -9
  6. package/src/assets/scss/components/_button.scss +147 -10
  7. package/src/assets/scss/components/_checkbox.scss +9 -0
  8. package/src/assets/scss/components/_datepicker.scss +3 -3
  9. package/src/assets/scss/components/_pagination.scss +4 -1
  10. package/src/assets/scss/components/_popover.scss +22 -0
  11. package/src/assets/scss/components/_segmeneted-control.scss +19 -8
  12. package/src/assets/scss/components/_select.scss +6 -8
  13. package/src/assets/scss/components/_text-field.scss +27 -11
  14. package/src/assets/scss/components/select/_dropdown-menu.scss +1 -0
  15. package/src/assets/scss/components/select/_dropdown-toggle.scss +0 -1
  16. package/src/assets/scss/directives/tooltip.scss +10 -5
  17. package/src/assets/scss/main.scss +48 -0
  18. package/src/components/alert/AlertBanner.vue +75 -0
  19. package/src/components/app/App.vue +6 -3
  20. package/src/components/button/Button.vue +3 -1
  21. package/src/components/button/NativeButton.js +4 -0
  22. package/src/components/button/index.stories.js +2 -2
  23. package/src/components/checkbox/Checkbox.vue +2 -1
  24. package/src/components/checkbox/RadioBox.vue +13 -7
  25. package/src/components/copyToClipboard/CopyToClipboard.vue +4 -1
  26. package/src/components/customize/PropertiesList.vue +0 -2
  27. package/src/components/customize/PropertiesPopupMenu.vue +1 -1
  28. package/src/components/customize/PropertyItem.vue +6 -24
  29. package/src/components/datepicker/DatePicker.vue +3 -1
  30. package/src/components/datepicker/DatePickerInline.vue +2 -2
  31. package/src/components/datepicker/DateRangePickerInline.vue +6 -1
  32. package/src/components/dropdown/Dropdown.vue +1 -1
  33. package/src/components/dropdown/DropdownMenu.vue +1 -1
  34. package/src/components/editable/EditButton.vue +1 -1
  35. package/src/components/filter/FilterBadge.vue +20 -1
  36. package/src/components/filter/FilterFacetsList.vue +67 -13
  37. package/src/components/filter/FilterPanel.vue +8 -4
  38. package/src/components/filter/NewFilter.vue +305 -0
  39. package/src/components/form/Label.vue +5 -5
  40. package/src/components/icon/components/nomi-ai-alt.vue +5 -0
  41. package/src/components/icon/components/nomi-arrow-right.vue +4 -0
  42. package/src/components/icon/components/nomi-bell.vue +5 -0
  43. package/src/components/icon/components/nomi-calendar-payment.vue +10 -0
  44. package/src/components/icon/components/nomi-card-plus.vue +1 -0
  45. package/src/components/icon/components/nomi-cash-provider.vue +9 -0
  46. package/src/components/icon/components/nomi-cash-repeat.vue +6 -0
  47. package/src/components/icon/components/nomi-category-edit.vue +5 -0
  48. package/src/components/icon/components/nomi-chavron-up.vue +4 -0
  49. package/src/components/icon/components/nomi-chavron_down.vue +4 -0
  50. package/src/components/icon/components/nomi-chavron_up.vue +4 -0
  51. package/src/components/icon/components/nomi-chevron-up.vue +4 -0
  52. package/src/components/icon/components/nomi-exit-right.vue +4 -0
  53. package/src/components/icon/components/nomi-help.vue +2 -3
  54. package/src/components/icon/components/nomi-history.vue +7 -0
  55. package/src/components/icon/components/nomi-lock.vue +1 -1
  56. package/src/components/icon/components/nomi-pen-alt.vue +4 -0
  57. package/src/components/icon/components/nomi-project.vue +2 -2
  58. package/src/components/icon/components/nomi-refresh-off.vue +4 -0
  59. package/src/components/icon/components/nomi-refresh.vue +4 -0
  60. package/src/components/icon/components/nomi-scissors.vue +1 -1
  61. package/src/components/icon/components/nomi-start.vue +28 -0
  62. package/src/components/icon/components/nomi-table-view.vue +1 -4
  63. package/src/components/icon/components/nomi-transactions-delete.vue +5 -0
  64. package/src/components/icon/components/nomi-type-array.vue +6 -0
  65. package/src/components/icon/components/nomi-type-boolean.vue +5 -0
  66. package/src/components/icon/components/nomi-type-date.vue +4 -0
  67. package/src/components/icon/components/nomi-type-null.vue +4 -0
  68. package/src/components/icon/components/nomi-type-number.vue +4 -0
  69. package/src/components/icon/components/nomi-type-object.vue +4 -0
  70. package/src/components/icon/components/nomi-type-string.vue +4 -0
  71. package/src/components/icon/components/nomi-unarchive.vue +17 -0
  72. package/src/components/icon/components/nomi-unlink.vue +10 -0
  73. package/src/components/icon/components/nomi-user.vue +3 -3
  74. package/src/components/icon/components/nomi-warning-triangle.vue +6 -0
  75. package/src/components/icon/components/nomi-warning_triangle_filled.vue +6 -0
  76. package/src/components/icon/convert-icons.js +3 -0
  77. package/src/components/icon/icons.js +390 -312
  78. package/src/components/icon/new-icons/ai-alt.svg +4 -0
  79. package/src/components/icon/new-icons/arrow-right-alt.svg +3 -0
  80. package/src/components/icon/new-icons/arrow-right.svg +3 -0
  81. package/src/components/icon/new-icons/arrow_left.svg +3 -0
  82. package/src/components/icon/new-icons/automation.svg +4 -0
  83. package/src/components/icon/new-icons/balance.svg +3 -0
  84. package/src/components/icon/new-icons/balance_turnover.svg +4 -0
  85. package/src/components/icon/new-icons/bar-horizontal.svg +6 -0
  86. package/src/components/icon/new-icons/bell.svg +4 -0
  87. package/src/components/icon/new-icons/calendar-payment.svg +9 -0
  88. package/src/components/icon/new-icons/card-plus.svg +1 -0
  89. package/src/components/icon/new-icons/cash-provider.svg +8 -0
  90. package/src/components/icon/new-icons/cash-repeat.svg +5 -0
  91. package/src/components/icon/new-icons/cash.svg +3 -0
  92. package/src/components/icon/new-icons/cashflow.svg +3 -0
  93. package/src/components/icon/new-icons/category-edit.svg +4 -0
  94. package/src/components/icon/new-icons/category.svg +4 -0
  95. package/src/components/icon/new-icons/category_alt.svg +3 -0
  96. package/src/components/icon/new-icons/chart-bars.svg +5 -0
  97. package/src/components/icon/new-icons/chart-donut.svg +3 -0
  98. package/src/components/icon/new-icons/chart-funnel.svg +5 -0
  99. package/src/components/icon/new-icons/chart-kpi.svg +7 -0
  100. package/src/components/icon/new-icons/chart-line.svg +4 -0
  101. package/src/components/icon/new-icons/chart-lines.svg +5 -0
  102. package/src/components/icon/new-icons/check-alt.svg +3 -0
  103. package/src/components/icon/new-icons/check.svg +3 -0
  104. package/src/components/icon/new-icons/chevron-down.svg +3 -0
  105. package/src/components/icon/new-icons/chevron-left.svg +3 -0
  106. package/src/components/icon/new-icons/chevron-right.svg +3 -0
  107. package/src/components/icon/new-icons/chevron-up.svg +3 -0
  108. package/src/components/icon/new-icons/collapse.svg +6 -0
  109. package/src/components/icon/new-icons/control-panel.svg +7 -0
  110. package/src/components/icon/new-icons/credit.svg +3 -0
  111. package/src/components/icon/new-icons/currencies.svg +3 -0
  112. package/src/components/icon/new-icons/debt.svg +3 -0
  113. package/src/components/icon/new-icons/demo.svg +6 -0
  114. package/src/components/icon/new-icons/dev.svg +3 -0
  115. package/src/components/icon/new-icons/dots.svg +5 -0
  116. package/src/components/icon/new-icons/duplicate.svg +4 -0
  117. package/src/components/icon/new-icons/exit-right.svg +3 -0
  118. package/src/components/icon/new-icons/export.svg +3 -0
  119. package/src/components/icon/new-icons/file.svg +3 -0
  120. package/src/components/icon/new-icons/folder.svg +3 -0
  121. package/src/components/icon/new-icons/goods-turnover.svg +3 -0
  122. package/src/components/icon/new-icons/goods.svg +4 -0
  123. package/src/components/icon/new-icons/help-alt.svg +3 -0
  124. package/src/components/icon/new-icons/help.svg +2 -3
  125. package/src/components/icon/new-icons/history.svg +6 -0
  126. package/src/components/icon/new-icons/integration.svg +3 -0
  127. package/src/components/icon/new-icons/link.svg +5 -0
  128. package/src/components/icon/new-icons/lock.svg +1 -1
  129. package/src/components/icon/new-icons/menu.svg +5 -0
  130. package/src/components/icon/new-icons/minus.svg +3 -0
  131. package/src/components/icon/new-icons/payment_calendar.svg +3 -0
  132. package/src/components/icon/new-icons/pc.svg +3 -0
  133. package/src/components/icon/new-icons/pen-alt.svg +3 -0
  134. package/src/components/icon/new-icons/planFact.svg +4 -0
  135. package/src/components/icon/new-icons/pnl.svg +7 -0
  136. package/src/components/icon/new-icons/project.svg +2 -2
  137. package/src/components/icon/new-icons/project_alt.svg +3 -0
  138. package/src/components/icon/new-icons/project_alt2.svg +3 -0
  139. package/src/components/icon/new-icons/promo.svg +3 -0
  140. package/src/components/icon/new-icons/refresh-off.svg +3 -0
  141. package/src/components/icon/new-icons/refresh.svg +3 -0
  142. package/src/components/icon/new-icons/scissors.svg +1 -1
  143. package/src/components/icon/new-icons/segment.svg +3 -0
  144. package/src/components/icon/new-icons/start.svg +27 -0
  145. package/src/components/icon/new-icons/strongbox.svg +3 -0
  146. package/src/components/icon/new-icons/subscription.svg +3 -0
  147. package/src/components/icon/new-icons/table-view.svg +1 -4
  148. package/src/components/icon/new-icons/time.svg +3 -0
  149. package/src/components/icon/new-icons/transactions_alt.svg +6 -0
  150. package/src/components/icon/new-icons/transactions_delete.svg +4 -0
  151. package/src/components/icon/new-icons/type-array.svg +5 -0
  152. package/src/components/icon/new-icons/type-boolean.svg +4 -0
  153. package/src/components/icon/new-icons/type-date.svg +3 -0
  154. package/src/components/icon/new-icons/type-null.svg +3 -0
  155. package/src/components/icon/new-icons/type-number.svg +3 -0
  156. package/src/components/icon/new-icons/type-object.svg +3 -0
  157. package/src/components/icon/new-icons/type-string.svg +3 -0
  158. package/src/components/icon/new-icons/types.svg +6 -0
  159. package/src/components/icon/new-icons/unarchive.svg +16 -0
  160. package/src/components/icon/new-icons/unlink.svg +9 -0
  161. package/src/components/icon/new-icons/user.svg +3 -3
  162. package/src/components/icon/new-icons/user_plus.svg +10 -0
  163. package/src/components/icon/new-icons/warehouse.svg +3 -0
  164. package/src/components/icon/new-icons/warning_triangle.svg +5 -0
  165. package/src/components/icon/new-icons/warning_triangle_filled.svg +5 -0
  166. package/src/components/kanban/BoardCard.vue +1 -1
  167. package/src/components/kanban/BoardCardTimer.vue +1 -1
  168. package/src/components/modal/DeleteConfirmModal.vue +10 -6
  169. package/src/components/modal/ItemEditor.vue +1 -1
  170. package/src/components/modal/Modal.vue +1 -1
  171. package/src/components/overlay/SensitiveOverlay.vue +2 -4
  172. package/src/components/panels/Panel.vue +110 -23
  173. package/src/components/panels/PanelItemEdit.vue +8 -6
  174. package/src/components/panels/PanelList.vue +163 -40
  175. package/src/components/panels/helpers.ts +29 -11
  176. package/src/components/popover/Popover.vue +105 -22
  177. package/src/components/segmented-control/SegmentedControl.vue +9 -3
  178. package/src/components/sortable/draggable.js +1 -1
  179. package/src/components/table/Table2.vue +68 -67
  180. package/src/components/table/TableBody.vue +17 -22
  181. package/src/components/table/TableGroup.vue +40 -24
  182. package/src/components/table/TableHeader.vue +86 -81
  183. package/src/components/table/TableRowToggle.vue +1 -9
  184. package/src/components/table/TableRows.vue +49 -55
  185. package/src/components/table/mobile.js +4 -0
  186. package/src/components/table/table2.scss +34 -15
  187. package/src/components/text-field/MoneyField.vue +10 -4
  188. package/src/components/text-field/TextField.vue +17 -8
  189. package/src/components/tree/TreeEditor.vue +3 -2
  190. package/src/components/view/View.vue +73 -208
  191. package/src/directives/appendToBody.js +1 -0
  192. package/src/helpers/validators.js +9 -35
  193. package/src/helpers/validators.spec.js +11 -48
  194. package/src/locales/en.js +4 -6
  195. package/src/locales/pl.js +158 -0
  196. package/src/locales/uk.js +6 -7
  197. package/src/components/icon/components/nomi-calendar-view.vue +0 -4
  198. package/src/components/icon/components/nomi-kanban-view.vue +0 -6
  199. package/src/components/icon/components/nomi-list-view.vue +0 -7
  200. package/src/components/icon/components/nomi-table-config.vue +0 -9
  201. package/src/components/icon/new-icons/calendar-view.svg +0 -3
  202. package/src/components/icon/new-icons/kanban-view.svg +0 -5
  203. package/src/components/icon/new-icons/list-view.svg +0 -6
  204. package/src/components/icon/new-icons/table-config.svg +0 -8
@@ -10,6 +10,7 @@
10
10
  <template v-for="(panel, n) of panelsStack">
11
11
  <panel
12
12
  :key="n"
13
+ :ref="`panel-${panel.id}`"
13
14
  :index="n"
14
15
  :panel="panel"
15
16
  :title="panel.title"
@@ -17,14 +18,22 @@
17
18
  :icon="panel.icon"
18
19
  :payload="panel.payload"
19
20
  :expandable="panelsStack.length > 1"
21
+ :isFullSize="isFullSize"
20
22
  :collapsed="panel.isCollapsed"
21
23
  :closeable="panel.isCloseable"
22
24
  :animate="panel.isAnimate"
23
25
  @open="openPanel($event[0], $event[1], n + 1)"
24
26
  @expand="expandPanel(panel)"
25
27
  @fullsize="fullsizePanel(panel)"
28
+ @collapse="collapsePanel(panel)"
26
29
  @close="closePanel(panel)"
30
+ @open-menu="$emit('open-menu', panel.type, panel.payload)"
31
+ @onboarding-progress="$emit('onboarding-progress', $event)"
32
+ @onboarding-step-viewed="$emit('onboarding-step-viewed', $event)"
27
33
  >
34
+ <template #before-header>
35
+ <slot name="before-header" :panel="panel" :index="n" :payload="panel.payload"></slot>
36
+ </template>
28
37
  <slot
29
38
  :name="panel.type"
30
39
  :panel="panel"
@@ -34,9 +43,9 @@
34
43
  :close="() => closePanel(panel)"
35
44
  :expand="() => expandPanel(panel)"
36
45
  :fullsize="() => fullsizePanel(panel)">
37
- <component :is="panels[panel.type].default || panels[panel.type]" :panel="panel" :payload="panel.payload" />
46
+ <component v-if="panel.components.default" :is="panel.components.default" :panel="panel" :payload="panel.payload" />
38
47
  </slot>
39
- <template v-if="$scopedSlots[`${panel.type}.title`] || panels[panel.type].title" #title>
48
+ <template v-if="$scopedSlots[`${panel.type}.title`] || panel.components.title" #title>
40
49
  <slot
41
50
  :name="`${panel.type}.title`"
42
51
  :panel="panel"
@@ -46,10 +55,10 @@
46
55
  :close="() => closePanel(panel)"
47
56
  :expand="() => expandPanel(panel)"
48
57
  :fullsize="() => fullsizePanel(panel)">
49
- <component v-if="panels[panel.type].title" :is="panels[panel.type].title" :panel="panel" :payload="panel.payload" />
58
+ <component v-if="panel.components.title" :is="panel.components.title" :panel="panel" :payload="panel.payload" />
50
59
  </slot>
51
60
  </template>
52
- <template v-if="$scopedSlots[`${panel.type}.buttons`] || panels[panel.type].buttons" #buttons>
61
+ <template v-if="$scopedSlots[`${panel.type}.buttons`] || panel.components.buttons" #buttons>
53
62
  <slot
54
63
  :name="`${panel.type}.buttons`"
55
64
  :panel="panel"
@@ -59,10 +68,10 @@
59
68
  :close="() => closePanel(panel)"
60
69
  :expand="() => expandPanel(panel)"
61
70
  :fullsize="() => fullsizePanel(panel)">
62
- <component v-if="panels[panel.type].buttons" :is="panels[panel.type].buttons" :panel="panel" :payload="panel.payload" />
71
+ <component v-if="panel.components.buttons" :is="panel.components.buttons" :panel="panel" :payload="panel.payload" />
63
72
  </slot>
64
73
  </template>
65
- <template v-if="$scopedSlots[`${panel.type}.header`] || panels[panel.type].header" #header>
74
+ <template v-if="$scopedSlots[`${panel.type}.header`] || panel.components.header" #header>
66
75
  <slot
67
76
  :name="`${panel.type}.header`"
68
77
  :panel="panel"
@@ -72,7 +81,7 @@
72
81
  :close="() => closePanel(panel)"
73
82
  :expand="() => expandPanel(panel)"
74
83
  :fullsize="() => fullsizePanel(panel)">
75
- <component v-if="panels[panel.type].header" :is="panels[panel.type].header" :panel="panel" :payload="panel.payload" />
84
+ <component v-if="panel.components.header" :is="panel.components.header" :panel="panel" :payload="panel.payload" />
76
85
  </slot>
77
86
  </template>
78
87
  </panel>
@@ -148,7 +157,6 @@ $double-an-time: $an-time * 2;
148
157
  //transition: opacity $an-time linear;
149
158
  }
150
159
  }
151
-
152
160
  //.slide-enter-active > div {
153
161
  // opacity: 0;
154
162
  //}
@@ -158,9 +166,11 @@ $double-an-time: $an-time * 2;
158
166
  //}
159
167
  </style>
160
168
  <script lang="ts">
161
- import { Vue, Component, Prop } from 'vue-property-decorator';
162
- import Panel from './Panel';
163
- import {hashToStack, stackToHash} from "./helpers";
169
+ import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
170
+ import itfIcon from '../icon/Icon.vue';
171
+ import Panel from './Panel.vue';
172
+ import {hashToStack, stackToHash} from "@itfin/components/src/components/panels/helpers";
173
+ import {emitGlobalEvent, setRootPanelList} from "@itfin/components/src/components/panels";
164
174
 
165
175
  interface VisualOptions {
166
176
  title: string;
@@ -175,6 +185,7 @@ export interface IPanel {
175
185
  payload: any;
176
186
  nocard?: boolean;
177
187
  isCollapsed: boolean;
188
+ isExpanded: boolean;
178
189
  isCloseable: boolean;
179
190
  isAnimate: boolean;
180
191
  open: (type: string, visOptions: VisualOptions, payload: any) => void;
@@ -195,6 +206,7 @@ export interface IPanel {
195
206
 
196
207
  @Component({
197
208
  components: {
209
+ itfIcon,
198
210
  Panel
199
211
  },
200
212
  directives: {
@@ -208,12 +220,26 @@ export interface IPanel {
208
220
  export default class PanelList extends Vue {
209
221
  @Prop() firstPanel: IPanel;
210
222
  @Prop() panels: Record<string, Component>;
223
+ @Prop({ default: () => {} }) searchPanel: (type: string) => boolean;
224
+ @Prop({ type: String, default: 'path' }) routeType: string;
211
225
 
212
226
  panelsStack:IPanel[] = [];
213
227
 
214
228
  nextId:number = 0;
215
229
 
230
+ @Watch('panels', { deep: true })
231
+ onPanelsPropChanged(newPanels: Record<string, Component>) {
232
+ this.panelsStack.forEach(stackedPanel => {
233
+ const newPanel = newPanels[stackedPanel.type];
234
+ if (newPanel && typeof newPanel.inAppOnboarding === 'function') {
235
+ const updatedOnboarding = newPanel.inAppOnboarding(this.$t.bind(this));
236
+ stackedPanel.inAppOnboarding = updatedOnboarding;
237
+ }
238
+ });
239
+ }
240
+
216
241
  created() {
242
+ setRootPanelList(this);
217
243
  if (this.firstPanel) {
218
244
  this.internalOpenPanel(this.firstPanel.type, this.firstPanel.payload);
219
245
  }
@@ -233,6 +259,7 @@ export default class PanelList extends Vue {
233
259
  const newStack = [...this.panelsStack];
234
260
  const index = newStack.findIndex(p => p.id === panel.id);
235
261
  newStack[index].isCollapsed = false;
262
+ newStack[index].isExpanded = true;
236
263
  this.panelsStack = newStack;
237
264
  this.ensureOnlyTwoOpenPanels(panel.id);
238
265
  this.setPanelHash();
@@ -254,10 +281,12 @@ export default class PanelList extends Vue {
254
281
  keepOpenIds.push(panel2.id);
255
282
  }
256
283
  if (keepOpenIds.length === 1) {
257
- if (newStack.length - 1 === indexKeep) {
258
- keepOpenIds.push(newStack[indexKeep - 1].id);
259
- } else {
260
- keepOpenIds.push(newStack[indexKeep + 1].id);
284
+ if (!openPanels.find(p => p.id === keepOpenId).isExpanded) {
285
+ if (newStack.length - 1 === indexKeep) {
286
+ keepOpenIds.push(newStack[indexKeep - 1].id);
287
+ } else {
288
+ keepOpenIds.push(newStack[indexKeep + 1].id);
289
+ }
261
290
  }
262
291
  }
263
292
  for (const panel of newStack) {
@@ -267,29 +296,42 @@ export default class PanelList extends Vue {
267
296
  this.panelsStack = newStack;
268
297
  }
269
298
 
270
- internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
271
- if (!this.panels[type]) {
272
- return;
299
+ async internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false, noExpand = false) {
300
+ let panel = this.panels[type];
301
+ if (!panel) {
302
+ panel = await this.searchPanel(type, this.panels);
303
+ if (!panel) {
304
+ console.error(`Panel type "${type}" not found`);
305
+ return;
306
+ }
307
+ panel.type = type;
273
308
  }
274
- if (typeof this.panels[type].caption !== 'function') {
309
+ if (typeof panel.caption !== 'function') {
275
310
  throw new Error('Panel component must have a "caption" function');
276
311
  }
277
312
  const newPanel:any = {
278
313
  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,
314
+ nocard: panel.nocard,
315
+ title: panel.caption(this.$t.bind(this), payload),
316
+ icon: panel.icon ? panel.icon(this.$t.bind(this), payload) : null,
317
+ components: {
318
+ default: panel.default ?? undefined,
319
+ buttons: panel.buttons ?? undefined,
320
+ header: panel.header ?? undefined,
321
+ title: panel.title ?? undefined,
322
+ },
282
323
  type,
283
324
  payload,
284
325
  isCollapsed: false,
285
326
  isCloseable: true,
286
327
  __events: {},
328
+ inAppOnboarding: panel.inAppOnboarding ? panel.inAppOnboarding(this.$t.bind(this)) : null,
287
329
  };
288
330
  if (!this.panelsStack.length || openIndex === 0) {
289
331
  newPanel.isCloseable = false;
290
332
  }
291
333
  let newStack = [...this.panelsStack];
292
- if (this.panels[type].permanentExpanded && newStack.length) {
334
+ if (panel.permanentExpanded && newStack.length) {
293
335
  for (const panel of newStack) {
294
336
  panel.isCollapsed = true;
295
337
  }
@@ -299,25 +341,32 @@ export default class PanelList extends Vue {
299
341
  isAnimation = newStack.length === openIndex;
300
342
  newStack = newStack.slice(0, openIndex);
301
343
  }
344
+ if (newStack.length > 0 && !newStack.find(p => !p.isCollapsed) && !noExpand) {
345
+ // якщо немає відкритих панелей, то перша панель має бути розгорнута
346
+ newStack[0].isCollapsed = false;
347
+ }
302
348
  this.panelsStack = newStack;
303
349
  return new Promise(res => {
304
350
  this.$nextTick(() => { // щоб панелі змінювались при редагуванні
305
351
  const n = newStack.length;
306
352
  newPanel.isAnimate = isAnimation;
307
- newPanel.permanentExpanded = !!this.panels[type].permanentExpanded;
353
+ newPanel.permanentExpanded = !!panel.permanentExpanded;
308
354
  newPanel.emit = (event, ...args) => this.emitEvent(event, ...args);
309
- newPanel.open = (type, payload) => this.openPanel(type, payload, n + 1);
355
+ newPanel.open = (type, payload, index?:number) => this.openPanel(type, payload, index ?? n + 1);
310
356
  newPanel.close = () => this.closePanel(newPanel);
311
357
  newPanel.expand = () => this.expandPanel(newPanel);
312
358
  newPanel.getTitle = () => newPanel.title;
313
359
  newPanel.getIcon = () => newPanel.icon;
314
- newPanel.setTitle = (title: string) => { newPanel.title = title; };
360
+ newPanel.setTitle = (title: string) => { newPanel.title = title; this.updateTitle() };
315
361
  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] = [];
362
+ newPanel.on = (eventName: string|string[], func: (event: string, ...args: any[]) => any) => {
363
+ const eventNames = Array.isArray(eventName) ? eventName : [eventName];
364
+ for (const evName of eventNames) {
365
+ if (!newPanel.__events[evName]) {
366
+ newPanel.__events[evName] = [];
367
+ }
368
+ newPanel.__events[evName].push(func);
319
369
  }
320
- newPanel.__events[eventName].push(func);
321
370
  };
322
371
  newPanel.off = (eventName, func: (event: string, ...args: any[]) => any) => {
323
372
  if (newPanel.__events[eventName]) {
@@ -337,9 +386,11 @@ export default class PanelList extends Vue {
337
386
  newPanel.getPayload = () => newPanel.payload;
338
387
  newPanel.setPayload = (value: any) => {
339
388
  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()
389
+ this.setPanelHash();
390
+ }
391
+ newPanel.rebuildAllOnboardingPopovers = (refreshDelay?: number) => this.rebuildAllOnboardingPopovers(refreshDelay);
392
+ newPanel.rebuildPanelOnboardingPopovers = (panelId: string | number, refreshDelay?: number) => {
393
+ this.rebuildPanelOnboardingPopovers(panelId, refreshDelay);
343
394
  }
344
395
  newStack.push(newPanel);
345
396
  this.panelsStack = newStack;
@@ -353,12 +404,20 @@ export default class PanelList extends Vue {
353
404
  });
354
405
  }
355
406
 
407
+ updateTitle() {
408
+ const titles = this.panelsStack.map(p => p.getTitle()).filter(Boolean).reverse();
409
+ this.$root.$options.head.titleChunk = titles.join(' / ');
410
+ this.$meta().refresh();
411
+ }
412
+
356
413
  async openPanel(type: string, payload: any, openIndex?: number) {
357
414
  await this.internalOpenPanel(type, payload, openIndex);
358
- this.setPanelHash()
415
+ this.setPanelHash();
416
+ if(openIndex) this.$nextTick(() => { this.rebuildAllOnboardingPopovers() });
359
417
  }
360
418
 
361
419
  emitEvent(event: string, ...args: any[]) {
420
+ emitGlobalEvent(event, ...args);
362
421
  for (const panel of this.panelsStack) {
363
422
  if (panel.__events[event]) {
364
423
  for (const func of panel.__events[event]) {
@@ -376,22 +435,63 @@ export default class PanelList extends Vue {
376
435
  openPanel.isCollapsed = false;
377
436
  }
378
437
  const openPanelIndex = this.panelsStack.findIndex(p => p === openPanel);
379
- if (openPanelIndex > 0 && !openPanel?.permanentExpanded) {
438
+ if (openPanelIndex > 0 && !openPanel?.permanentExpanded && !openPanel.isExpanded) {
380
439
  this.panelsStack[openPanelIndex - 1].isCollapsed = false;
381
440
  }
382
441
  this.ensureOnlyTwoOpenPanels(openPanel.id);
383
442
  this.setPanelHash();
384
443
  this.emitEvent('panels.closed', panel);
385
444
  this.emitEvent('panels.changed', this.panelsStack);
445
+ this.$nextTick(() => { this.rebuildAllOnboardingPopovers() });
386
446
  }
387
447
 
388
448
  fullsizePanel(panel: IPanel) {
389
449
  const newStack = [...this.panelsStack];
390
450
  for (const p of newStack) {
451
+ p.isLastOpened = !p.isCollapsed && p !== panel;
391
452
  p.isCollapsed = p !== panel;
453
+ if (!p.isCollapsed) {
454
+ p.isExpanded = true;
455
+ }
456
+ }
457
+ this.panelsStack = newStack;
458
+ this.setPanelHash();
459
+ this.$nextTick(() => { this.rebuildAllOnboardingPopovers() });
460
+ }
461
+
462
+ get isFullSize() {
463
+ return this.panelsStack.filter(p => !p.isCollapsed).length === 1;
464
+ }
465
+
466
+ expandPanel(panel: IPanel) {
467
+ const newStack = [...this.panelsStack];
468
+ const index = newStack.findIndex(p => p.id === panel.id);
469
+ newStack[index].isCollapsed = false;
470
+ for (const p of newStack) {
471
+ p.isExpanded = false;
472
+ }
473
+ this.panelsStack = newStack;
474
+ this.ensureOnlyTwoOpenPanels(panel.id);
475
+ this.setPanelHash();
476
+ this.$nextTick(() => { this.rebuildAllOnboardingPopovers() });
477
+ }
478
+
479
+ collapsePanel(panel: IPanel) {
480
+ const newStack = [...this.panelsStack];
481
+ const currenctIndex = newStack.findIndex(p => p.id === panel.id);
482
+ const lastOpenedIndex = newStack.findIndex(p => p.isLastOpened);
483
+ if (lastOpenedIndex !== -1) { // якщо зебрежена остання відкрита панель
484
+ newStack[lastOpenedIndex].isCollapsed = false;
485
+ } else if (newStack[currenctIndex-1]) { // якщо після оновлення сторінки відсутнє значення "остання відкрита", то відкриваємо ту, що зліва
486
+ newStack[currenctIndex-1].isCollapsed = false;
487
+ }
488
+ for (const p of newStack) {
489
+ p.isExpanded = false;
392
490
  }
393
491
  this.panelsStack = newStack;
492
+ this.ensureOnlyTwoOpenPanels(panel.id);
394
493
  this.setPanelHash();
494
+ this.$nextTick(() => { this.rebuildAllOnboardingPopovers() });
395
495
  }
396
496
 
397
497
  getPanels(type) {
@@ -403,14 +503,19 @@ export default class PanelList extends Vue {
403
503
  }
404
504
 
405
505
  setPanelHash() {
406
- const hash = stackToHash(this.panelsStack).replace(/^#/, '');
407
- this.$router.push({ hash });
506
+ const hash = stackToHash(this.panelsStack, this.routeType === 'path').replace(/^#/, '');
507
+ if (this.routeType === 'path') {
508
+ this.$router.push({ path: `${hash}` });
509
+ } else {
510
+ this.$router.push({ hash });
511
+ }
512
+ this.updateTitle();
408
513
  }
409
514
 
410
515
  async parsePanelHash() {
411
- const {hash} = location;
516
+ const hash = this.routeType === 'path' ? location.pathname : location.hash;
412
517
  if (hash) {
413
- const panels = hashToStack(hash);
518
+ const panels = hashToStack(hash, this.routeType === 'path');
414
519
  const newStack = [];
415
520
  this.panelsStack = [];
416
521
  for (const panelIndex in panels) {
@@ -419,18 +524,25 @@ export default class PanelList extends Vue {
419
524
  // reuse panel
420
525
  this.panelsStack[panelIndex].payload = panel.payload;
421
526
  this.panelsStack[panelIndex].isCollapsed = panel.isCollapsed;
527
+ this.panelsStack[panelIndex].isExpanded = false;
422
528
  newStack.push(this.panelsStack[panelIndex]);
423
529
  } else {
424
- const resPanel = await this.internalOpenPanel(panel.type, panel.payload, undefined, true);
530
+ const resPanel = await this.internalOpenPanel(panel.type, panel.payload, undefined, true, true);
425
531
  if (resPanel) {
426
532
  resPanel.isCollapsed = panel.isCollapsed;
533
+ resPanel.isExpanded = false;
427
534
  resPanel.isAnimate = false;
428
535
  newStack.push(resPanel);
429
536
  }
430
537
  }
431
538
  }
539
+ const hasExpanded = newStack.length > 1 && newStack.filter(p => !p.isCollapsed).length === 1;
540
+ if (hasExpanded) {
541
+ newStack[newStack.findIndex(p => !p.isCollapsed)].isExpanded = true;
542
+ }
432
543
  this.panelsStack = newStack;
433
544
  this.emitEvent('panels.changed', this.panelsStack);
545
+ this.updateTitle();
434
546
  }
435
547
  }
436
548
 
@@ -452,5 +564,16 @@ export default class PanelList extends Vue {
452
564
  element.classList.add('animate');
453
565
  }
454
566
  }
567
+
568
+ rebuildPanelOnboardingPopovers(panelId: string | number, refreshDelay?: number) {
569
+ const panelRef = this.$refs[`panel-${panelId}`];
570
+ if (panelRef?.[0]) panelRef[0].rebuildOnboardingPopovers(refreshDelay);
571
+ }
572
+
573
+ rebuildAllOnboardingPopovers(refreshDelay?: number) {
574
+ this.panelsStack.forEach((panel) => {
575
+ this.rebuildPanelOnboardingPopovers(panel.id, refreshDelay);
576
+ });
577
+ }
455
578
  }
456
579
  </script>
@@ -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
  };
@@ -24,20 +24,26 @@ export default @Component({
24
24
  class itfPopover extends Vue {
25
25
  @PropSync('visible') value;
26
26
 
27
- @Prop({ type: String, default: 'bottom', validator: (value) => ['bottom', 'left', 'right', 'top'].includes(value) }) placement;
27
+ @Prop({ type: String, default: 'bottom', validator: (value) => ['bottom', 'left', 'right', 'top', 'auto'].includes(value) }) placement;
28
28
 
29
29
  @Prop({ type: String, default: 'click', validator: (value) => ['click', 'focus', 'hover', 'manual'].includes(value) }) trigger;
30
30
 
31
31
  @Prop({ type: String, default: '' }) customClass;
32
+ @Prop({ type: Boolean }) appendToBody;
33
+ @Prop({ type: Boolean }) appendToContext;
34
+ @Prop({ type: String }) locator;
35
+ @Prop({ type: Boolean, default: false }) isActivatorHighlighted;
32
36
 
33
37
  modalId = '';
34
-
35
38
  modalEl = null;
39
+ targetEl = null;
36
40
 
37
41
  ignoreUpdate = false; // потрібно щоб не оновлювалось visible декілька разів, бо буде баг коли вікно відкривається і одразу закривається
38
42
 
43
+ initTimeout = null;
44
+
39
45
  beforeDestroy() {
40
- this.hide();
46
+ this.destroy();
41
47
  }
42
48
 
43
49
  @Watch('value')
@@ -46,9 +52,26 @@ class itfPopover extends Vue {
46
52
  return;
47
53
  }
48
54
  if (newValue) {
49
- this.modalEl.show();
55
+ this.modalEl?.show();
56
+ } else {
57
+ this.modalEl?.hide();
58
+ }
59
+ }
60
+
61
+ @Watch('locator')
62
+ onLocatorChanged(newValue, oldValue) {
63
+ if (newValue !== oldValue && newValue && this.modalEl) {
64
+ this.initPopover();
65
+ }
66
+ }
67
+
68
+ @Watch('isActivatorHighlighted')
69
+ onIsActivatorHighlightedChanged(newValue, oldValue) {
70
+ if (!this.targetEl) return;
71
+ if (newValue) {
72
+ this.targetEl.classList.add('pulsing');
50
73
  } else {
51
- this.modalEl.hide();
74
+ this.targetEl.classList.remove('pulsing');
52
75
  }
53
76
  }
54
77
 
@@ -58,14 +81,62 @@ class itfPopover extends Vue {
58
81
  }
59
82
 
60
83
  async mounted() {
84
+ this.refresh(100);
85
+ }
86
+
87
+ destroy() {
88
+ if (this.initTimeout) {
89
+ clearTimeout(this.initTimeout);
90
+ this.initTimeout = null;
91
+ }
92
+ if (this.modalEl) {
93
+ this.modalEl.dispose();
94
+ this.modalEl = null;
95
+ }
96
+ if (this.targetEl) this.resetTargetEl();
97
+ }
98
+
99
+ refresh(delay = 300) {
100
+ this.destroy();
101
+
102
+ if (this.initTimeout) clearTimeout(this.initTimeout);
103
+ this.initTimeout = setTimeout(() => this.initPopover(), delay);
104
+ }
105
+
106
+ async initPopover() {
61
107
  if (typeof window === 'undefined') {
62
108
  return;
63
109
  }
64
- const el = this.$refs.popover;
110
+
111
+ // Remove listeners from the previous target element before finding the new one
112
+ if (this.modalEl) this.destroy();
113
+
114
+ // Determine target
115
+ if (this.locator) {
116
+ this.targetEl = document.querySelector(this.locator);
117
+ if (!this.targetEl) return;
118
+ } else {
119
+ this.targetEl = this.$el;
120
+ }
121
+
122
+ if (this.isActivatorHighlighted) this.targetEl.classList.add('pulsing');
123
+
124
+ const el = this.$refs.popover || "";
65
125
  const { default: Popover } = await import('bootstrap/js/src/popover');
66
126
 
67
- const context = this.$el.closest('.itf-append-context') || document.body;
68
- this.modalEl = new Popover(this.$el, {
127
+ if (this._isDestroyed) return;
128
+
129
+ let context = document.body;
130
+ if (this.appendToContext && this.targetEl instanceof Node && this.targetEl.parentNode) {
131
+ context = this.targetEl.closest('.itf-append-context') || document.body;
132
+ this.targetEl.parentNode.removeChild(this.targetEl);
133
+ context.appendChild(this.targetEl); // should append only to body
134
+ } else if (this.appendToBody && this.targetEl instanceof Node && this.targetEl.parentNode) {
135
+ this.targetEl.parentNode.removeChild(this.targetEl);
136
+ context.appendChild(this.targetEl); // should append only to body
137
+ }
138
+
139
+ this.modalEl = new Popover(this.targetEl, {
69
140
  content: el,
70
141
  container: context,
71
142
  sanitize: false,
@@ -74,26 +145,38 @@ class itfPopover extends Vue {
74
145
  placement: this.placement,
75
146
  trigger: this.trigger,
76
147
  });
148
+
77
149
  this.onVisibleChanged(this.value);
78
- this.$el.addEventListener('shown.bs.popover', () => {
79
- this.ignoreUpdate = true;
80
- this.value = true;
81
- document.body.addEventListener('click', this.clickOutside);
82
- this.$nextTick(() => this.ignoreUpdate = false);
83
- });
84
- this.$el.addEventListener('hidden.bs.popover', () => {
85
- this.ignoreUpdate = true;
86
- this.value = false;
87
- document.body.removeEventListener('click', this.clickOutside);
88
- this.$nextTick(() => this.ignoreUpdate = false);
89
- });
150
+ this.targetEl.addEventListener('shown.bs.popover', this.handleShown);
151
+ this.targetEl.addEventListener('hidden.bs.popover', this.handleHidden);
152
+ }
153
+
154
+ resetTargetEl() {
155
+ this.targetEl.removeEventListener('shown.bs.popover', this.handleShown);
156
+ this.targetEl.removeEventListener('hidden.bs.popover', this.handleHidden);
157
+ this.targetEl.classList.remove('pulsing');
158
+ this.targetEl = null;
159
+ }
160
+
161
+ handleShown() {
162
+ this.ignoreUpdate = true;
163
+ this.value = true;
164
+ document.body.addEventListener('click', this.clickOutside);
165
+ this.$nextTick(() => this.ignoreUpdate = false);
166
+ }
167
+
168
+ handleHidden() {
169
+ this.ignoreUpdate = true;
170
+ this.value = false;
171
+ document.body.removeEventListener('click', this.clickOutside);
172
+ this.$nextTick(() => this.ignoreUpdate = false);
90
173
  }
91
174
 
92
175
  clickOutside(e) {
93
176
  if (this.trigger !== 'click' || !e.target) {
94
177
  return;
95
178
  }
96
- if (this.$el !== e.target && e.target.closest('.itf-popover') !== this.$refs.popover) {
179
+ if (this.targetEl !== e.target && e.target.closest('.itf-popover') !== this.$refs.popover) {
97
180
  this.hide();
98
181
  }
99
182
  }
@@ -107,7 +190,7 @@ class itfPopover extends Vue {
107
190
  hide() {
108
191
  if (this.modalEl) {
109
192
  // треба затримка щоб попав не показався знову через отримання фокусу
110
- setTimeout(() => this.modalEl.hide(), 10);
193
+ setTimeout(() => this.modalEl?.hide(), 10);
111
194
  }
112
195
  }
113
196
  }