300 lines
7.7 KiB
Vue
300 lines
7.7 KiB
Vue
<script>
|
|
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
|
import Mousetrap from 'mousetrap';
|
|
import VirtualList from 'vue-virtual-scroll-list';
|
|
import Item from './item.vue';
|
|
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
|
|
|
|
export const MAX_FILE_FINDER_RESULTS = 40;
|
|
export const FILE_FINDER_ROW_HEIGHT = 55;
|
|
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
|
|
|
|
const originalStopCallback = Mousetrap.stopCallback;
|
|
|
|
export default {
|
|
components: {
|
|
Item,
|
|
VirtualList,
|
|
},
|
|
props: {
|
|
files: {
|
|
type: Array,
|
|
required: true,
|
|
},
|
|
visible: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
showDiffStats: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
clearSearchOnClose: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
focusedIndex: -1,
|
|
searchText: '',
|
|
mouseOver: false,
|
|
cancelMouseOver: false,
|
|
};
|
|
},
|
|
computed: {
|
|
filteredBlobs() {
|
|
const searchText = this.searchText.trim();
|
|
|
|
if (searchText === '') {
|
|
return this.files.slice(0, MAX_FILE_FINDER_RESULTS);
|
|
}
|
|
|
|
return fuzzaldrinPlus.filter(this.files, searchText, {
|
|
key: 'path',
|
|
maxResults: MAX_FILE_FINDER_RESULTS,
|
|
});
|
|
},
|
|
filteredBlobsLength() {
|
|
return this.filteredBlobs.length;
|
|
},
|
|
listShowCount() {
|
|
return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
|
|
},
|
|
listHeight() {
|
|
return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
|
|
},
|
|
showClearInputButton() {
|
|
return this.searchText.trim() !== '';
|
|
},
|
|
},
|
|
watch: {
|
|
visible() {
|
|
this.$nextTick(() => {
|
|
if (!this.visible) {
|
|
if (this.clearSearchOnClose) {
|
|
this.searchText = '';
|
|
}
|
|
} else {
|
|
this.focusedIndex = 0;
|
|
|
|
if (this.$refs.searchInput) {
|
|
this.$refs.searchInput.focus();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
searchText() {
|
|
this.focusedIndex = -1;
|
|
|
|
this.$nextTick(() => {
|
|
this.focusedIndex = 0;
|
|
});
|
|
},
|
|
focusedIndex() {
|
|
if (!this.mouseOver) {
|
|
this.$nextTick(() => {
|
|
const el = this.$refs.virtualScrollList.$el;
|
|
const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
|
|
const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
|
|
|
|
if (this.focusedIndex === 0) {
|
|
// if index is the first index, scroll straight to start
|
|
el.scrollTop = 0;
|
|
} else if (this.focusedIndex === this.filteredBlobsLength - 1) {
|
|
// if index is the last index, scroll to the end
|
|
el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
|
|
} else if (scrollTop >= bottom + el.scrollTop) {
|
|
// if element is off the bottom of the scroll list, scroll down one item
|
|
el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
|
|
} else if (scrollTop < el.scrollTop) {
|
|
// if element is off the top of the scroll list, scroll up one item
|
|
el.scrollTop = scrollTop;
|
|
}
|
|
});
|
|
}
|
|
},
|
|
},
|
|
mounted() {
|
|
if (this.files.length) {
|
|
this.focusedIndex = 0;
|
|
}
|
|
|
|
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
|
|
if (e.preventDefault) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
this.toggle(!this.visible);
|
|
});
|
|
|
|
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
|
|
},
|
|
methods: {
|
|
toggle(visible) {
|
|
this.$emit('toggle', visible);
|
|
},
|
|
clearSearchInput() {
|
|
this.searchText = '';
|
|
|
|
this.$nextTick(() => {
|
|
this.$refs.searchInput.focus();
|
|
});
|
|
},
|
|
onKeydown(e) {
|
|
switch (e.keyCode) {
|
|
case UP_KEY_CODE:
|
|
e.preventDefault();
|
|
this.mouseOver = false;
|
|
this.cancelMouseOver = true;
|
|
if (this.focusedIndex > 0) {
|
|
this.focusedIndex -= 1;
|
|
} else {
|
|
this.focusedIndex = this.filteredBlobsLength - 1;
|
|
}
|
|
break;
|
|
case DOWN_KEY_CODE:
|
|
e.preventDefault();
|
|
this.mouseOver = false;
|
|
this.cancelMouseOver = true;
|
|
if (this.focusedIndex < this.filteredBlobsLength - 1) {
|
|
this.focusedIndex += 1;
|
|
} else {
|
|
this.focusedIndex = 0;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
onKeyup(e) {
|
|
switch (e.keyCode) {
|
|
case ENTER_KEY_CODE:
|
|
this.openFile(this.filteredBlobs[this.focusedIndex]);
|
|
break;
|
|
case ESC_KEY_CODE:
|
|
this.toggle(false);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
openFile(file) {
|
|
this.toggle(false);
|
|
this.$emit('click', file);
|
|
},
|
|
onMouseOver(index) {
|
|
if (!this.cancelMouseOver) {
|
|
this.mouseOver = true;
|
|
this.focusedIndex = index;
|
|
}
|
|
},
|
|
onMouseMove(index) {
|
|
this.cancelMouseOver = false;
|
|
this.onMouseOver(index);
|
|
},
|
|
mousetrapStopCallback(e, el, combo) {
|
|
if (
|
|
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
|
|
el.classList.contains('inputarea')
|
|
) {
|
|
return true;
|
|
} else if (combo === 'command+p' || combo === 'ctrl+p') {
|
|
return false;
|
|
}
|
|
|
|
return originalStopCallback(e, el, combo);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="file-finder-overlay" @mousedown.self="toggle(false)">
|
|
<div class="dropdown-menu diff-file-changes file-finder show">
|
|
<div :class="{ 'has-value': showClearInputButton }" class="dropdown-input">
|
|
<input
|
|
ref="searchInput"
|
|
v-model="searchText"
|
|
:placeholder="__('Search files')"
|
|
type="search"
|
|
class="dropdown-input-field"
|
|
autocomplete="off"
|
|
@keydown="onKeydown($event)"
|
|
@keyup="onKeyup($event)"
|
|
/>
|
|
<i
|
|
:class="{
|
|
hidden: showClearInputButton,
|
|
}"
|
|
aria-hidden="true"
|
|
class="fa fa-search dropdown-input-search"
|
|
></i>
|
|
<i
|
|
:aria-label="__('Clear search input')"
|
|
role="button"
|
|
class="fa fa-times dropdown-input-clear"
|
|
@click="clearSearchInput"
|
|
></i>
|
|
</div>
|
|
<div>
|
|
<virtual-list ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul">
|
|
<template v-if="filteredBlobsLength">
|
|
<li v-for="(file, index) in filteredBlobs" :key="file.key">
|
|
<item
|
|
:file="file"
|
|
:search-text="searchText"
|
|
:focused="index === focusedIndex"
|
|
:index="index"
|
|
:show-diff-stats="showDiffStats"
|
|
class="disable-hover"
|
|
@click="openFile"
|
|
@mouseover="onMouseOver"
|
|
@mousemove="onMouseMove"
|
|
/>
|
|
</li>
|
|
</template>
|
|
<li v-else class="dropdown-menu-empty-item">
|
|
<div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
|
|
<template v-if="loading">
|
|
{{ __('Loading...') }}
|
|
</template>
|
|
<template v-else>
|
|
{{ __('No files found.') }}
|
|
</template>
|
|
</div>
|
|
</li>
|
|
</virtual-list>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.file-finder-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
z-index: 200;
|
|
}
|
|
|
|
.file-finder {
|
|
top: 10px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.diff-file-changes {
|
|
top: 50px;
|
|
max-height: 327px;
|
|
}
|
|
</style>
|