407 lines
9.8 KiB
Vue
407 lines
9.8 KiB
Vue
<template>
|
|
<div>
|
|
<div
|
|
class="field has-addons"
|
|
:id="$id()"
|
|
>
|
|
<div class="control has-icons-left has-icons-right is-expanded">
|
|
<div
|
|
:class="['dropdown', pos, { 'is-active': focused }]"
|
|
style="width: 100%"
|
|
>
|
|
<div
|
|
class="dropdown-trigger"
|
|
style="width: 100%"
|
|
>
|
|
<input
|
|
:class="[
|
|
'input',
|
|
{
|
|
'has-text-dark': disabled,
|
|
},
|
|
]"
|
|
:disabled="disabled"
|
|
type="text"
|
|
@focus="setFocus"
|
|
@blur="lostFocus"
|
|
@keyup.enter="pressEnter"
|
|
@keyup.esc="lostFocus"
|
|
@keyup="beginSearch"
|
|
v-model="value"
|
|
:placeholder="placeholder"
|
|
/>
|
|
<span class="icon is-left">
|
|
<Icon
|
|
name="material-symbols:search"
|
|
:size="20"
|
|
/>
|
|
</span>
|
|
<span class="icon is-right">
|
|
<Icon
|
|
name="material-symbols:keyboard-arrow-down-rounded"
|
|
:size="20"
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div
|
|
class="dropdown-menu"
|
|
style="min-width: 100%"
|
|
role="menu"
|
|
@click="doClick()"
|
|
>
|
|
<div
|
|
class="dropdown-content px-3"
|
|
style="min-width: 100%"
|
|
>
|
|
<ScrollBox
|
|
v-if="data.length > 0"
|
|
v-bind="{
|
|
data: data,
|
|
name: field,
|
|
fontsize: 14,
|
|
maxheight: '200px',
|
|
notick: true,
|
|
}"
|
|
@selected="choose"
|
|
/>
|
|
<p
|
|
v-else
|
|
class="has-text-grey px-2 py-1"
|
|
>
|
|
<Icon
|
|
v-if="isLoading"
|
|
name="svg-spinners:90-ring"
|
|
:size="22"
|
|
/>
|
|
<span v-else>Không có giá trị thoả mãn</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="clearable && value"
|
|
class="control"
|
|
>
|
|
<button
|
|
class="button is-light"
|
|
@click="clearValue"
|
|
style="height: 100%"
|
|
type="button"
|
|
>
|
|
<span class="icon">
|
|
<Icon
|
|
name="material-symbols:close-rounded"
|
|
:size="16"
|
|
/>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<div
|
|
v-if="viewaddon"
|
|
class="control"
|
|
>
|
|
<button
|
|
class="button is-primary is-light"
|
|
@click="viewInfo()"
|
|
style="height: 100%"
|
|
type="button"
|
|
>
|
|
<span class="icon">
|
|
<Icon
|
|
name="material-symbols:visibility-rounded"
|
|
:size="20"
|
|
/>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<div
|
|
class="control"
|
|
v-if="addon"
|
|
>
|
|
<button
|
|
class="button is-success is-light"
|
|
@click="addNew()"
|
|
style="height: 100%"
|
|
type="button"
|
|
>
|
|
<span class="icon">
|
|
<Icon
|
|
name="material-symbols:add-rounded"
|
|
:size="18"
|
|
/>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<Modal
|
|
@dataevent="dataevent"
|
|
@close="showmodal = undefined"
|
|
v-bind="showmodal"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
const props = defineProps({
|
|
api: String,
|
|
field: String,
|
|
column: Array,
|
|
first: Boolean,
|
|
optionid: String,
|
|
filter: Object,
|
|
addon: Object,
|
|
viewaddon: Object,
|
|
position: String,
|
|
disabled: Boolean,
|
|
vdata: Array,
|
|
clearable: Boolean,
|
|
placeholder: String,
|
|
searchfield: Array,
|
|
});
|
|
const { $copy, $dialog, $empty, $find, $findapi, $findIndex, $getdata, $id, $nonAccent, $store } = useNuxtApp();
|
|
const emit = defineEmits(["option", "modalevent"]);
|
|
const search = ref();
|
|
const data = ref([]);
|
|
const isLoading = ref(false);
|
|
const timer = ref();
|
|
const value = ref();
|
|
const selected = ref();
|
|
const showmodal = ref();
|
|
const params = ref(props.api && $findapi(props.api).params);
|
|
const orgdata = ref();
|
|
const focused = ref(false);
|
|
const count1 = ref(0);
|
|
const count2 = ref(0);
|
|
const pos = ref();
|
|
|
|
getPos();
|
|
|
|
if (props.vdata) {
|
|
orgdata.value = props.vdata;
|
|
orgdata.value.forEach((v) => (v.search = $nonAccent(v[props.field])));
|
|
}
|
|
if (props.first) {
|
|
data.value = orgdata.value ? $copy(orgdata.value) : await getData();
|
|
if (props.optionid) {
|
|
let f = {};
|
|
f[props.field] = props.optionid;
|
|
if (props.optionid > 0) f = { id: props.optionid };
|
|
selected.value = $find(data.value, f);
|
|
if (selected.value && props.vdata) {
|
|
value.value = selected.value[props.field];
|
|
}
|
|
}
|
|
} else if (props.optionid) {
|
|
selected.value = await $getdata(props.api, { id: props.optionid }, undefined, true);
|
|
}
|
|
if (selected.value) doSelect(selected.value);
|
|
|
|
watch(
|
|
() => props.optionid,
|
|
() => {
|
|
if (props.optionid) selected.value = $find(data.value, { id: props.optionid });
|
|
if (selected.value) doSelect(selected.value);
|
|
else value.value = undefined;
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.filter,
|
|
async () => {
|
|
data.value = await getData();
|
|
},
|
|
);
|
|
|
|
watch(
|
|
() => props.vdata,
|
|
(newVal) => {
|
|
if (newVal) {
|
|
orgdata.value = $copy(props.vdata);
|
|
orgdata.value.forEach((v) => (v.search = $nonAccent(v[props.field])));
|
|
data.value = $copy(orgdata.value);
|
|
selected.value = undefined;
|
|
value.value = undefined;
|
|
if (props.optionid) selected.value = $find(data.value, { id: props.optionid });
|
|
if (selected.value) doSelect(selected.value);
|
|
}
|
|
},
|
|
);
|
|
|
|
function choose(v) {
|
|
focused.value = false;
|
|
count1.value = 0;
|
|
count2.value = 0;
|
|
doSelect(v);
|
|
}
|
|
|
|
function setFocus() {
|
|
focused.value = true;
|
|
count1.value = 0;
|
|
count2.value = 0;
|
|
}
|
|
|
|
function lostFocus() {
|
|
setTimeout(() => {
|
|
if (focused.value && count1.value === 0) focused.value = false;
|
|
}, 200);
|
|
}
|
|
|
|
function pressEnter() {
|
|
if (data.value.length === 0) return;
|
|
choose(data.value[0]);
|
|
}
|
|
|
|
function doClick() {
|
|
count1.value += 1;
|
|
}
|
|
|
|
function doSelect(option) {
|
|
if ($empty(option)) return;
|
|
emit("option", option);
|
|
emit("modalevent", { name: "option", data: option });
|
|
selected.value = option;
|
|
value.value = selected.value[props.field];
|
|
}
|
|
|
|
function clearValue() {
|
|
value.value = undefined;
|
|
selected.value = undefined;
|
|
emit("option", null);
|
|
emit("modalevent", { name: "option", data: null });
|
|
}
|
|
|
|
function findObject(val) {
|
|
const rows = $copy(orgdata.value);
|
|
if ($empty(val)) data.value = rows;
|
|
else {
|
|
const text = $nonAccent(val);
|
|
data.value = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0);
|
|
}
|
|
}
|
|
|
|
async function getData() {
|
|
isLoading.value = false;
|
|
params.value.filter = props.filter;
|
|
const data = await $getdata(props.api, undefined, params.value);
|
|
isLoading.value = true;
|
|
return data;
|
|
}
|
|
|
|
async function getApi(val) {
|
|
if (props.vdata) return findObject(val);
|
|
const text = val ? val.toLowerCase() : "";
|
|
const f = {};
|
|
|
|
// Sử dụng searchfield nếu có, nếu không thì dùng column
|
|
const fieldsToSearch = props.searchfield || props.column;
|
|
|
|
fieldsToSearch.map((v) => {
|
|
f[`${v}__icontains`] = text;
|
|
});
|
|
params.value.filter_or = f;
|
|
if (props.filter) params.value.filter = $copy(props.filter);
|
|
data.value = await $getdata(props.api, undefined, params.value);
|
|
}
|
|
|
|
function beginSearch(e) {
|
|
const val = e.target.value;
|
|
search.value = val;
|
|
clearTimeout(timer.value);
|
|
timer.value = setTimeout(() => getApi(val), 150);
|
|
}
|
|
|
|
function addNew() {
|
|
showmodal.value = props.addon;
|
|
}
|
|
|
|
function dataevent(v) {
|
|
console.log("SearchBox received dataevent:", v); // Debug log
|
|
|
|
if (!v || !v.id) {
|
|
console.error("Invalid data received in SearchBox:", v);
|
|
return;
|
|
}
|
|
|
|
// Tìm và cập nhật trong danh sách
|
|
const idx = $findIndex(data.value, { id: v.id });
|
|
if (idx < 0) {
|
|
// Nếu chưa có trong danh sách, thêm vào đầu
|
|
data.value.unshift(v);
|
|
console.log("Added new item to data:", v);
|
|
} else {
|
|
// Nếu đã có, cập nhật
|
|
data.value[idx] = v;
|
|
console.log("Updated existing item in data:", v);
|
|
}
|
|
|
|
// Cập nhật orgdata nếu có
|
|
if (orgdata.value) {
|
|
let orgIdx = $findIndex(orgdata.value, { id: v.id });
|
|
if (orgIdx < 0) {
|
|
orgdata.value.unshift(v);
|
|
// Thêm search field cho orgdata
|
|
if (props.field && v[props.field]) {
|
|
v.search = $nonAccent(v[props.field]);
|
|
}
|
|
} else {
|
|
orgdata.value[orgIdx] = v;
|
|
if (props.field && v[props.field]) {
|
|
orgdata.value[orgIdx].search = $nonAccent(v[props.field]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// **Tự động select item vừa tạo/cập nhật**
|
|
doSelect(v);
|
|
|
|
// Đóng modal
|
|
showmodal.value = undefined;
|
|
console.log("SearchBox data after update:", data.value);
|
|
}
|
|
|
|
function viewInfo() {
|
|
if (!selected.value)
|
|
return $dialog(
|
|
$store.lang === "vi" ? "Vui lòng lựa chọn trước khi xem thông tin." : "Please select before viewing",
|
|
$store.lang === "vi" ? "Thông báo" : "Notice",
|
|
);
|
|
showmodal.value = {
|
|
...props.viewaddon,
|
|
vbind: { row: selected.value },
|
|
};
|
|
}
|
|
|
|
function getPos() {
|
|
switch (props.position) {
|
|
case "is-top-left":
|
|
pos.value = "is-up is-left";
|
|
break;
|
|
case "is-top-right":
|
|
pos.value = "is-up is-right";
|
|
break;
|
|
case "is-bottom-left":
|
|
pos.value = "is-right";
|
|
break;
|
|
case "is-bottom-right":
|
|
pos.value = "is-right";
|
|
break;
|
|
}
|
|
}
|
|
</script>
|
|
<style scoped>
|
|
.field:not(:last-child) {
|
|
margin-bottom: 0;
|
|
}
|
|
.button.is-success {
|
|
&.is-light {
|
|
--bulma-button-background-l: 89%;
|
|
}
|
|
|
|
&:hover,
|
|
&.is-hovered {
|
|
--bulma-button-background-l-delta: -10%;
|
|
}
|
|
}
|
|
</style>
|