305 lines · 10.9 KB · Vue
RawDownload
<script>import {SvgIcon} from '../svg.js';import {GET} from '../modules/fetch.js'; export default {  components: {SvgIcon},  data: () => {    const el = document.getElementById('diff-commit-select');    return {      menuVisible: false,      isLoading: false,      locale: {        filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'),      },      commits: [],      hoverActivated: false,      lastReviewCommitSha: null,    };  },  computed: {    commitsSinceLastReview() {      if (this.lastReviewCommitSha) {        return this.commits.length - this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) - 1;      }      return 0;    },    queryParams() {      return this.$el.parentNode.getAttribute('data-queryparams');    },    issueLink() {      return this.$el.parentNode.getAttribute('data-issuelink');    },  },  mounted() {    document.body.addEventListener('click', this.onBodyClick);    this.$el.addEventListener('keydown', this.onKeyDown);    this.$el.addEventListener('keyup', this.onKeyUp);  },  unmounted() {    document.body.removeEventListener('click', this.onBodyClick);    this.$el.removeEventListener('keydown', this.onKeyDown);    this.$el.removeEventListener('keyup', this.onKeyUp);  },  methods: {    onBodyClick(event) {      // close this menu on click outside of this element when the dropdown is currently visible opened      if (this.$el.contains(event.target)) return;      if (this.menuVisible) {        this.toggleMenu();      }    },    onKeyDown(event) {      if (!this.menuVisible) return;      const item = document.activeElement;      if (!this.$el.contains(item)) return;      switch (event.key) {        case 'ArrowDown': // select next element          event.preventDefault();          this.focusElem(item.nextElementSibling, item);          break;        case 'ArrowUp': // select previous element          event.preventDefault();          this.focusElem(item.previousElementSibling, item);          break;        case 'Escape': // close menu          event.preventDefault();          item.tabIndex = -1;          this.toggleMenu();          break;      }    },    onKeyUp(event) {      if (!this.menuVisible) return;      const item = document.activeElement;      if (!this.$el.contains(item)) return;      if (event.key === 'Shift' && this.hoverActivated) {        // shift is not pressed anymore -> deactivate hovering and reset hovered and selected        this.hoverActivated = false;        for (const commit of this.commits) {          commit.hovered = false;          commit.selected = false;        }      }    },    highlight(commit) {      if (!this.hoverActivated) return;      const indexSelected = this.commits.findIndex((x) => x.selected);      const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id);      for (const [idx, commit] of this.commits.entries()) {        commit.hovered = Math.min(indexSelected, indexCurrentElem) <= idx && idx <= Math.max(indexSelected, indexCurrentElem);      }    },    /** Focus given element */    focusElem(elem, prevElem) {      if (elem) {        elem.tabIndex = 0;        if (prevElem) prevElem.tabIndex = -1;        elem.focus();      }    },    /** Opens our menu, loads commits before opening */    async toggleMenu() {      this.menuVisible = !this.menuVisible;      // load our commits when the menu is not yet visible (it'll be toggled after loading)      // and we got no commits      if (!this.commits.length && this.menuVisible && !this.isLoading) {        this.isLoading = true;        try {          await this.fetchCommits();        } finally {          this.isLoading = false;        }      }      // set correct tabindex to allow easier navigation      this.$nextTick(() => {        const expandBtn = this.$el.querySelector('#diff-commit-list-expand');        const showAllChanges = this.$el.querySelector('#diff-commit-list-show-all');        if (this.menuVisible) {          this.focusElem(showAllChanges, expandBtn);        } else {          this.focusElem(expandBtn, showAllChanges);        }      });    },    /** Load the commits to show in this dropdown */    async fetchCommits() {      const resp = await GET(`${this.issueLink}/commits/list`);      const results = await resp.json();      this.commits.push(...results.commits.map((x) => {        x.hovered = false;        return x;      }));      this.commits.reverse();      this.lastReviewCommitSha = results.last_review_commit_sha || null;      if (this.lastReviewCommitSha && !this.commits.some((x) => x.id === this.lastReviewCommitSha)) {        // the lastReviewCommit is not available (probably due to a force push)        // reset the last review commit sha        this.lastReviewCommitSha = null;      }      Object.assign(this.locale, results.locale);    },    showAllChanges() {      window.location.assign(`${this.issueLink}/files${this.queryParams}`);    },    /** Called when user clicks on since last review */    changesSinceLastReviewClick() {      window.location.assign(`${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`);    },    /** Clicking on a single commit opens this specific commit */    commitClicked(commitId, newWindow = false) {      const url = `${this.issueLink}/commits/${commitId}${this.queryParams}`;      if (newWindow) {        window.open(url);      } else {        window.location.assign(url);      }    },    /**     * When a commit is clicked with shift this enables the range     * selection. Second click (with shift) defines the end of the     * range. This opens the diff of this range     * Exception: first commit is the first commit of this PR. Then     * the diff from beginning of PR up to the second clicked commit is     * opened     */    commitClickedShift(commit) {      this.hoverActivated = !this.hoverActivated;      commit.selected = true;      // Second click -> determine our range and open links accordingly      if (!this.hoverActivated) {        // find all selected commits and generate a link        if (this.commits[0].selected) {          // first commit is selected - generate a short url with only target sha          const lastCommitIdx = this.commits.findLastIndex((x) => x.selected);          if (lastCommitIdx === this.commits.length - 1) {            // user selected all commits - just show the normal diff page            window.location.assign(`${this.issueLink}/files${this.queryParams}`);          } else {            window.location.assign(`${this.issueLink}/files/${this.commits[lastCommitIdx].id}${this.queryParams}`);          }        } else {          const start = this.commits[this.commits.findIndex((x) => x.selected) - 1].id;          const end = this.commits.findLast((x) => x.selected).id;          window.location.assign(`${this.issueLink}/files/${start}..${end}${this.queryParams}`);        }      }    },  },};</script><template>  <div class="ui scrolling dropdown custom">    <button      class="ui basic button"      id="diff-commit-list-expand"      @click.stop="toggleMenu()"      :data-tooltip-content="locale.filter_changes_by_commit"      aria-haspopup="true"      aria-controls="diff-commit-selector-menu"      :aria-label="locale.filter_changes_by_commit"      aria-activedescendant="diff-commit-list-show-all"    >      <svg-icon name="octicon-git-commit"/>    </button>    <div class="menu left transition" id="diff-commit-selector-menu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">      <div class="loading-indicator is-loading" v-if="isLoading"/>      <div v-if="!isLoading" class="vertical item" id="diff-commit-list-show-all" role="menuitem" @keydown.enter="showAllChanges()" @click="showAllChanges()">        <div class="gt-ellipsis">          {{ locale.show_all_commits }}        </div>        <div class="gt-ellipsis text light-2 tw-mb-0">          {{ locale.stats_num_commits }}        </div>      </div>      <!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review -->      <div        v-if="lastReviewCommitSha != null" role="menuitem"        class="vertical item"        :class="{disabled: !commitsSinceLastReview}"        @keydown.enter="changesSinceLastReviewClick()"        @click="changesSinceLastReviewClick()"      >        <div class="gt-ellipsis">          {{ locale.show_changes_since_your_last_review }}        </div>        <div class="gt-ellipsis text light-2">          {{ commitsSinceLastReview }} commits        </div>      </div>      <span v-if="!isLoading" class="info text light-2">{{ locale.select_commit_hold_shift_for_range }}</span>      <template v-for="commit in commits" :key="commit.id">        <div          class="vertical item" role="menuitem"          :class="{selection: commit.selected, hovered: commit.hovered}"          @keydown.enter.exact="commitClicked(commit.id)"          @keydown.enter.shift.exact="commitClickedShift(commit)"          @mouseover.shift="highlight(commit)"          @click.exact="commitClicked(commit.id)"          @click.ctrl.exact="commitClicked(commit.id, true)"          @click.meta.exact="commitClicked(commit.id, true)"          @click.shift.exact.stop.prevent="commitClickedShift(commit)"        >          <div class="tw-flex-1 tw-flex tw-flex-col tw-gap-1">            <div class="gt-ellipsis commit-list-summary">              {{ commit.summary }}            </div>            <div class="gt-ellipsis text light-2">              {{ commit.committer_or_author_name }}              <span class="text right">                <relative-time prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>              </span>            </div>          </div>          <div class="tw-font-mono">            {{ commit.short_sha }}          </div>        </div>      </template>    </div>  </div></template><style scoped>  .hovered:not(.selection) {    background-color: var(--color-small-accent) !important;  }  .selection {    background-color: var(--color-accent) !important;  }   .info {    display: inline-block;    padding: 7px 14px !important;    line-height: 1.4;    width: 100%;  }   #diff-commit-selector-menu {    overflow-x: hidden;    max-height: 450px;  }   #diff-commit-selector-menu .loading-indicator {    height: 200px;    width: 350px;  }   #diff-commit-selector-menu .item,  #diff-commit-selector-menu .info {    display: flex !important;    flex-direction: row;    line-height: 1.4;    padding: 7px 14px !important;    border-top: 1px solid var(--color-secondary) !important;    gap: 0.25em;  }   #diff-commit-selector-menu .item:focus {    color: var(--color-text);    background: var(--color-hover);  }   #diff-commit-selector-menu .commit-list-summary {    max-width: min(380px, 96vw);  }</style>