Commit d32eaee2 authored by Phil Hughes's avatar Phil Hughes

correctly show the dropdown with `t` keypress

added arrow key navigation in the dropdown
enter & click open the file
highlight occurrences of the searched text in the drppdown item
fixed some performance issues when rendering
limit the dropdown items to a maximum of 20 - this may change to more
depending on other performance changes
parent 748a2f2b
<script> <script>
import { mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import VirtualList from 'vue-virtual-scroll-list'; import VirtualList from 'vue-virtual-scroll-list';
import Item from './item.vue'; import Item from './item.vue';
import router from '../../ide_router';
const MAX_RESULTS = 20;
export default { export default {
components: { components: {
...@@ -11,89 +14,153 @@ export default { ...@@ -11,89 +14,153 @@ export default {
}, },
data() { data() {
return { return {
focusedIndex: 0,
searchText: '', searchText: '',
}; };
}, },
computed: { computed: {
...mapGetters(['allBlobs']), ...mapGetters(['allBlobs']),
...mapState(['loading']), ...mapState(['fileFindVisible', 'loading']),
filteredBlobs() { filteredBlobs() {
const searchText = this.searchText.trim(); const searchText = this.searchText.trim();
if (searchText === '') return this.allBlobs; if (searchText === '') return this.allBlobs.slice(0, MAX_RESULTS);
return fuzzaldrinPlus.filter(this.allBlobs, searchText, { return fuzzaldrinPlus.filter(this.allBlobs, searchText, {
key: 'path', key: 'path',
maxResults: MAX_RESULTS,
}); });
}, },
listShowCount() { listShowCount() {
if (this.filteredBlobs.length === 0) return 1; if (!this.filteredBlobs.length) return 1;
return this.filteredBlobs.length > 5 ? 5 : this.filteredBlobs.length; return this.filteredBlobs.length > 5 ? 5 : this.filteredBlobs.length;
}, },
listHeight() { listHeight() {
return this.listShowCount > 1 ? 55 : 33; return this.filteredBlobs.length ? 55 : 33;
},
},
watch: {
fileFindVisible() {
this.$nextTick(() => this.$refs.searchInput.focus());
},
searchText() {
if (this.searchText.trim() !== '') {
this.focusedIndex = 0;
}
}, },
}, },
mounted() { methods: {
this.$refs.searchInput.focus(); ...mapActions(['toggleFileFinder']),
onKeydown(e) {
switch (e.keyCode) {
case 38:
// UP
e.preventDefault();
if (this.focusedIndex > 0) this.focusedIndex -= 1;
break;
case 40:
// DOWN
e.preventDefault();
if (this.focusedIndex < this.filteredBlobs.length - 1) this.focusedIndex += 1;
break;
default:
break;
}
},
onKeyup(e) {
switch (e.keyCode) {
case 13:
// ENTER
this.openFile(this.filteredBlobs[this.focusedIndex]);
break;
default:
break;
}
},
openFile(file) {
this.toggleFileFinder(false);
router.push(`/project${file.url}`);
},
}, },
}; };
</script> </script>
<template> <template>
<div class="dropdown-menu diff-file-changes ide-file-finder" style="display: block;"> <div
<div class="dropdown-input"> class="ide-file-finder-overlay"
<input @click.self="toggleFileFinder(false)"
type="search" >
class="dropdown-input-field" <div
placeholder="Search files" class="dropdown-menu diff-file-changes ide-file-finder show"
autocomplete="off" >
v-model="searchText" <div class="dropdown-input">
ref="searchInput" <input
/> type="search"
<i class="dropdown-input-field"
aria-hidden="true" placeholder="Search files"
class="fa fa-search dropdown-input-search" autocomplete="off"
></i> v-model="searchText"
</div> ref="searchInput"
<div> @keydown="onKeydown($event)"
<virtual-list @keyup="onKeyup($event)"
:size="listHeight" />
:remain="listShowCount" <i
wtag="ul" aria-hidden="true"
> class="fa fa-search dropdown-input-search"
<template v-if="filteredBlobs.length"> ></i>
</div>
<div>
<virtual-list
:size="listHeight"
:remain="listShowCount"
:start="focusedIndex"
wtag="ul"
>
<template v-if="filteredBlobs.length">
<li
v-for="(file, index) in filteredBlobs"
:key="file.key"
>
<item
:file="file"
:search-text="searchText"
:focused="index === focusedIndex"
@click="openFile"
/>
</li>
</template>
<li <li
v-for="file in filteredBlobs" v-else
:key="file.key" class="dropdown-menu-empty-itemhidden"
> >
<item <a href="">
:file="file" <template v-if="loading">
/> Loading...
</template>
<template v-else>
No files found.
</template>
</a>
</li> </li>
</template> </virtual-list>
<li </div>
v-else
class="dropdown-menu-empty-itemhidden"
>
<a href="">
<template v-if="loading">
Loading...
</template>
<template v-else>
No files found.
</template>
</a>
</li>
</virtual-list>
</div> </div>
</div> </div>
</template> </template>
<style> <style>
.ide-file-finder-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
}
.ide-file-finder { .ide-file-finder {
top: 100px; top: 10px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
......
<script> <script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import FileIcon from '../../../vue_shared/components/file_icon.vue'; import FileIcon from '../../../vue_shared/components/file_icon.vue';
export default { export default {
...@@ -10,14 +11,42 @@ export default { ...@@ -10,14 +11,42 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
focused: {
type: Boolean,
required: true,
},
searchText: {
type: String,
required: true,
},
},
methods: {
clickRow() {
this.$emit('click', this.file);
},
highlightText(text) {
const occurrences = fuzzaldrinPlus.match(text, this.searchText);
return text
.split('')
.map(
(char, i) =>
`<span class="${occurrences.indexOf(i) >= 0 ? 'highlighted' : ''}">${char}</span>`,
)
.join('');
},
}, },
}; };
</script> </script>
<template> <template>
<a <a
href="" href="#"
class="diff-changed-file" class="diff-changed-file"
:class="{
'is-focused': focused,
}"
@click.prevent="clickRow"
> >
<file-icon <file-icon
:file-name="file.name" :file-name="file.name"
...@@ -25,12 +54,23 @@ export default { ...@@ -25,12 +54,23 @@ export default {
css-classes="diff-file-changed-icon append-right-8" css-classes="diff-file-changed-icon append-right-8"
/> />
<span class="diff-changed-file-content append-right-8"> <span class="diff-changed-file-content append-right-8">
<strong class="diff-changed-file-name"> <strong
{{ file.name }} class="diff-changed-file-name"
v-html="highlightText(this.file.name)"
>
</strong> </strong>
<span class="diff-changed-file-path prepend-top-5"> <span
{{ file.path }} class="diff-changed-file-path prepend-top-5"
v-html="highlightText(this.file.path)"
>
</span> </span>
</span> </span>
</a> </a>
</template> </template>
<style>
.highlighted {
color: #1b69b6;
font-weight: 600;
}
</style>
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import Mousetrap from 'mousetrap';
import ideSidebar from './ide_side_bar.vue'; import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue'; import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue'; import repoTabs from './repo_tabs.vue';
...@@ -50,6 +51,14 @@ export default { ...@@ -50,6 +51,14 @@ export default {
}); });
return returnValue; return returnValue;
}; };
Mousetrap.bind('t', e => {
e.preventDefault();
this.toggleFileFinder(true);
});
},
methods: {
...mapActions(['toggleFileFinder']),
}, },
}; };
</script> </script>
...@@ -59,7 +68,7 @@ export default { ...@@ -59,7 +68,7 @@ export default {
class="ide-view" class="ide-view"
> >
<find-file <find-file
v-if="fileFindVisible" v-show="fileFindVisible"
/> />
<ide-sidebar /> <ide-sidebar />
<div <div
......
...@@ -112,6 +112,9 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -112,6 +112,9 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
}; };
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -37,4 +37,12 @@ export const hasChanges = state => !!state.changedFiles.length; ...@@ -37,4 +37,12 @@ export const hasChanges = state => !!state.changedFiles.length;
export const hasMergeRequest = state => !!state.currentMergeRequestId; export const hasMergeRequest = state => !!state.currentMergeRequestId;
export const allBlobs = state => export const allBlobs = state =>
Object.keys(state.entries).reduce((acc, key) => acc.concat(state.entries[key]), []); Object.keys(state.entries).reduce((acc, key) => {
const entry = state.entries[key];
if (entry.type === 'blob') {
acc.push(entry);
}
return acc;
}, []);
...@@ -53,3 +53,5 @@ export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; ...@@ -53,3 +53,5 @@ export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const TOGGLE_FILE_FINDER = 'SHOW_FILE_FINDER';
...@@ -95,6 +95,11 @@ export default { ...@@ -95,6 +95,11 @@ export default {
delayViewerUpdated, delayViewerUpdated,
}); });
}, },
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, {
fileFindVisible,
});
},
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
...@@ -17,5 +17,5 @@ export default () => ({ ...@@ -17,5 +17,5 @@ export default () => ({
entries: {}, entries: {},
viewer: 'editor', viewer: 'editor',
delayViewerUpdated: false, delayViewerUpdated: false,
fileFindVisible: true, fileFindVisible: false,
}); });
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
} }
.ide-view { .ide-view {
position: relative;
display: flex; display: flex;
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$header-height});
margin-top: 0; margin-top: 0;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment