code.gitea.io/gitea@v1.22.3/web_src/js/components/DiffCommitSelector.vue (about) 1 <script> 2 import {SvgIcon} from '../svg.js'; 3 import {GET} from '../modules/fetch.js'; 4 5 export default { 6 components: {SvgIcon}, 7 data: () => { 8 const el = document.getElementById('diff-commit-select'); 9 return { 10 menuVisible: false, 11 isLoading: false, 12 locale: { 13 filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'), 14 }, 15 commits: [], 16 hoverActivated: false, 17 lastReviewCommitSha: null, 18 }; 19 }, 20 computed: { 21 commitsSinceLastReview() { 22 if (this.lastReviewCommitSha) { 23 return this.commits.length - this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) - 1; 24 } 25 return 0; 26 }, 27 queryParams() { 28 return this.$el.parentNode.getAttribute('data-queryparams'); 29 }, 30 issueLink() { 31 return this.$el.parentNode.getAttribute('data-issuelink'); 32 }, 33 }, 34 mounted() { 35 document.body.addEventListener('click', this.onBodyClick); 36 this.$el.addEventListener('keydown', this.onKeyDown); 37 this.$el.addEventListener('keyup', this.onKeyUp); 38 }, 39 unmounted() { 40 document.body.removeEventListener('click', this.onBodyClick); 41 this.$el.removeEventListener('keydown', this.onKeyDown); 42 this.$el.removeEventListener('keyup', this.onKeyUp); 43 }, 44 methods: { 45 onBodyClick(event) { 46 // close this menu on click outside of this element when the dropdown is currently visible opened 47 if (this.$el.contains(event.target)) return; 48 if (this.menuVisible) { 49 this.toggleMenu(); 50 } 51 }, 52 onKeyDown(event) { 53 if (!this.menuVisible) return; 54 const item = document.activeElement; 55 if (!this.$el.contains(item)) return; 56 switch (event.key) { 57 case 'ArrowDown': // select next element 58 event.preventDefault(); 59 this.focusElem(item.nextElementSibling, item); 60 break; 61 case 'ArrowUp': // select previous element 62 event.preventDefault(); 63 this.focusElem(item.previousElementSibling, item); 64 break; 65 case 'Escape': // close menu 66 event.preventDefault(); 67 item.tabIndex = -1; 68 this.toggleMenu(); 69 break; 70 } 71 }, 72 onKeyUp(event) { 73 if (!this.menuVisible) return; 74 const item = document.activeElement; 75 if (!this.$el.contains(item)) return; 76 if (event.key === 'Shift' && this.hoverActivated) { 77 // shift is not pressed anymore -> deactivate hovering and reset hovered and selected 78 this.hoverActivated = false; 79 for (const commit of this.commits) { 80 commit.hovered = false; 81 commit.selected = false; 82 } 83 } 84 }, 85 highlight(commit) { 86 if (!this.hoverActivated) return; 87 const indexSelected = this.commits.findIndex((x) => x.selected); 88 const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id); 89 for (const [idx, commit] of this.commits.entries()) { 90 commit.hovered = Math.min(indexSelected, indexCurrentElem) <= idx && idx <= Math.max(indexSelected, indexCurrentElem); 91 } 92 }, 93 /** Focus given element */ 94 focusElem(elem, prevElem) { 95 if (elem) { 96 elem.tabIndex = 0; 97 if (prevElem) prevElem.tabIndex = -1; 98 elem.focus(); 99 } 100 }, 101 /** Opens our menu, loads commits before opening */ 102 async toggleMenu() { 103 this.menuVisible = !this.menuVisible; 104 // load our commits when the menu is not yet visible (it'll be toggled after loading) 105 // and we got no commits 106 if (!this.commits.length && this.menuVisible && !this.isLoading) { 107 this.isLoading = true; 108 try { 109 await this.fetchCommits(); 110 } finally { 111 this.isLoading = false; 112 } 113 } 114 // set correct tabindex to allow easier navigation 115 this.$nextTick(() => { 116 const expandBtn = this.$el.querySelector('#diff-commit-list-expand'); 117 const showAllChanges = this.$el.querySelector('#diff-commit-list-show-all'); 118 if (this.menuVisible) { 119 this.focusElem(showAllChanges, expandBtn); 120 } else { 121 this.focusElem(expandBtn, showAllChanges); 122 } 123 }); 124 }, 125 /** Load the commits to show in this dropdown */ 126 async fetchCommits() { 127 const resp = await GET(`${this.issueLink}/commits/list`); 128 const results = await resp.json(); 129 this.commits.push(...results.commits.map((x) => { 130 x.hovered = false; 131 return x; 132 })); 133 this.commits.reverse(); 134 this.lastReviewCommitSha = results.last_review_commit_sha || null; 135 if (this.lastReviewCommitSha && this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) === -1) { 136 // the lastReviewCommit is not available (probably due to a force push) 137 // reset the last review commit sha 138 this.lastReviewCommitSha = null; 139 } 140 Object.assign(this.locale, results.locale); 141 }, 142 showAllChanges() { 143 window.location = `${this.issueLink}/files${this.queryParams}`; 144 }, 145 /** Called when user clicks on since last review */ 146 changesSinceLastReviewClick() { 147 window.location = `${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`; 148 }, 149 /** Clicking on a single commit opens this specific commit */ 150 commitClicked(commitId, newWindow = false) { 151 const url = `${this.issueLink}/commits/${commitId}${this.queryParams}`; 152 if (newWindow) { 153 window.open(url); 154 } else { 155 window.location = url; 156 } 157 }, 158 /** 159 * When a commit is clicked with shift this enables the range 160 * selection. Second click (with shift) defines the end of the 161 * range. This opens the diff of this range 162 * Exception: first commit is the first commit of this PR. Then 163 * the diff from beginning of PR up to the second clicked commit is 164 * opened 165 */ 166 commitClickedShift(commit) { 167 this.hoverActivated = !this.hoverActivated; 168 commit.selected = true; 169 // Second click -> determine our range and open links accordingly 170 if (!this.hoverActivated) { 171 // find all selected commits and generate a link 172 if (this.commits[0].selected) { 173 // first commit is selected - generate a short url with only target sha 174 const lastCommitIdx = this.commits.findLastIndex((x) => x.selected); 175 if (lastCommitIdx === this.commits.length - 1) { 176 // user selected all commits - just show the normal diff page 177 window.location = `${this.issueLink}/files${this.queryParams}`; 178 } else { 179 window.location = `${this.issueLink}/files/${this.commits[lastCommitIdx].id}${this.queryParams}`; 180 } 181 } else { 182 const start = this.commits[this.commits.findIndex((x) => x.selected) - 1].id; 183 const end = this.commits.findLast((x) => x.selected).id; 184 window.location = `${this.issueLink}/files/${start}..${end}${this.queryParams}`; 185 } 186 } 187 }, 188 }, 189 }; 190 </script> 191 <template> 192 <div class="ui scrolling dropdown custom"> 193 <button 194 class="ui basic button" 195 id="diff-commit-list-expand" 196 @click.stop="toggleMenu()" 197 :data-tooltip-content="locale.filter_changes_by_commit" 198 aria-haspopup="true" 199 aria-controls="diff-commit-selector-menu" 200 :aria-label="locale.filter_changes_by_commit" 201 aria-activedescendant="diff-commit-list-show-all" 202 > 203 <svg-icon name="octicon-git-commit"/> 204 </button> 205 <div class="left menu transition" id="diff-commit-selector-menu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'"> 206 <div class="loading-indicator is-loading" v-if="isLoading"/> 207 <div v-if="!isLoading" class="vertical item" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()"> 208 <div class="gt-ellipsis"> 209 {{ locale.show_all_commits }} 210 </div> 211 <div class="gt-ellipsis text light-2 tw-mb-0"> 212 {{ locale.stats_num_commits }} 213 </div> 214 </div> 215 <!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review --> 216 <div 217 v-if="lastReviewCommitSha != null" role="menuitem" 218 class="vertical item" 219 :class="{disabled: !commitsSinceLastReview}" 220 @keydown.enter="changesSinceLastReviewClick()" 221 @click="changesSinceLastReviewClick()" 222 > 223 <div class="gt-ellipsis"> 224 {{ locale.show_changes_since_your_last_review }} 225 </div> 226 <div class="gt-ellipsis text light-2"> 227 {{ commitsSinceLastReview }} commits 228 </div> 229 </div> 230 <span v-if="!isLoading" class="info text light-2">{{ locale.select_commit_hold_shift_for_range }}</span> 231 <template v-for="commit in commits" :key="commit.id"> 232 <div 233 class="vertical item" role="menuitem" 234 :class="{selection: commit.selected, hovered: commit.hovered}" 235 @keydown.enter.exact="commitClicked(commit.id)" 236 @keydown.enter.shift.exact="commitClickedShift(commit)" 237 @mouseover.shift="highlight(commit)" 238 @click.exact="commitClicked(commit.id)" 239 @click.ctrl.exact="commitClicked(commit.id, true)" 240 @click.meta.exact="commitClicked(commit.id, true)" 241 @click.shift.exact.stop.prevent="commitClickedShift(commit)" 242 > 243 <div class="tw-flex-1 tw-flex tw-flex-col tw-gap-1"> 244 <div class="gt-ellipsis commit-list-summary"> 245 {{ commit.summary }} 246 </div> 247 <div class="gt-ellipsis text light-2"> 248 {{ commit.committer_or_author_name }} 249 <span class="text right"> 250 <!-- TODO: make this respect the PreferredTimestampTense setting --> 251 <relative-time prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time> 252 </span> 253 </div> 254 </div> 255 <div class="tw-font-mono"> 256 {{ commit.short_sha }} 257 </div> 258 </div> 259 </template> 260 </div> 261 </div> 262 </template> 263 <style scoped> 264 .hovered:not(.selection) { 265 background-color: var(--color-small-accent) !important; 266 } 267 .selection { 268 background-color: var(--color-accent) !important; 269 } 270 271 .info { 272 display: inline-block; 273 padding: 7px 14px !important; 274 line-height: 1.4; 275 width: 100%; 276 } 277 278 #diff-commit-selector-menu { 279 margin-top: 0.25em; 280 overflow-x: hidden; 281 max-height: 450px; 282 } 283 284 #diff-commit-selector-menu .loading-indicator { 285 height: 200px; 286 width: 350px; 287 } 288 289 #diff-commit-selector-menu .item, 290 #diff-commit-selector-menu .info { 291 display: flex !important; 292 flex-direction: row; 293 line-height: 1.4; 294 padding: 7px 14px !important; 295 gap: 0.25em; 296 } 297 #diff-commit-selector-menu .item:not(:first-child), 298 #diff-commit-selector-menu .info:not(:first-child) { 299 border-top: 1px solid var(--color-secondary) !important; 300 } 301 #diff-commit-selector-menu .item:focus { 302 color: var(--color-text); 303 background: var(--color-hover); 304 } 305 306 #diff-commit-selector-menu .commit-list-summary { 307 max-width: min(380px, 96vw); 308 } 309 </style>