summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/ide/components/file_finder/index.vue
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ide/components/file_finder/index.vue')
-rw-r--r--app/assets/javascripts/ide/components/file_finder/index.vue245
1 files changed, 245 insertions, 0 deletions
diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue
new file mode 100644
index 00000000000..ea2b13a8b21
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_finder/index.vue
@@ -0,0 +1,245 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import VirtualList from 'vue-virtual-scroll-list';
+import Item from './item.vue';
+import router from '../../ide_router';
+import {
+ MAX_FILE_FINDER_RESULTS,
+ FILE_FINDER_ROW_HEIGHT,
+ FILE_FINDER_EMPTY_ROW_HEIGHT,
+} from '../../constants';
+import {
+ UP_KEY_CODE,
+ DOWN_KEY_CODE,
+ ENTER_KEY_CODE,
+ ESC_KEY_CODE,
+} from '../../../lib/utils/keycodes';
+
+export default {
+ components: {
+ Item,
+ VirtualList,
+ },
+ data() {
+ return {
+ focusedIndex: 0,
+ searchText: '',
+ mouseOver: false,
+ cancelMouseOver: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['allBlobs']),
+ ...mapState(['fileFindVisible', 'loading']),
+ filteredBlobs() {
+ const searchText = this.searchText.trim();
+
+ if (searchText === '') {
+ return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
+ }
+
+ return fuzzaldrinPlus
+ .filter(this.allBlobs, searchText, {
+ key: 'path',
+ maxResults: MAX_FILE_FINDER_RESULTS,
+ })
+ .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
+ },
+ 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: {
+ fileFindVisible() {
+ this.$nextTick(() => {
+ if (!this.fileFindVisible) {
+ this.searchText = '';
+ } else {
+ this.focusedIndex = 0;
+
+ if (this.$refs.searchInput) {
+ this.$refs.searchInput.focus();
+ }
+ }
+ });
+ },
+ searchText() {
+ 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;
+ }
+ });
+ }
+ },
+ },
+ methods: {
+ ...mapActions(['toggleFileFinder']),
+ 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.toggleFileFinder(false);
+ break;
+ default:
+ break;
+ }
+ },
+ openFile(file) {
+ this.toggleFileFinder(false);
+ router.push(`/project${file.url}`);
+ },
+ onMouseOver(index) {
+ if (!this.cancelMouseOver) {
+ this.mouseOver = true;
+ this.focusedIndex = index;
+ }
+ },
+ onMouseMove(index) {
+ this.cancelMouseOver = false;
+ this.onMouseOver(index);
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-finder-overlay"
+ @mousedown.self="toggleFileFinder(false)"
+ >
+ <div
+ class="dropdown-menu diff-file-changes ide-file-finder show"
+ >
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ :placeholder="__('Search files')"
+ autocomplete="off"
+ v-model="searchText"
+ ref="searchInput"
+ @keydown="onKeydown($event)"
+ @keyup="onKeyup($event)"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ :class="{
+ hidden: showClearInputButton
+ }"
+ ></i>
+ <i
+ role="button"
+ :aria-label="__('Clear search input')"
+ class="fa fa-times dropdown-input-clear"
+ :class="{
+ show: showClearInputButton
+ }"
+ @click="clearSearchInput"
+ ></i>
+ </div>
+ <div>
+ <virtual-list
+ :size="listHeight"
+ :remain="listShowCount"
+ wtag="ul"
+ ref="virtualScrollList"
+ >
+ <template v-if="filteredBlobsLength">
+ <li
+ v-for="(file, index) in filteredBlobs"
+ :key="file.key"
+ >
+ <item
+ class="disable-hover"
+ :file="file"
+ :search-text="searchText"
+ :focused="index === focusedIndex"
+ :index="index"
+ @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>