2
0
mirror of https://github.com/frappe/books.git synced 2025-02-06 22:18:34 +00:00
books/src/components/SearchBar.vue
2022-05-23 16:18:22 +05:30

321 lines
7.7 KiB
Vue

<script setup>
const keys = useKeys();
</script>
<template>
<div>
<!-- Search Bar Button -->
<button
@click="open"
class="
focus:outline-none
shadow-button
flex flex-row
gap-1
text-base text-gray-700
bg-gray-100
rounded-md
h-8
w-48
px-3
items-center
hover:bg-gray-200
"
>
<feather-icon name="search" class="w-4 h-4" />
<p>{{ t`Search` }}</p>
<div v-if="!inputValue" class="text-gray-500 ml-auto">
{{ modKey('k') }}
</div>
</button>
</div>
<!-- Search Modal -->
<Modal :open-modal="openModal">
<!-- Search Input -->
<div class="p-1">
<input
ref="input"
type="search"
autocomplete="off"
spellcheck="false"
:placeholder="t`Type to search...`"
v-model="inputValue"
@focus="search"
@input="search"
@keydown.up="up"
@keydown.down="down"
@keydown.enter="() => select()"
@keydown.esc="close"
class="
bg-gray-100
text-2xl
focus:outline-none
w-full
placeholder-gray-700
text-gray-900
rounded-md
p-3
"
/>
</div>
<hr v-if="suggestions.length" />
<!-- Search List -->
<div :style="`max-height: ${49 * 6 - 1}px`" class="overflow-scroll">
<div
v-for="(si, i) in suggestions"
:key="`${i}-${si.key}`"
ref="suggestions"
class="hover:bg-blue-100 cursor-pointer"
:class="idx === i ? 'bg-blue-100' : ''"
@click="select(i)"
>
<div
class="flex flex-row w-full justify-between px-3 items-center"
style="height: 48px"
>
<p class="text-gray-900">
{{ si.label }}
</p>
<div
class="text-base px-2 py-1 rounded-xl flex items-center"
:class="groupColorClassMap[si.group]"
>
{{ groupLabelMap[si.group] }}
</div>
</div>
<hr v-if="i !== suggestions.length - 1" />
</div>
</div>
<!-- Footer -->
<hr />
<div class="m-1 flex justify-between items-center flex-col gap-2 text-sm select-none">
<!-- Group Filters -->
<div class="flex flex-row gap-2">
<button
v-for="(g, i) in searchGroups"
:key="g"
class="border px-1 py-0.5 rounded-lg"
:class="getGroupFilterButtonClass(g)"
@click="groupFilters[g] = !groupFilters[g]"
>
{{ groupLabelMap[g]
}}<span
class="ml-2 whitespace-nowrap brightness-50 tracking-tighter"
:class="`text-${groupColorMap[g]}-500`"
>{{ modKey(String(i + 1)) }}</span
>
</button>
</div>
<!-- Keybindings Help -->
<div class="flex text-sm gap-8 text-gray-500">
<p>↑↓ {{ t`Navigate` }}</p>
<p>↩ {{ t`Select` }}</p>
<p><span class="tracking-tighter">esc</span> {{ t`Close` }}</p>
</div>
</div>
</Modal>
</template>
<script>
import { t } from 'fyo';
import { fuzzyMatch } from 'src/utils';
import { getBgTextColorClass } from 'src/utils/colors';
import { getSearchList, searchGroups } from 'src/utils/search';
import { routeTo } from 'src/utils/ui';
import { useKeys } from 'src/utils/vueUtils';
import { getIsNullOrUndef } from 'utils/';
import { nextTick, watch } from 'vue';
import Modal from './Modal.vue';
export default {
data() {
return {
idx: 0,
searchGroups,
openModal: false,
inputValue: '',
searchList: [],
groupFilters: {
List: true,
Report: true,
Create: true,
Page: true,
Docs: true,
},
};
},
components: { Modal },
mounted() {
this.makeSearchList();
watch(this.keys, (keys) => {
if (
keys.size === 2 &&
keys.has('KeyK') &&
(keys.has('MetaLeft') || keys.has('ControlLeft'))
) {
this.open();
}
if (!this.openModal) {
return;
}
if (keys.size === 1 && keys.has('Escape')) {
this.close();
}
const input = this.$refs.input;
if (!getIsNullOrUndef(input) && document.activeElement !== input) {
input.focus();
}
this.setFilter(keys);
});
this.openModal = false;
},
activated() {
this.openModal = false;
},
methods: {
setFilter(keys) {
if (!keys.has('MetaLeft') && !keys.has('ControlLeft')) {
return;
}
if (!keys.size === 2) {
return;
}
const matches = [...keys].join(',').match(/Digit(\d+)/);
if (!matches) {
return;
}
const digit = matches[1];
const index = parseInt(digit) - 1;
const group = searchGroups[index];
if (!group || this.groupFilters[group] === undefined) {
return;
}
this.groupFilters[group] = !this.groupFilters[group];
},
modKey(key) {
key = key.toUpperCase();
if (this.platform === 'Mac') {
return `${key}`;
}
return `Ctrl ${key}`;
},
open() {
this.openModal = true;
nextTick(() => {
this.$refs.input.focus();
});
},
close() {
this.openModal = false;
this.reset();
},
reset() {
this.searchGroups.forEach((g) => {
this.groupFilters[g] = true;
});
this.inputValue = '';
},
up() {
this.idx = Math.max(this.idx - 1, 0);
this.scrollToHighlighted();
},
down() {
this.idx = Math.max(
Math.min(this.idx + 1, this.suggestions.length - 1),
0
);
this.scrollToHighlighted();
},
select(idx) {
this.idx = idx ?? this.idx;
this.suggestions[this.idx]?.action();
this.close();
},
scrollToHighlighted() {
const ref = this.$refs.suggestions[this.idx];
ref.scrollIntoView({ block: 'nearest' });
},
async makeSearchList() {
const searchList = getSearchList();
this.searchList = searchList.map((d) => {
if (d.route && !d.action) {
d.action = () => {
routeTo(d.route);
};
}
return d;
});
},
getGroupFilterButtonClass(g) {
const isOn = this.groupFilters[g];
const color = this.groupColorMap[g];
if (isOn) {
return `${getBgTextColorClass(color)} border-${color}-100`;
}
return `text-${color}-600 border-${color}-100`;
},
},
computed: {
groupLabelMap() {
return {
Create: t`Create`,
List: t`List`,
Report: t`Report`,
Docs: t`Docs`,
Page: t`Page`,
};
},
groupColorMap() {
return {
Docs: 'blue',
Create: 'green',
List: 'teal',
Report: 'yellow',
Page: 'orange',
};
},
groupColorClassMap() {
return searchGroups.reduce((map, g) => {
map[g] = getBgTextColorClass(this.groupColorMap[g]);
return map;
}, {});
},
suggestions() {
const filters = new Set(
this.searchGroups.filter((g) => this.groupFilters[g])
);
return this.searchList
.filter((si) => filters.has(si.group))
.map((si) => ({
...fuzzyMatch(this.inputValue, `${si.label} ${si.group}`),
si,
}))
.filter(({ isMatch }) => isMatch)
.sort((a, b) => a.distance - b.distance)
.map(({ si }) => si);
},
},
};
</script>
<style scoped>
input[type='search']::-webkit-search-decoration,
input[type='search']::-webkit-search-cancel-button,
input[type='search']::-webkit-search-results-button,
input[type='search']::-webkit-search-results-decoration {
display: none;
}
</style>