Skip to content

Commit

Permalink
feat: add arrow navigation to projects
Browse files Browse the repository at this point in the history
  • Loading branch information
Kholid060 committed Sep 16, 2021
1 parent e11fcc2 commit d53cf54
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,13 @@
</li>
</ul>
</div>
<div
class="bg-gray-900 text-gray-200 text-sm rounded-lg min-w-[200px]"
>
<div class="bg-gray-900 text-gray-200 text-sm rounded-lg min-w-[200px]">
<p class="flex-1 text-gray-400 p-2">Logs</p>
<pre
class="max-h-64 max-w-sm overflow-auto scroll px-2 pb-2"
style="font-family: 'Jetbrains mono', monospace;"
>{{ logs }}</pre>
style="font-family: 'Jetbrains mono', monospace"
>{{ logs }}</pre
>
</div>
</div>
</ui-popover>
Expand Down Expand Up @@ -164,7 +163,9 @@ export default {
logs.value = '';
ipcRenderer.callMain('terminal:log').then((data) => {
const terminalId = Object.keys(data).find((item) => item.startsWith('package'));
const terminalId = Object.keys(data).find((item) =>
item.startsWith('package')
);
if (!terminalId) return;
Expand Down
5 changes: 5 additions & 0 deletions packages/renderer/src/components/home/HomeProjects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,8 @@ export default {
},
};
</script>
<style>
.project-card.is-active {
@apply ring-2 ring-primary;
}
</style>
4 changes: 3 additions & 1 deletion packages/renderer/src/components/home/view/ViewGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
<ui-card
v-for="project in projects"
:key="project.id"
class="list-transition hover:ring-2 hover:ring-gray-700"
:data-project-id="project.id"
:data-project-name="project.name"
class="list-transition hover:ring-2 hover:ring-gray-700 project-card"
>
<router-link :to="`/project/${project.id}`" class="block text-overflow">
{{ project.name }}
Expand Down
4 changes: 3 additions & 1 deletion packages/renderer/src/components/home/view/ViewList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
<ui-list-item
v-for="project in projects"
:key="project.id"
class="group even:bg-gray-500 even:bg-opacity-5"
:data-project-id="project.id"
:data-project-name="project.name"
class="group even:bg-gray-500 even:bg-opacity-5 project-card"
>
<ui-button
class="mr-4"
Expand Down
5 changes: 4 additions & 1 deletion packages/renderer/src/components/package/AddPackageModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@
lazy
></ui-img>
<div class="text-overflow flex-1 mr-4">
<p class="text-overflow cursor-pointer" @click="packageDetails(item.package.name)">
<p
class="text-overflow cursor-pointer"
@click="packageDetails(item.package.name)"
>
{{ item.package.name }}
<span class="text-gray-300 text-sm"
>({{ item.package.version }})</span
Expand Down
70 changes: 69 additions & 1 deletion packages/renderer/src/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,19 @@
}
</route>
<script>
import { computed, shallowReactive, onMounted } from 'vue';
import {
computed,
shallowRef,
shallowReactive,
onMounted,
onUnmounted,
watch,
} from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { nanoid } from 'nanoid';
import { debounce } from '@/utils/helper';
import KeyboardNavigation from '@/utils/keyboardNavigation';
import Project from '@/models/project';
import HomeNav from '@/components/home/HomeNav.vue';
import HomeProjects from '@/components/home/HomeProjects.vue';
Expand All @@ -47,8 +57,11 @@ export default {
components: { HomeNav, HomeProjects },
setup() {
const store = useStore();
const router = useRouter();
const { ipcRenderer, existsSync, path } = window.electron;
const keyboardNavigation = shallowRef(null);
const state = shallowReactive({
search: '',
viewType: 'list',
Expand Down Expand Up @@ -143,9 +156,64 @@ export default {
store.dispatch('saveToStorage', 'projects');
});
}
function deleteProject(name, id) {
const confirm = window.confirm(`Are you sure want to delete "${name}"?`);
if (confirm) Project.delete(id);
}
const navigationBreakpoints = {
default: 2,
'(min-width: 1024px)': 3,
'(min-width: 1280px)': 4,
};
watch(
projects,
debounce(() => {
keyboardNavigation.value?.refresh();
}, 200)
);
watch(
() => state.viewType,
debounce((value) => {
keyboardNavigation.value?.setOptions({
breakpoints:
value === 'grid' ? navigationBreakpoints : { default: 1 },
});
}, 200)
);
onMounted(() => {
state.viewType = localStorage.getItem('view-type') || 'list';
keyboardNavigation.value = new KeyboardNavigation({
itemSelector: '.project-card',
});
keyboardNavigation.value.on('keydown', ({ key }, activeItem) => {
if (!activeItem) return;
const { projectId, projectName } = activeItem.dataset;
switch (key) {
case 'Enter':
router.push(`/project/${projectId}`);
break;
case 'Backspace':
case 'Delete':
deleteProject(projectName, projectId);
break;
default:
}
});
setTimeout(() => {
keyboardNavigation.value.refresh();
}, 250);
});
onUnmounted(() => {
keyboardNavigation.value.destroy();
});
return {
Expand Down
147 changes: 147 additions & 0 deletions packages/renderer/src/utils/keyboardNavigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
class EventHandler {
constructor() {
this.keydownHandler = this._keydownHandler.bind(this);
}

activeItemIndex() {
return this.items.indexOf(this.activeItem);
}

getBreakpoint() {
let breakpoint = this.breakpoints?.default || 1;

Object.keys(this.breakpoints || {}).forEach((mediaQuery) => {
if (window.matchMedia(mediaQuery).matches)
breakpoint = this.breakpoints[mediaQuery];
});

return breakpoint;
}

_keydownHandler(event) {
const keyHandlers = {
ArrowUp: this.upHandler.bind(this),
ArrowDown: this.downHandler.bind(this),
ArrowLeft: this.leftHandler.bind(this),
ArrowRight: this.rightHandler.bind(this),
};
const elementBlacklist = ['INPUT', 'SELECT', 'TEXTAREA'];
const isInBlacklist = elementBlacklist.includes(event.target.tagName);

if (keyHandlers[event.key] && !isInBlacklist) {
event.preventDefault();

this.activeItem?.classList.remove(...this.activeClass);
keyHandlers[event.key]();
this.activeItem?.classList.add(...this.activeClass);
this.activeItem?.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else if (event.key === 'Enter') {
this._fireEvent('select', this.activeItem);
}

this._fireEvent('keydown', event, this.activeItem);
}

upHandler() {
const brekpoint = this.getBreakpoint();
const prevIndex = this.activeItemIndex() - brekpoint;

if (prevIndex < 0) this.activeItem = this.items[this.items.length - 1];
else this.activeItem = this.items[prevIndex];
}

downHandler() {
const brekpoint = this.getBreakpoint();
const nextIndex = this.activeItemIndex() + brekpoint;

if (nextIndex > this.items.length - 1) this.activeItem = this.items[0];
else this.activeItem = this.items[nextIndex];
}

leftHandler() {
const index = this.activeItemIndex();
const prevItem = this.items[index - 1];

if (prevItem) this.activeItem = prevItem;
else if (index <= 0) this.activeItem = this.items[this.items.length - 1];
}

rightHandler() {
const index = this.activeItemIndex();
const nextItem = this.items[index + 1];

if (nextItem) this.activeItem = nextItem;
else if (index + 1 > this.items.length - 1) this.activeItem = this.items[0];
}
}

class KeyboardNavigation extends EventHandler {
constructor({
container = document,
itemSelector = '',
activeClass = 'is-active',
breakpoints,
}) {
super();

this.itemSelector = itemSelector;
this.activeClass = activeClass.split(' ');
this.breakpoints = breakpoints;
this.container =
typeof container === 'string'
? document.querySelector(container)
: container;
this.activeItem = null;
this.items = [];
this.listeners = {};

this._retrieveItems();
window.addEventListener('keydown', this.keydownHandler);
}

on(name, callback) {
(this.listeners[name] = this.listeners[name] || []).push(callback);
}

refresh() {
this._retrieveItems();
this.activeItem?.classList.remove(...this.activeClass);
this.activeItem = null;
}

destroy() {
window.removeEventListener('keydown', this.keydownHandler);

this.listeners = {};
this.activeItem?.classList.remove(...this.activeClass);
}

setOptions(options = {}, refresh = true) {
const mutableKeys = [
'container',
'itemSelector',
'activeClass',
'breakpoints',
];

for (const key in options) {
if (!mutableKeys.includes(key)) continue;

this[key] = options[key];
}

if (refresh) this.refresh();
}

_retrieveItems() {
this.items = [...this.container.querySelectorAll(this.itemSelector)];
}

_fireEvent(name, ...params) {
(this.listeners[name] || []).forEach((callback) => {
callback(...params);
});
}
}

export default KeyboardNavigation;

0 comments on commit d53cf54

Please sign in to comment.