wave-ui 3.1.3 → 3.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-ui",
3
- "version": "3.1.3",
3
+ "version": "3.3.0",
4
4
  "description": "An emerging UI framework for Vue.js (2 & 3) with only the bright side. :sunny:",
5
5
  "author": "Antoni Andre <antoniandre.web@gmail.com>",
6
6
  "homepage": "https://antoniandre.github.io/wave-ui",
@@ -165,7 +165,7 @@ $spinner-size: 40;
165
165
  z-index: 1;
166
166
  // Background-color must not transition to not affect the hover & focus states
167
167
  // in :before & :after.
168
- transition: $transition-duration, background-color 0s, padding 0s;
168
+ transition: all $transition-duration, background-color 0s, padding 0s;
169
169
  -webkit-tap-highlight-color: transparent;
170
170
 
171
171
  @include themeable;
@@ -253,10 +253,10 @@ $spinner-size: 40;
253
253
  // Button states.
254
254
  // ------------------------------------------------------
255
255
  // Hover & focus states - inside button.
256
- &:hover:before, &:focus:before {opacity: 0.08;}
257
- &--dark:hover:before, &--dark:focus:before {opacity: 0.2;}
258
- &--outline:hover:before, &--outline:focus:before,
259
- &--text:hover:before, &--text:focus:before {opacity: 0.12;}
256
+ &:hover:before, &:focus-visible:before {opacity: 0.2;}
257
+ &--dark:hover:before, &--dark:focus-visible:before {opacity: 0.4;}
258
+ &--outline:hover:before, &--outline:focus-visible:before,
259
+ &--text:hover:before, &--text:focus-visible:before {opacity: 0.12;}
260
260
 
261
261
  // Focus state - outside button.
262
262
  &:after {
@@ -273,22 +273,25 @@ $spinner-size: 40;
273
273
  transition: opacity 0.2s cubic-bezier(0.45, 0.05, 0.55, 0.95), transform 0.25s ease-in;
274
274
  transform: scale(0.85, 0.7);
275
275
  }
276
- &:focus:after {
276
+ &:focus-visible:after {
277
277
  opacity: 0.4;
278
278
  transform: scale(1);
279
279
  transition: opacity 0.2s cubic-bezier(0.45, 0.05, 0.55, 0.95), transform 0.25s ease-out;
280
280
  }
281
- &--dark:focus:after {opacity: 0.2;}
281
+ &--dark:focus-visible:after {opacity: 0.2;}
282
282
 
283
283
  // Active state.
284
- &:active:before {opacity: 0.2;}
285
- &--dark:active:before, &.primary--bg:active:before {opacity: 0.25;}
284
+ &:active {transform: scale(1.02);}
285
+ &:active:before {opacity: 0.3;}
286
+ &--dark:active:before, &.primary--bg:active:before {opacity: 0.35;}
286
287
 
287
288
  // Disable visual feedback on loading and disabled buttons.
288
289
  &--loading:hover:before,
289
- &--loading:focus:before,
290
+ &--loading:focus-visible:before,
290
291
  &--loading:active:before,
291
292
  &[disabled]:before {opacity: 0;}
293
+ &--loading:active,
294
+ &[disabled] {transform: none;}
292
295
  // ------------------------------------------------------
293
296
 
294
297
  // Disable events binding on nested content.
@@ -304,6 +307,7 @@ $spinner-size: 40;
304
307
  align-items: center;
305
308
  justify-content: center;
306
309
  background: inherit;
310
+ border-radius: inherit;
307
311
 
308
312
  svg {height: 75%;}
309
313
  circle {
@@ -190,7 +190,7 @@ export default {
190
190
  },
191
191
  selectionString () {
192
192
  return this.inputValue && this.inputValue.map(
193
- item => item[this.itemValueKey] !== undefined ? item[this.itemLabelKey] : (item[this.itemLabelKey] !== undefined ? item[this.itemLabelKey] : item)
193
+ item => item[this.itemValueKey] !== undefined ? item[this.itemLabelKey] : (item[this.itemLabelKey] ?? item)
194
194
  ).join(', ')
195
195
  },
196
196
  classes () {
@@ -9,7 +9,8 @@
9
9
  col.w-table__col(
10
10
  v-for="(header, i) in headers"
11
11
  :key="i"
12
- :width="header.width || null")
12
+ :width="header.width || null"
13
+ :class="colClasses[i]")
13
14
 
14
15
  //- Table header.
15
16
  thead(v-if="!noHeaders")
@@ -135,42 +136,41 @@
135
136
  slot(name="footer")
136
137
  tr.w-table__row.w-table__pagination-wrap(v-if="pagination && paginationConfig")
137
138
  td.w-table__cell(:colspan="headers.length")
138
- .w-table__pagination
139
- | {{ paginationConfig }}
139
+ .w-table__pagination.w-pagination
140
140
  slot(
141
141
  name="pagination"
142
- :range="`${paginationConfig.start}-${paginationConfig.end} of ${paginationConfig.total}`"
142
+ :range="`${paginationConfig.start}-${paginationConfig.end}`"
143
143
  :total="paginationConfig.total")
144
- w-select.pagination-number.pagination-number--items-per-page(
144
+ w-select.w-pagination__items-per-page(
145
145
  v-if="paginationConfig.itemsPerPageOptions"
146
146
  v-model="paginationConfig.itemsPerPage"
147
- @input="updatePaginationConfig"
147
+ @input="updatePaginationConfig({ itemsPerPage: paginationConfig.itemsPerPage })"
148
148
  :items="paginationConfig.itemsPerPageOptions"
149
149
  label-position="left"
150
150
  label="Items per page"
151
151
  label-color="inherit")
152
- .pagination-arrows
153
- w-button.pagination-arrow.pagination-arrow--prev(
152
+ .pages-wrap
153
+ w-button.w-pagination__arrow.w-pagination__arrow--prev(
154
154
  @click="goToPage('-1')"
155
155
  :disabled="paginationConfig.page <= 1"
156
156
  icon="wi-chevron-left"
157
157
  text
158
158
  lg)
159
- w-button.pagination-arrow.pagination-arrow--prev(
159
+ w-button.w-pagination__page(
160
160
  v-for="i in paginationConfig.pagesCount"
161
161
  :key="i"
162
- @click="goToPage(i)"
162
+ @click="i !== paginationConfig.page && goToPage(i)"
163
+ :class="{ 'w-pagination__page--active': i === paginationConfig.page }"
163
164
  round
164
- text
165
165
  lg) {{ i }}
166
- w-button.pagination-arrow.pagination-arrow--next(
166
+ w-button.w-pagination__arrow.w-pagination__arrow--next(
167
167
  @click="goToPage('+1')"
168
168
  :disabled="paginationConfig.page >= paginationConfig.pagesCount"
169
169
  icon="wi-chevron-right"
170
170
  text
171
171
  lg)
172
- span.pagination-number.pagination-number--results.
173
- {{ paginationConfig.start }}-{{ paginationConfig.end }} of {{ paginationConfig.total }}
172
+ span.w-pagination__results.
173
+ {{ paginationConfig.start }}-{{ paginationConfig.end || paginationConfig.total }} of {{ paginationConfig.total }}
174
174
  </template>
175
175
 
176
176
  <script>
@@ -195,6 +195,9 @@ export default {
195
195
  loading: { type: [Boolean, String] }, // Bool or 'header' to only display the bar in the header.
196
196
  // Allow single sort: `+id`, or multiple in an array like: ['+id', '-firstName'].
197
197
  sort: { type: [String, Array] },
198
+ sortFunction: { type: Function },
199
+ filter: { type: Function },
200
+ fetch: { type: Function },
198
201
 
199
202
  expandableRows: {
200
203
  validator: value => {
@@ -229,11 +232,16 @@ export default {
229
232
  // Useful to select or expand a row, and even after a filter, the same row will stay selected or expanded.
230
233
  uidKey: { type: String, default: 'id' },
231
234
 
232
- filter: { type: Function },
233
- sortFunction: { type: Function },
234
235
  mobileBreakpoint: { type: Number, default: 0 },
235
236
  resizableColumns: { type: Boolean },
236
237
 
238
+ // An object containing:
239
+ // - itemsPerPage
240
+ // - itemsPerPageOptions
241
+ // - start
242
+ // - end
243
+ // - page
244
+ // - total
237
245
  pagination: {
238
246
  type: [Boolean, Object, String],
239
247
  validator: object => {
@@ -283,7 +291,7 @@ export default {
283
291
  computed: {
284
292
  tableItems () {
285
293
  return this.items.map((item, i) => {
286
- item._uid = item[this.uidKey] !== undefined ? item[this.uidKey] : i
294
+ item._uid = item[this.uidKey] ?? i
287
295
  return item
288
296
  })
289
297
  },
@@ -293,7 +301,7 @@ export default {
293
301
  },
294
302
 
295
303
  sortedItems () {
296
- if (!this.activeSorting.length || this.sortFunction) return this.filteredItems
304
+ if (!this.activeSorting.length || this.sortFunction || this.fetch) return this.filteredItems
297
305
 
298
306
  // Only sort with 1 key for now, may handle more later.
299
307
  const sortKey1 = this.activeSorting[0].replace(/^[+-]/, '')
@@ -310,6 +318,10 @@ export default {
310
318
  })
311
319
  },
312
320
 
321
+ paginatedItems () {
322
+ return typeof this.fetch === 'function' ? this.sortedItems : this.sortedItems.slice(this.paginationConfig.start - 1, this.paginationConfig.end)
323
+ },
324
+
313
325
  // Returns an object containing { key1: '+', key2: '-' }. With + or - for ASC/DESC.
314
326
  activeSortingKeys () {
315
327
  return this.activeSorting.reduce((obj, item) => {
@@ -324,8 +336,16 @@ export default {
324
336
  }
325
337
  },
326
338
 
339
+ colClasses () {
340
+ return this.headers.map(header => {
341
+ return { 'w-table__col--highlighted': this.activeSortingKeys[header.key] }
342
+ }) || []
343
+ },
344
+
327
345
  classes () {
328
346
  return {
347
+ 'w-table--loading': this.loading,
348
+ 'w-table--loading-in-header': this.loading === 'header',
329
349
  'w-table--fixed-layout': this.fixedLayout || this.resizableColumns || this.hasStickyColumn,
330
350
  'w-table--mobile': this.isMobile || null,
331
351
  'w-table--resizable-cols': this.resizableColumns || null,
@@ -354,10 +374,6 @@ export default {
354
374
  // Faster lookup than array.includes(uid) and also cached.
355
375
  expandedRowsByUid () {
356
376
  return this.expandedRowsInternal.reduce((obj, uid) => (obj[uid] = true) && obj, {})
357
- },
358
-
359
- paginatedItems () {
360
- return this.sortedItems.slice(this.paginationConfig.start, this.paginationConfig.end)
361
377
  }
362
378
  },
363
379
 
@@ -389,9 +405,8 @@ export default {
389
405
 
390
406
  this.$emit('update:sort', this.activeSorting)
391
407
 
392
- if (typeof this.sortFunction === 'function') {
393
- await this.sortFunction(this.activeSorting)
394
- }
408
+ if (typeof this.sortFunction === 'function') await this.sortFunction(this.activeSorting)
409
+ else if (typeof this.fetch === 'function') await this.callApiFetch()
395
410
  },
396
411
 
397
412
  doSelectRow (item, index) {
@@ -553,33 +568,75 @@ export default {
553
568
  }, 0)
554
569
  },
555
570
 
556
- updatePaginationConfig () {
557
- const itemsPerPage = this.pagination?.itemsPerPage || 20
571
+ initPagination () {
572
+ const itemsPerPage = this.pagination?.itemsPerPage ?? 20 // Can also be `0` for all.
573
+
558
574
  const itemsPerPageOptions = this.pagination?.itemsPerPageOptions || [20, 100, { label: 'All', value: 0 }]
559
- const total = this.pagination?.total || this.items.length
560
- const itemsPerPageOrTotal = itemsPerPage || total
561
- const page = this.pagination?.page || 1
562
- this.paginationConfig = {
575
+ // If the given itemsPerPage is not in the itemsPerPageOptions, add it.
576
+ if (!itemsPerPageOptions.find(item => (item?.value ?? item) === +itemsPerPage)) {
577
+ itemsPerPageOptions.push(itemsPerPage)
578
+ }
579
+ this.paginationConfig.itemsPerPageOptions = itemsPerPageOptions.map(item => ({
580
+ label: ['string', 'number'].includes(typeof item) ? item.toString() : (item.label || item.value),
581
+ value: ['string', 'number'].includes(typeof item) ? ~~item : (item.value ?? item.label)
582
+ }))
583
+ // Sort the options in an ascending order.
584
+ this.paginationConfig.itemsPerPageOptions.sort((a, b) => a.value < b.value ? -1 : 1)
585
+ const optionAll = this.paginationConfig.itemsPerPageOptions.shift()
586
+ this.paginationConfig.itemsPerPageOptions.push(optionAll)
587
+
588
+ this.updatePaginationConfig({
563
589
  itemsPerPage,
564
- itemsPerPageOptions: itemsPerPageOptions.map(item => ({
565
- label: ['string', 'number'].includes(typeof item) ? item.toString() : (item.label || item.value),
566
- value: ['string', 'number'].includes(typeof item) ? ~~item : (item.value ?? item.label)
567
- })),
568
- page,
569
- start: this.pagination?.start || 1,
570
- end: total >= (itemsPerPageOrTotal * page) ? (itemsPerPageOrTotal * page) : (total % (itemsPerPageOrTotal * page)),
571
- total,
572
- pagesCount: Math.ceil(total / itemsPerPageOrTotal)
590
+ page: this.pagination.page || 1,
591
+ total: this.pagination.total || this.items.length
592
+ })
593
+ },
594
+
595
+ updatePaginationConfig ({ itemsPerPage, page, total }) {
596
+ if (total) this.paginationConfig.total = total
597
+ if (itemsPerPage !== undefined) {
598
+ this.paginationConfig.itemsPerPage = itemsPerPage
599
+ itemsPerPage = itemsPerPage || this.paginationConfig.total // If `0`, take all the results.
600
+ this.paginationConfig.page = 1;
601
+ ({ page } = this.paginationConfig) // Shorthand var for next lines.
602
+ total = this.paginationConfig.total // Shorthand var for next lines.
603
+ this.paginationConfig.start = 1
604
+ this.paginationConfig.end = total >= (itemsPerPage * page) ? (itemsPerPage * page) : (total % (itemsPerPage * page))
605
+ this.paginationConfig.pagesCount = Math.ceil(total / itemsPerPage)
573
606
  }
607
+ if (page) this.goToPage(page)
574
608
  },
575
609
 
576
- goToPage (page) {
610
+ /**
611
+ * Goes to a given page or to the next or previous page.
612
+ *
613
+ * @param {Number|String} page a number to go to a specific page or `-1`, `+1` for prev & next page.
614
+ */
615
+ async goToPage (page) {
577
616
  if (['-1', '+1'].includes(page)) this.paginationConfig.page += +page
578
617
  else this.paginationConfig.page = page
579
- const { itemsPerPage } = this.paginationConfig
618
+ const { itemsPerPage, total } = this.paginationConfig
580
619
  this.paginationConfig.page = Math.max(1, this.paginationConfig.page)
581
620
  this.paginationConfig.start = (itemsPerPage * (this.paginationConfig.page - 1)) + 1
582
- this.paginationConfig.end = (this.paginationConfig.start - 1) + itemsPerPage
621
+ this.paginationConfig.end = (this.paginationConfig.start - 1) + (itemsPerPage || total)
622
+
623
+ if (typeof this.fetch === 'function') await this.callApiFetch()
624
+ },
625
+
626
+ /**
627
+ * Call a user provided fetch function in order to fetch table items from an API.
628
+ * While waiting for the call to resolve, nothing in the table will change.
629
+ */
630
+ async callApiFetch () {
631
+ const { page, start, end, total, itemsPerPage } = this.paginationConfig
632
+ return await this.fetch({
633
+ page,
634
+ start,
635
+ end: end || total,
636
+ total,
637
+ itemsPerPage: itemsPerPage || total,
638
+ sorting: this.activeSorting
639
+ })
583
640
  }
584
641
  },
585
642
 
@@ -590,7 +647,7 @@ export default {
590
647
  if ((this.expandedRows || []).length) this.expandedRowsInternal = this.expandedRows
591
648
  if ((this.selectedRows || []).length) this.selectedRowsInternal = this.selectedRows
592
649
 
593
- if (this.pagination) this.updatePaginationConfig()
650
+ if (this.pagination) this.initPagination()
594
651
  },
595
652
 
596
653
  watch: {
@@ -617,9 +674,14 @@ export default {
617
674
  this.selectedRowsInternal = Array.isArray(array) && array.length ? this.selectedRows : []
618
675
  },
619
676
 
620
- pagination: {
621
- handler () { this.updatePaginationConfig() },
622
- deep: true
677
+ 'pagination.page' (page) {
678
+ this.updatePaginationConfig({ page })
679
+ },
680
+ 'pagination.itemsPerPage' (itemsPerPage) {
681
+ this.updatePaginationConfig({ itemsPerPage })
682
+ },
683
+ 'pagination.total' (total) {
684
+ this.updatePaginationConfig({ total })
623
685
  }
624
686
  }
625
687
  }
@@ -655,6 +717,12 @@ $tr-border-top: 1px;
655
717
  user-select: none;
656
718
  }
657
719
 
720
+ // Table columns.
721
+ // ------------------------------------------------------
722
+ &__col--highlighted {
723
+ background-color: rgba(var(--w-contrast-bg-color-rgb), 0.04);
724
+ }
725
+
658
726
  // Table headers.
659
727
  // ------------------------------------------------------
660
728
  thead {position: relative;}
@@ -744,7 +812,7 @@ $tr-border-top: 1px;
744
812
  left: 0;
745
813
  right: 0;
746
814
  }
747
- &__progress-bar td {padding: 0;height: 1px;}
815
+ &__progress-bar td {padding: 0;height: 0;}
748
816
  @-moz-document url-prefix() {
749
817
  &__progress-bar td {height: 100%;}
750
818
  }
@@ -753,7 +821,7 @@ $tr-border-top: 1px;
753
821
  display: flex;
754
822
  align-items: center;
755
823
  justify-content: center;
756
- height:100%;
824
+ height: 100%;
757
825
  width: 100%;
758
826
  padding-top: 2 * $base-increment;
759
827
  padding-bottom: 2 * $base-increment;
@@ -761,6 +829,9 @@ $tr-border-top: 1px;
761
829
 
762
830
  // Table body.
763
831
  // ------------------------------------------------------
832
+ tbody {transition: opacity $transition-duration;}
833
+ &--loading-in-header tbody {opacity: 0.6;}
834
+
764
835
  tbody tr {border-top: $tr-border-top solid rgba(var(--w-base-color-rgb), 0.06);}
765
836
  // Don't apply built-in bg color if a bg color is already found on a tr.
766
837
  tbody tr:nth-child(odd):not(.no-data):not([class*="--bg"]) {background-color: $table-tr-odd-color;}
@@ -818,52 +889,79 @@ $tr-border-top: 1px;
818
889
 
819
890
  // Table footer.
820
891
  // ------------------------------------------------------
821
- &__footer &__cell {
822
- padding-top: $base-increment;
823
- padding-bottom: $base-increment;
824
- }
825
-
826
892
  &--fixed-footer tfoot {
827
893
  position: sticky;
828
- bottom: 0;
894
+ bottom: -1px;
829
895
  background-color: $base-bg-color;
830
896
  z-index: 1; // For sticky columns to go under.
831
897
 
832
898
  &:after {
833
899
  content: '';
834
900
  position: absolute;
835
- bottom: 0;
901
+ top: 0;
836
902
  left: 0;
837
903
  right: 0;
838
- border-bottom: $border;
904
+ border-top: $border;
839
905
  }
840
906
  }
841
907
 
908
+ &__footer &__cell {
909
+ padding-top: $base-increment;
910
+ padding-bottom: $base-increment;
911
+ }
912
+
842
913
  // Pagination.
843
914
  // ------------------------------------------------------
844
- &__pagination-wrap {
845
- border-top: $border;
846
- }
847
915
  &__pagination {
848
916
  display: flex;
849
917
  align-items: center;
850
918
  justify-content: flex-end;
851
919
 
852
- .pagination-number--items-per-page {
853
- flex-grow: 0;
920
+ .w-pagination__items-per-page {
921
+ flex: 0 0 auto;
854
922
  text-align: right;
855
923
  }
856
924
 
857
- .pagination-arrows {
925
+ .pages-wrap {
858
926
  margin-left: 3 * $base-increment;
859
927
  margin-right: 3 * $base-increment;
928
+ overflow: auto;
929
+ max-height: 4.5em;
930
+ }
931
+
932
+ .w-pagination__page {
933
+ margin: 0.5 * $base-increment;
934
+ font-size: 0.9em;
935
+ aspect-ratio: 1;
936
+ overflow: hidden;
937
+ color: rgba(var(--w-base-color-rgb), 0.65);
938
+ background-color: rgba(var(--w-base-bg-color-rgb), 0.4);
939
+
940
+ &:hover:before {
941
+ background-color: $primary;
942
+ opacity: 0.1;
943
+ }
944
+ &:active:before {
945
+ background-color: $primary;
946
+ opacity: 0.2;
947
+ }
948
+
949
+ &--active {
950
+ font-weight: bold;
951
+ color: $primary;
952
+
953
+ &:before {
954
+ background-color: $primary;
955
+ opacity: 0.1;
956
+ }
957
+ }
860
958
  }
861
959
 
862
- .pagination-number--of {
960
+ .w-pagination__results {
863
961
  margin-left: $base-increment;
864
962
  margin-right: $base-increment;
963
+ white-space: nowrap;
865
964
  }
866
- .w-select__selection {max-width: 60px;}
867
965
  }
868
966
  }
869
967
 
@@ -6,11 +6,11 @@ ul.w-tree(:class="classes")
6
6
  :class="itemClasses(item)")
7
7
  //- The keys `route` & `disabled` are always present in any currentDepthItems.
8
8
  component.w-tree__item-label(
9
- :is="!disabled && !item.disabled && item.route ? (!$router || hasExternalLink(item) ? 'a' : 'router-link') : 'div'"
9
+ :is="getTreeItemComponent(item)"
10
10
  v-bind="item.route && { [!$router || hasExternalLink(item) ? 'href' : 'to']: item.route }"
11
11
  @click="!disabled && !item.disabled && onLabelClick(item, $event)"
12
12
  @keydown="!disabled && !item.disabled && onLabelKeydown(item, $event)"
13
- :tabindex="!disabled && !item.disabled && (item.children || item.branch || selectable) && !(unexpandableEmpty && !item.children) ? 0 : null")
13
+ :tabindex="getTreeItemTabindex(item)")
14
14
  //- @click.stop to not follow link if item is a link.
15
15
  w-button.w-tree__item-expand(
16
16
  v-if="(item.children || item.branch) && ((expandOpenIcon && item.open) || expandIcon) && !(unexpandableEmpty && !item.children)"
@@ -22,7 +22,12 @@ ul.w-tree(:class="classes")
22
22
  :disabled="disabled || item.disabled"
23
23
  text
24
24
  sm)
25
- slot(name="item" :item="item.originalItem" :depth="depth" :open="item.open")
25
+ slot(
26
+ name="item"
27
+ :item="item.originalItem"
28
+ :depth="depth"
29
+ :path="item.path"
30
+ :open="item.open")
26
31
  w-icon(v-if="itemIcon(item)" class="w-tree__item-icon" :color="item.originalItem[itemIconColorKey] || iconColor") {{ itemIcon(item) }}
27
32
  span(v-html="item.label")
28
33
  span.ml1(v-if="counts && (item.children || item.branch)").
@@ -30,13 +35,14 @@ ul.w-tree(:class="classes")
30
35
  component(
31
36
  :is="noTransition ? 'div' : 'w-transition-expand'"
32
37
  :y="!noTransition || null"
33
- @after-enter="$emit('open', { item: item.originalItem, open: item.open, depth })"
34
- @after-leave="$emit('close', { item: item.originalItem, open: item.open, depth })")
38
+ @after-enter="$emit('open', { item: item.originalItem, open: item.open, depth, path: getTreeItemPathForOutput(item) })"
39
+ @after-leave="$emit('close', { item: item.originalItem, open: item.open, depth, path: getTreeItemPathForOutput(item) })")
35
40
  w-tree(
36
41
  v-if="item.children && item.open"
37
42
  v-bind="$props"
38
43
  :depth="depth + 1"
39
44
  :data="item.originalItem.children"
45
+ :parent="item"
40
46
  @before-open="$emit('before-open', $event)"
41
47
  @open="$emit('open', $event)"
42
48
  @before-close="$emit('before-close', $event)"
@@ -44,8 +50,8 @@ ul.w-tree(:class="classes")
44
50
  @click="$emit('click', $event)"
45
51
  @select="$emit('select', $event)"
46
52
  @update:model-value="$emit('update:model-value', $event)")
47
- template(#item="{ item, depth, open }")
48
- slot(name="item" :item="item" :depth="depth" :open="open")
53
+ template(#item="{ item, depth, path, open }")
54
+ slot(name="item" :item="item" :depth="depth" :path="path" :open="open")
49
55
  </template>
50
56
 
51
57
  <script>
@@ -60,7 +66,8 @@ export default {
60
66
  props: {
61
67
  modelValue: { type: [Object, Array] },
62
68
  data: { type: [Object, Array], required: true },
63
- depth: { type: Number, default: 0 },
69
+ depth: { type: Number, default: 0 }, // To get the context from nested items.
70
+ parent: { type: Object, default: null }, // To get the context from nested items.
64
71
  branchClass: { type: String },
65
72
  leafClass: { type: String },
66
73
  branchIcon: { type: String },
@@ -115,7 +122,7 @@ export default {
115
122
  if (!Array.isArray(items)) items = [items]
116
123
 
117
124
  items.forEach((item, i) => {
118
- this.currentDepthItems.push({
125
+ const itemWrapper = {
119
126
  originalItem: item, // Store the original item to return it on event emits.
120
127
  _uid: this.depth.toString() + (i + 1),
121
128
  label: item[this.itemLabelKey],
@@ -124,11 +131,48 @@ export default {
124
131
  route: item[this.itemRouteKey],
125
132
  disabled: item[this.itemDisabledKey],
126
133
  depth: this.depth,
127
- open: !!(oldItems[i]?.open || this.expandAll || item[this.itemOpenKey])
128
- })
134
+ open: !!(oldItems[i]?.open || this.expandAll || item[this.itemOpenKey]),
135
+ parent: this.parent || null,
136
+ path: [] // Ancestors path from root to leaf including self.
137
+ }
138
+ itemWrapper.path = this.getTreeItemPath(itemWrapper)
139
+ this.currentDepthItems.push(itemWrapper)
129
140
  })
130
141
  },
131
142
 
143
+ getTreeItemComponent (item) {
144
+ return !this.disabled && !item.disabled && item.route ? (!this.$router || this.hasExternalLink(item) ? 'a' : 'router-link') : 'div'
145
+ },
146
+
147
+ getTreeItemTabindex (item) {
148
+ return !this.disabled && !item.disabled && (item.children || item.branch || this.selectable) && !(this.unexpandableEmpty && !item.children) ? 0 : null
149
+ },
150
+
151
+ /**
152
+ * Get the tree path of the given item.
153
+ * The full ancestors items are stored in the array and not only their `originalItem`s in case
154
+ * it is mutated before we return it to the user through slots and emitted events.
155
+ * Before it is returned to the user, this array is mapped to only give the `originalItem`s.
156
+ *
157
+ * @param {Object} item the tree item to get the ancestors path for.
158
+ * @return an array of item objects from the root to the leaf (including the item itself).
159
+ */
160
+ getTreeItemPath (item) {
161
+ const ancestorsPath = [item]
162
+
163
+ let ancestor = item.parent
164
+ while (ancestor) {
165
+ ancestorsPath.push(ancestor)
166
+ ancestor = ancestor.parent
167
+ }
168
+ ancestorsPath.reverse()
169
+ return ancestorsPath
170
+ },
171
+
172
+ getTreeItemPathForOutput (item) {
173
+ return item.path.map(item => item.originalItem)
174
+ },
175
+
132
176
  /**
133
177
  * Expand/collapse the given tree item when possible (not disabled, has children).
134
178
  *
@@ -139,7 +183,12 @@ export default {
139
183
  if (typeof open === 'boolean') item.open = open
140
184
  else item.open = !item.open
141
185
 
142
- const emitParams = { item: item.originalItem, open: item.open, depth: this.depth }
186
+ const emitParams = {
187
+ item: item.originalItem,
188
+ open: item.open,
189
+ depth: this.depth,
190
+ path: this.getTreeItemPathForOutput(item)
191
+ }
143
192
 
144
193
  this.$emit(item.open ? 'before-open' : 'before-close', emitParams)
145
194
 
@@ -154,14 +203,24 @@ export default {
154
203
  const route = item[this.itemRouteKey]
155
204
  if (route && this.$router && !this.hasExternalLink(item)) e.preventDefault()
156
205
 
157
- this.$emit('click', { item: item.originalItem, depth: this.depth, e })
206
+ this.$emit('click', {
207
+ item: item.originalItem,
208
+ depth: this.depth,
209
+ path: this.getTreeItemPathForOutput(item),
210
+ e
211
+ })
158
212
  if (item.children || (item.branch && !this.unexpandableEmpty)) this.expandDepth(item)
159
213
 
160
214
  if (this.selectable) this.emitItemSelection(item, e)
161
215
  },
162
216
 
163
217
  emitItemSelection (item, e) {
164
- const emitParams = { item: item.originalItem, depth: this.depth, e }
218
+ const emitParams = {
219
+ item: item.originalItem,
220
+ depth: this.depth,
221
+ path: this.getTreeItemPathForOutput(item),
222
+ e
223
+ }
165
224
  if (item.children || (item.branch && !this.unexpandableEmpty)) {
166
225
  emitParams.open = item.open
167
226
  }
@@ -299,8 +358,8 @@ $expand-icon-size: 20px;
299
358
  right: - $base-increment - 2px;
300
359
  border-radius: $border-radius;
301
360
  }
302
- &:hover:before {background-color: rgba($primary, 0.05);}
303
- &:focus:before {background-color: rgba($primary, 0.1);}
361
+ &:hover:before {background-color: $primary;opacity: 0.1;}
362
+ &:focus:before {background-color: $primary;opacity: 0.2;}
304
363
  }
305
364
  &__item--leaf &__item-label:before {
306
365
  left: - $base-increment;