282 lines
8.6 KiB
Vue
282 lines
8.6 KiB
Vue
|
<template>
|
||
|
<!-- <input
|
||
|
v-model="data.search"
|
||
|
@change="onChange"
|
||
|
type="text"
|
||
|
class="text-base font-medium block w-full rounded-md border transition ease-in-out focus:ring-1 border-gray-300 border-solid py-2 px-3 text-gray-700 placeholder-gray-400 focus:border-blue-200 focus:ring-blue-500 focus:outline-none"
|
||
|
v-bind:name="props.name"
|
||
|
/>
|
||
|
<ul v-if="data.isOpen" class="mt-1 border-2 border-slate-50 overflow-auto shadow-lg rounded list-none">
|
||
|
<li
|
||
|
:class="['hover:bg-blue-100 hover:text-blue-800', 'w-full list-none text-left py-2 px-3 cursor-pointer']"
|
||
|
v-for="(result, i) in data.results"
|
||
|
:key="i"
|
||
|
>
|
||
|
{{ result.name }}
|
||
|
</li>
|
||
|
</ul> -->
|
||
|
|
||
|
<!-- <div class="flex-col justify-center relative"> -->
|
||
|
<div class="relative">
|
||
|
<input
|
||
|
v-model="data.search"
|
||
|
type="text"
|
||
|
:class="inputElClass"
|
||
|
:name="props.name"
|
||
|
:placeholder="placeholder"
|
||
|
autocomplete="off"
|
||
|
@keydown.down="onArrowDown"
|
||
|
@keydown.up="onArrowUp"
|
||
|
/>
|
||
|
<svg
|
||
|
class="w-4 h-4 absolute left-2.5 top-3.5"
|
||
|
xmlns="http://www.w3.org/2000/svg"
|
||
|
fill="none"
|
||
|
viewBox="0 0 24 24"
|
||
|
stroke="currentColor"
|
||
|
>
|
||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||
|
</svg>
|
||
|
</div>
|
||
|
<!-- <ul v-if="data.isOpen" class="bg-white border border-gray-100 w-full mt-2 max-h-28 overflow-y-auto"> -->
|
||
|
<!-- :ref="(el) => { ul[i] = el }" -->
|
||
|
<ul v-if="data.isOpen" class="bg-white dark:bg-slate-800 w-full mt-2 max-h-28 overflow-y-auto scroll-smooth">
|
||
|
<li
|
||
|
class="pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-yellow-50 hover:text-gray-900"
|
||
|
:class="{
|
||
|
'bg-yellow-50 text-gray-900': i == selectedIndex,
|
||
|
}"
|
||
|
v-for="(result, i) in data.results"
|
||
|
:key="i"
|
||
|
:ref="
|
||
|
(el: HTMLLIElement | null) => {
|
||
|
ul[i] = el;
|
||
|
}
|
||
|
"
|
||
|
>
|
||
|
<svg class="absolute w-4 h-4 left-2 top-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||
|
<path
|
||
|
fill-rule="evenodd"
|
||
|
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
|
||
|
clip-rule="evenodd"
|
||
|
/>
|
||
|
</svg>
|
||
|
<!-- <b>Gar</b>{{ result.name }} -->
|
||
|
|
||
|
<!-- <span>
|
||
|
{{ makeBold(result.name) }}
|
||
|
</span> -->
|
||
|
<span
|
||
|
v-for="(item, index) in makeBold(result.name)"
|
||
|
:key="index"
|
||
|
:class="{
|
||
|
'font-bold': data.search.toLowerCase().includes(item.toLowerCase()),
|
||
|
'is-active': index == selectedIndex,
|
||
|
}"
|
||
|
>
|
||
|
{{ item }}
|
||
|
</span>
|
||
|
</li>
|
||
|
</ul>
|
||
|
<!-- </div> -->
|
||
|
</template>
|
||
|
|
||
|
<script setup lang="ts">
|
||
|
import { reactive, ref, computed, Ref, watch } from 'vue';
|
||
|
import axios from 'axios';
|
||
|
let props = defineProps({
|
||
|
name: {
|
||
|
type: String,
|
||
|
required: false,
|
||
|
default: 'autocomplete',
|
||
|
},
|
||
|
source: {
|
||
|
type: [String, Array, Function],
|
||
|
required: true,
|
||
|
default: '',
|
||
|
},
|
||
|
label: {
|
||
|
type: String,
|
||
|
required: false,
|
||
|
default: 'name',
|
||
|
},
|
||
|
responseProperty: {
|
||
|
type: String,
|
||
|
required: false,
|
||
|
default: 'name',
|
||
|
},
|
||
|
placeholder: {
|
||
|
type: String,
|
||
|
default: null,
|
||
|
},
|
||
|
icon: {
|
||
|
type: String,
|
||
|
default: null,
|
||
|
},
|
||
|
required: Boolean,
|
||
|
borderless: Boolean,
|
||
|
transparent: Boolean,
|
||
|
ctrlKFocus: Boolean,
|
||
|
});
|
||
|
|
||
|
const inputElClass = computed(() => {
|
||
|
const base = [
|
||
|
'px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full',
|
||
|
'dark:placeholder-gray-400',
|
||
|
'h-12',
|
||
|
props.borderless ? 'border-0' : 'border',
|
||
|
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
|
||
|
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
||
|
];
|
||
|
// if (props.icon) {
|
||
|
base.push('pl-10');
|
||
|
// }
|
||
|
return base;
|
||
|
});
|
||
|
|
||
|
let search = ref('');
|
||
|
let data = reactive({
|
||
|
search: search,
|
||
|
isOpen: false,
|
||
|
results: [],
|
||
|
});
|
||
|
let error = ref('');
|
||
|
let selectedIndex: Ref<number> = ref(0);
|
||
|
// const listItem = ref(null);
|
||
|
const ul: Ref<Array<HTMLLIElement | null>> = ref([]);
|
||
|
|
||
|
watch(selectedIndex, (selectedIndex) => {
|
||
|
if (selectedIndex != null && ul.value != null) {
|
||
|
const currentElement: HTMLLIElement | null = ul.value[selectedIndex];
|
||
|
currentElement &&
|
||
|
currentElement.scrollIntoView({
|
||
|
behavior: 'smooth',
|
||
|
block: 'nearest',
|
||
|
inline: 'start',
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
watch(search, async () => {
|
||
|
await onChange();
|
||
|
});
|
||
|
|
||
|
// function clear() {
|
||
|
// data.search = "";
|
||
|
// data.isOpen = false;
|
||
|
// data.results = [];
|
||
|
// error.value = "";
|
||
|
// // this.$emit("clear");
|
||
|
// }
|
||
|
|
||
|
// function onChange() {
|
||
|
// if (!props.source || !data.search) {
|
||
|
// return false;
|
||
|
// }
|
||
|
|
||
|
// data.isOpen = true;
|
||
|
// arrayLikeSearch(props.source);
|
||
|
// }
|
||
|
async function onChange() {
|
||
|
if (!props.source || !data.search) return false;
|
||
|
|
||
|
selectedIndex.value = 0;
|
||
|
|
||
|
if (data.search.length >= 2) {
|
||
|
data.isOpen = true;
|
||
|
switch (true) {
|
||
|
case typeof props.source === 'string':
|
||
|
return await request(props.source, data.search);
|
||
|
// case typeof props.source === 'function':
|
||
|
// return props.source(data.search).then((response) => {
|
||
|
// data.results = getResults(response);
|
||
|
// });
|
||
|
case Array.isArray(props.source):
|
||
|
return arrayLikeSearch(props.source);
|
||
|
default:
|
||
|
throw new Error('typeof source is ' + typeof props.source);
|
||
|
}
|
||
|
} else {
|
||
|
data.results = [];
|
||
|
data.isOpen = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getResults(response) {
|
||
|
// if (props.responseProperty) {
|
||
|
// let foundObj;
|
||
|
// JSON.stringify(response, (_, nestedValue) => {
|
||
|
// if (nestedValue && nestedValue[props.responseProperty]) foundObj = nestedValue[props.responseProperty];
|
||
|
|
||
|
// return nestedValue;
|
||
|
// });
|
||
|
// return foundObj;
|
||
|
// }
|
||
|
if (Array.isArray(response)) {
|
||
|
return response;
|
||
|
}
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
// function setResult(result) {
|
||
|
// data.search = result[props.label];
|
||
|
// data.isOpen = false;
|
||
|
// }
|
||
|
|
||
|
// function request(url) {
|
||
|
// return axios.get(url).then((response) => {
|
||
|
// data.results = getResults(response);
|
||
|
// });
|
||
|
// }
|
||
|
async function request(url, param) {
|
||
|
try {
|
||
|
let response = await searchTerm(url, param);
|
||
|
error.value = '';
|
||
|
data.results = getResults(response);
|
||
|
// this.results = res.data;
|
||
|
// this.loading = false;
|
||
|
} catch (error) {
|
||
|
error.value = error.message;
|
||
|
// this.loading = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async function searchTerm(term: string, param): Promise<any> {
|
||
|
let res = await axios.get(term, { params: { filter: param } });
|
||
|
// console.log(res.data);
|
||
|
return res.data; //.response;//.docs;
|
||
|
}
|
||
|
// async function request(term: string): Promise<any> {
|
||
|
// let res = await axios.get('/api/persons', { params: { filter: term } });
|
||
|
// return res.data; //.response;//.docs;
|
||
|
// }
|
||
|
|
||
|
function arrayLikeSearch(items) {
|
||
|
data.results = items.filter((item) => {
|
||
|
return item.toLowerCase().indexOf(data.search.toLowerCase()) > -1;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function makeBold(suggestion) {
|
||
|
const query = data.search.valueOf();
|
||
|
const regex = new RegExp(query.split('').join('-?'), 'i');
|
||
|
const test = suggestion.replace(regex, (match) => '<split>' + match + '<split>');
|
||
|
// return suggestion.match(regex);
|
||
|
// const splitWord = suggestion.match(regex);
|
||
|
return test.split('<split>');
|
||
|
}
|
||
|
|
||
|
function onArrowDown() {
|
||
|
if (data.results.length > 0) {
|
||
|
selectedIndex.value = selectedIndex.value === data.results.length - 1 ? 0 : selectedIndex.value + 1;
|
||
|
// const currentElement: HTMLLIElement = ul.value[selectedIndex.value];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function onArrowUp() {
|
||
|
if (data.results.length > 0) {
|
||
|
selectedIndex.value = selectedIndex.value == 0 || selectedIndex.value == -1 ? data.results.length - 1 : selectedIndex.value - 1;
|
||
|
}
|
||
|
}
|
||
|
</script>
|