|
1 | 1 | <template> |
2 | | - <div class="control has-icons-left"> |
3 | | - <div class="select is-rounded"> |
4 | | - <select @change="careerChanged($event.target.value)" class="browser-default" :value="selectedCareer"> |
5 | | - <option value="Select a career" disabled="" selected="">Select a career</option> |
6 | | - <optgroup v-for="[key, value] in items" :label="key"> |
7 | | - <option v-for="option in value" :value="option.id"> |
8 | | - {{ option.name }} |
9 | | - </option> |
10 | | - </optgroup> |
11 | | - </select> |
| 2 | + <div class="combo-box control dropdown is-expanded is-rounded" :class="{ 'is-active': isOpen, 'is-loading': isLoading }"> |
| 3 | + <div class="dropdown-trigger"> |
| 4 | + <div class="control has-icons-left has-icons-right"> |
| 5 | + <input type="text" |
| 6 | + v-model="inputText" |
| 7 | + @focus="isOpen = true" |
| 8 | + placeholder="Select a career" |
| 9 | + class="combo-input input is-rounded" /> |
| 10 | + <div class="icon is-small is-left"> |
| 11 | + <i class="fas fa-database"></i> |
| 12 | + </div> |
| 13 | + <span v-if="!isLoading" class="icon is-small is-right"> |
| 14 | + <i class="fas fa-angle-down" aria-hidden="true"></i> |
| 15 | + </span> |
| 16 | + </div> |
12 | 17 | </div> |
13 | | - <div class="icon is-small is-left"> |
14 | | - <i class="fas fa-database"></i> |
| 18 | + |
| 19 | + <div v-if="isOpen" class="combo-dropdown dropdown-menu" role="menu"> |
| 20 | + <div class="dropdown-content"> |
| 21 | + <template v-for="[key, value] in filteredGroups" :key="key"> |
| 22 | + <div class="combo-group-label dropdown-item"> |
| 23 | + {{ key }} |
| 24 | + </div> |
| 25 | + |
| 26 | + <button v-for="option in value" |
| 27 | + :key="option.id" |
| 28 | + @click="selectValue(option)" |
| 29 | + class="combo-option dropdown-item"> |
| 30 | + {{ option.name }} |
| 31 | + </button> |
| 32 | + </template> |
| 33 | + |
| 34 | + <div v-if="filteredGroups.size === 0" class="dropdown-item"> |
| 35 | + No results |
| 36 | + </div> |
| 37 | + </div> |
15 | 38 | </div> |
16 | 39 | </div> |
17 | 40 | </template> |
18 | 41 |
|
| 42 | +<style scoped> |
| 43 | + .combo-box input { |
| 44 | + min-width: 20rem; |
| 45 | + } |
| 46 | +
|
| 47 | + .combo-group-label { |
| 48 | + font-weight: bold; |
| 49 | + } |
| 50 | +
|
| 51 | + .combo-group-label:not(:first-child) { |
| 52 | + margin-top: 6px; |
| 53 | + } |
| 54 | +
|
| 55 | + .combo-option { |
| 56 | + padding-left: 1.5rem; |
| 57 | + } |
| 58 | +</style> |
| 59 | + |
19 | 60 | <script lang="ts"> |
20 | 61 | import { defineComponent } from 'vue'; |
21 | 62 | import type { PropType } from 'vue' |
22 | 63 | import type { CareerListItem, Filters } from 'types'; |
23 | 64 | import { fetchCareerListItems } from '../utils/api'; |
24 | 65 |
|
25 | 66 | interface ComponentData { |
26 | | - items: Map<string, CareerListItem[]> | null; |
| 67 | + items: CareerListItem[] | null; |
27 | 68 | isLoading: boolean; |
| 69 | + isOpen: boolean; |
| 70 | + inputText: string; |
28 | 71 | } |
29 | 72 | |
30 | 73 | export default defineComponent({ |
|
40 | 83 | data(): ComponentData { |
41 | 84 | return { |
42 | 85 | items: null, |
43 | | - isLoading: false |
| 86 | + isLoading: false, |
| 87 | + isOpen: false, |
| 88 | + inputText: '' |
44 | 89 | }; |
45 | 90 | }, |
46 | 91 | methods: { |
47 | 92 | async queryData(filters: Filters) { |
48 | 93 | this.isLoading = true; |
49 | 94 | try { |
50 | 95 | const arr = await fetchCareerListItems(filters); |
51 | | - const groupedMap = arr.reduce( |
52 | | - (entryMap, e) => entryMap.set(this.getPlayerName(e), [...entryMap.get(this.getPlayerName(e)) || [], e]), |
53 | | - new Map<string, CareerListItem[]>() |
54 | | - ); |
55 | | -
|
56 | | - this.items = groupedMap; |
| 96 | + this.items = arr; |
| 97 | + this.updateInputText(); |
57 | 98 | } |
58 | 99 | finally { |
59 | 100 | this.isLoading = false; |
60 | 101 | } |
61 | 102 | }, |
| 103 | + selectValue(value) { |
| 104 | + this.inputText = value.name; |
| 105 | + this.isOpen = false; |
| 106 | + this.careerChanged(value.id); |
| 107 | + }, |
| 108 | + updateInputText() { |
| 109 | + const selItem = this.items.find(i => i.id === this.selectedCareer); |
| 110 | + this.inputText = selItem?.name ?? ''; |
| 111 | + }, |
| 112 | + onClickOutside(e) { |
| 113 | + if (!e.target.closest(".combo-box")) { |
| 114 | + this.isOpen = false; |
| 115 | + } |
| 116 | + }, |
62 | 117 | careerChanged(careerId: string) { |
63 | 118 | this.$emit('update:selectedCareer', careerId); |
64 | 119 | this.$emit('careerChanged', careerId); |
|
67 | 122 | return entry.userPreferredName ? entry.userPreferredName : entry.user; |
68 | 123 | } |
69 | 124 | }, |
| 125 | + computed: { |
| 126 | + canEdit(): boolean { |
| 127 | + return this.career != null && currentUser != null && this.career.userLogin === currentUser.userName; |
| 128 | + }, |
| 129 | + filteredGroups() { |
| 130 | + let items = this.items; |
| 131 | + if (!items) return new Map<string, CareerListItem[]>(); |
| 132 | +
|
| 133 | + const text = this.inputText.toLowerCase(); |
| 134 | + if (text.length > 0) { |
| 135 | + items = this.items.filter(i => this.getPlayerName(i).toLowerCase().includes(text) || |
| 136 | + i.name.toLowerCase().includes(text)); |
| 137 | + } |
| 138 | +
|
| 139 | + const groupedMap = items.reduce( |
| 140 | + (entryMap, e) => entryMap.set(this.getPlayerName(e), [...entryMap.get(this.getPlayerName(e)) || [], e]), |
| 141 | + new Map<string, CareerListItem[]>() |
| 142 | + ); |
| 143 | +
|
| 144 | + return groupedMap; |
| 145 | + } |
| 146 | + }, |
70 | 147 | mounted() { |
71 | 148 | this.$nextTick(function () { |
72 | 149 | this.queryData(this.filters); |
73 | 150 | }); |
| 151 | + document.addEventListener('click', this.onClickOutside); |
| 152 | + }, |
| 153 | + beforeUnmount() { |
| 154 | + document.removeEventListener('click', this.onClickOutside); |
74 | 155 | }, |
75 | 156 | watch: { |
| 157 | + selectedCareer() { |
| 158 | + if (this.items) { |
| 159 | + this.updateInputText(); |
| 160 | + } |
| 161 | + }, |
76 | 162 | filters: { |
77 | 163 | handler() { |
78 | 164 | this.queryData(this.filters); |
|
0 commit comments