Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spotlight search. #66

Merged
merged 3 commits into from Mar 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions resources/js/app.js
Expand Up @@ -15,6 +15,7 @@ const router = new VueRouter({

Vue.component('alert', require('./components/Alert').default);
Vue.component('omnibox', require('./components/Omnibox').default);
Vue.component('spotlight', require('./components/Spotlight').default);
Vue.component('site-header', require('./components/SiteHeader').default);
Vue.component('request-tabs', require('./components/RequestTabs').default);
Vue.component('sidebar-menu', require('./components/SidebarMenu').default);
Expand All @@ -38,6 +39,9 @@ new Vue({
message: '',
autoClose: 0,
},
spotlight: {
open: false
}
};
},
});
4 changes: 3 additions & 1 deletion resources/js/components/ContentSpace.vue
Expand Up @@ -19,7 +19,9 @@ export default {
<circle fill="#fff" cx="18" cy="18.008" r="3.055" />
<circle fill="#f3f3f3" cx="18" cy="18.008" r="1.648" /></svg>

<p class="text-center text-gray-500 text-lg">{{ description }}</p>
<slot>
<p class="text-center text-gray-600">{{ description }}</p>
</slot>
</div>
</section>
</template>
20 changes: 18 additions & 2 deletions resources/js/components/SidebarMenu.vue
Expand Up @@ -20,13 +20,13 @@ export default {

mounted() {
this.loadRequests();
this.spotlightWithKey();
},

methods: {
toggle() {
this.isOpen = !this.isOpen
},

loadRequests() {
this.ready = false;

Expand All @@ -35,6 +35,17 @@ export default {
this.ready = true;
});
},
openSpotlight() {
this.$root.spotlight.open = true;
},
spotlightWithKey() {
document.onkeyup = e => {
if (e.ctrlKey && e.code == 'Space') {
e.preventDefault();
this.openSpotlight();
}
}
}
}
}
</script>
Expand All @@ -46,7 +57,12 @@ export default {
<h3 class="font-semibold text-gray-700">Requests</h3>
</div>
<div class="inline-flex items-center">
<a href="#" class="ml-3" @click.prevent="loadRequests" title="refresh">
<a href="#" @click.prevent="openSpotlight()" title="spotlight search">
<svg class="h-4 w-4 fill-current text-gray-300 hover:text-gray-500" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6 11.2c.037.028.073.059.107.093l3 3a1 1 0 1 1-1.414 1.414l-3-3a1.009 1.009 0 0 1-.093-.107 7 7 0 1 1 1.4-1.4zM7 12A5 5 0 1 0 7 2a5 5 0 0 0 0 10z" fill-rule="evenodd"></path>
</svg>
</a>
<a href="#" class="ml-4" @click.prevent="loadRequests" title="refresh">
<svg class="h-5 w-5 fill-current text-gray-300 hover:text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M10 3v2a5 5 0 0 0-3.54 8.54l-1.41 1.41A7 7 0 0 1 10 3zm4.95 2.05A7 7 0 0 1 10 17v-2a5 5 0 0 0 3.54-8.54l1.41-1.41zM10 20l-4-4 4-4v8zm0-12V0l4 4-4 4z" />
</svg>
Expand Down
235 changes: 235 additions & 0 deletions resources/js/components/Spotlight.vue
@@ -0,0 +1,235 @@
<script>
export default {
created() {
document.addEventListener('keydown', this.handleEventKey);
},

destroyed() {
document.removeEventListener('keydown', this.handleEventKey);
},

data() {
return {
query: '',
endpointList: [],
currentActiveItem: null
}
},

mounted() {
this.getEndpoints();
this.focusedSearchInput();
},

computed: {
filteredEndpoints() {
const normalizedQuery = this.query.toLowerCase().trim();
return this.endpointList.filter(item => {
return item.title.toLowerCase().includes(normalizedQuery);
})
},
lastItem() {
return this.filteredEndpoints.length - 1;
},
resultsNotEmpty() {
return this.query && this.filteredEndpoints.length > 0;
}
},

methods: {
focusedSearchInput() {
this.$nextTick(() => {
this.$refs.spotlightSearchInput.focus();
})
},
async getEndpoints() {
try {
const { data } = await this.http('/' + Compass.path + '/request');
const endpoints = await data.data.list.map(item => ({
id: item.id,
title: item.title,
description: item.description,
method: item.content.selectedMethod || item.info.methods[0]
}));

this.endpointList = endpoints;
} catch (err) {/* throw */}
},
handleEventKey(e) {
e.stopPropagation();
if (e.key == 'Escape') {
this.close();
}
},
close() {
this.$root.spotlight.open = false;
},
onPointedItem(index) {
this.currentActiveItem = index;
this.focusedSearchInput();
},
updateQuery(val) {
this.query = val;
if (this.resultsNotEmpty) {
this.currentActiveItem = 0;
}
},
pointerDown() {
if (this.resultsNotEmpty && this.currentActiveItem < this.lastItem) {
this.currentActiveItem++
this.adjustScroll();
// this.debugPointer();
}
},
pointerUp() {
if (this.resultsNotEmpty && this.currentActiveItem > 0) {
this.currentActiveItem--
this.adjustScroll();
// this.debugPointer();
}
},
adjustScroll() {
if (this.highlightedItem().distance.top <= this.viewport().top) {
return this.$refs.results.scrollTop = this.highlightedItem().distance.top;
} else if (this.highlightedItem().distance.bottom >= this.viewport().bottom) {
return this.$refs.results.scrollTop = this.viewport().top + this.highlightedItem().height;
}
},
highlightedItem() {
const itemHeight = this.$refs.item[this.currentActiveItem].$el.offsetHeight;
const pointerTop = itemHeight * this.currentActiveItem;
const pointerBottom = pointerTop + itemHeight;

return {
height: itemHeight,
distance: {
top: pointerTop,
bottom: pointerBottom
}
}
},
viewport() {
return {
top: this.$refs.results.scrollTop,
bottom: this.$refs.results.offsetHeight + this.$refs.results.scrollTop
}
},
goTo() {
if (this.resultsNotEmpty) {
const endpoint = this.filteredEndpoints[this.currentActiveItem];
this.$router.push({name: 'cortex', params: {id: endpoint.id}}).catch(err => {/* throw */});
this.close();
}
},
debugPointer() {
console.log('-----------------------------------');
console.log('Viewport Top: ', this.viewport().top + 'px');
console.log('Viewport Bottom: ', this.viewport().bottom + 'px');
console.log('Scroll Top: ', this.$refs.results.scrollTop + 'px');
console.log('Highlighted item (top): ', this.highlightedItem().distance.top + 'px -- (distance)');
console.log('Highlighted item (bottom): ', this.highlightedItem().distance.bottom + 'px -- (distance)');
console.log('-----------------------------------');
}
}
}
</script>

<template>
<div v-show="$root.spotlight.open">
<transition name="modal">
<div class="flex items-start justify-center z-50 fixed inset-0 overflow-y-auto modal-mask">
<div class="outline-none my-10 modal-container" aria-modal="true" tabindex="-1">
<div class="spotlight-search-container">
<div class="spotlight-search-contents">
<div class="spotlight-search-bar">
<div class="flex items-center py-4 pl-3 rounded-t-lg bg-white">
<div class="pointer-events-none mr-3">
<svg class="fill-current pointer-events-none text-gray-400 w-3 h-3" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6 11.2c.037.028.073.059.107.093l3 3a1 1 0 1 1-1.414 1.414l-3-3a1.009 1.009 0 0 1-.093-.107 7 7 0 1 1 1.4-1.4zM7 12A5 5 0 1 0 7 2a5 5 0 0 0 0 10z" fill-rule="evenodd"></path>
</svg>
</div>
<input
ref="spotlightSearchInput"
class="spotlight-search-input"
type="text"
placeholder="Type to search"
autocomplete="off"
@input="updateQuery($event.target.value)"
@keydown.down.prevent="pointerDown()"
@keydown.up.prevent="pointerUp()"
@keypress.enter.prevent.stop.self="goTo()">
</div>
</div>
<div v-if="query && !filteredEndpoints.length" class="spotlight-search-results-empty">
No results
</div>
<div v-if="resultsNotEmpty" class="spotlight-search-results" ref="results">
<div class="px-2 pt-2">
<!-- <div class="uppercase text-xs font-medium text-gray-500 ml-1 mb-2">Results</div> -->
<router-link
v-for="(endpoint, index) in filteredEndpoints"
:key="index"
:to="{name:'cortex', params:{id: endpoint.id}}"
:class="currentActiveItem==index ? 'text-gray-100 bg-primary' : 'text-gray-500'"
class="flex items-center justify-between rounded-lg p-2 cursor-pointer no-underline outline-none"
tabindex="-1"
ref="item"
@mouseenter.native="onPointedItem(index)"
@click.native="close">

<div class="w-full text-xs leading-5 overflow-hidden mr-3 font-normal">
<div class="mb-1">
<span :class="{'text-gray-600': currentActiveItem!=index}" class="font-medium uppercase">
{{ endpoint.method }}
</span>
<span class="capitalize">
— {{ endpoint.title }}
</span>
</div>
<div v-show="currentActiveItem==index" class="capitalize font-normal truncate">
<span :class="endpoint.description ? '' : 'italic'">
{{ endpoint.description || 'No description available' }}
</span>
</div>
</div>
<div>
<svg :class="{'text-gray-400': currentActiveItem!=index}" class="fill-current w-3 h-3" style="transform: scaleY(-1);" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 5a5 5 0 0 1 0 10 1 1 0 0 1 0-2 3 3 0 0 0 0-6l-6.586-.007L6.45 9.528a1 1 0 0 1-1.414 1.414L.793 6.7a.997.997 0 0 1 0-1.414l4.243-4.243A1 1 0 0 1 6.45 2.457L3.914 4.993z" fill-rule="evenodd"></path>
</svg>
</div>
</router-link>
</div>
</div>
<div class="spotlight-search-footer">
<div class="flex">
<div class="flex items-center mr-4">
<svg class="fill-current pointer-events-none text-gray-500" style="width: 10px;height: 10px;" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M9 3.417V15a1 1 0 0 1-2 0V3.417L2.409 8.008A1 1 0 0 1 .993 6.593l6.3-6.3a1 1 0 0 1 1.414 0l6.3 6.3a1 1 0 0 1-1.416 1.415z" fill-rule="evenodd"></path>
</svg>
<svg class="fill-current pointer-events-none text-gray-500" style="width: 10px;height: 10px;" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M9 12.583l4.591-4.591a1 1 0 0 1 1.416 1.415l-6.3 6.3a1 1 0 0 1-1.414 0l-6.3-6.3A1 1 0 0 1 2.41 7.992L7 12.583V1a1 1 0 1 1 2 0z" fill-rule="evenodd"></path>
</svg>
<span class="ml-1">Navigate</span>
</div>
<div class="flex items-center mr-4">
<svg class="fill-current pointer-events-none text-gray-500" style="width: 10px;height: 10px; transform: scaleY(-1);" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 5a5 5 0 0 1 0 10 1 1 0 0 1 0-2 3 3 0 0 0 0-6l-6.586-.007L6.45 9.528a1 1 0 0 1-1.414 1.414L.793 6.7a.997.997 0 0 1 0-1.414l4.243-4.243A1 1 0 0 1 6.45 2.457L3.914 4.993z" fill-rule="evenodd"></path>
</svg>
<span class="ml-1">Return</span>
</div>
</div>
<div class="flex items-center">
Press
<kbd class="font-mono mx-2 border border-gray-200 rounded shadow-sm inline-block align-middle font-semibold text-gray-600" style="padding: 3px 5px;">
esc
</kbd>
to exit
</div>
</div>
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
14 changes: 13 additions & 1 deletion resources/js/pages/welcome.vue
Expand Up @@ -7,5 +7,17 @@ export default {
</script>

<template>
<content-space />
<content-space>
<div class="flex items-center text-center text-gray-600">
Press
<kbd class="mx-2 inline-block align-middle text-xs font-mono border pointer-events-none border-gray-200 rounded shadow-sm" style="padding: 3px 5px;">
ctrl
</kbd>
+
<kbd class="mx-2 inline-block align-middle text-xs font-mono border pointer-events-none border-gray-200 rounded shadow-sm" style="padding: 3px 5px;">
space
</kbd>
to find endpoint
</div>
</content-space>
</template>
4 changes: 3 additions & 1 deletion resources/sass/app.scss
Expand Up @@ -2,8 +2,10 @@
@tailwind components;

/* purgecss start ignore */
@import "multiselect";
@import "modal";
@import "spotlight";
@import "codemirror";
@import "multiselect";
/* purgecss end ignore */

@tailwind utilities;
34 changes: 34 additions & 0 deletions resources/sass/modal.scss
@@ -0,0 +1,34 @@
.modal-mask {
background: rgba(94, 63, 59, 0.1);
transition: opacity .3s ease;
}

.modal-container {
transition: all .3s ease;
}

.modal-enter {
opacity: 0;
}

.modal-leave-active {
opacity: 0;
}

.modal-enter .modal-container,
.modal-leave-active .modal-container {
transform: scale(1.1);
}

.fade-enter-active {
transition: opacity .5s;
}

.fade-leave-active {
transition: opacity 0s;
}

.fade-enter,
.fade-leave-to {
opacity: 0;
}