feat(laptops): implement keyboard arrow navigation and select-on-enter for employee assignment dropdown
This commit is contained in:
parent
5e46514f62
commit
ab27bfc34d
1 changed files with 37 additions and 5 deletions
42
laptops.php
42
laptops.php
|
|
@ -434,8 +434,13 @@
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input type="text" x-model="userSearchQuery"
|
<input type="text" x-model="userSearchQuery"
|
||||||
@focus="showUserDropdown = true; userSearchQuery = ''"
|
@focus="showUserDropdown = true; userSearchQuery = ''; userHighlightIndex = -1"
|
||||||
@input="showUserDropdown = true" placeholder="사용자 검색 (이름, 사번)"
|
@input="showUserDropdown = true; userHighlightIndex = -1"
|
||||||
|
@keydown.arrow-down.prevent="if(showUserDropdown) { userHighlightIndex = (userHighlightIndex + 1) % filteredUsers.length; adjustScroll(); }"
|
||||||
|
@keydown.arrow-up.prevent="if(showUserDropdown) { userHighlightIndex = (userHighlightIndex - 1 + filteredUsers.length) % filteredUsers.length; adjustScroll(); }"
|
||||||
|
@keydown.enter.prevent="if(showUserDropdown && userHighlightIndex >= 0 && filteredUsers[userHighlightIndex]) { selectUser(filteredUsers[userHighlightIndex]); } else if (showUserDropdown) { showUserDropdown = false; } else { submitAsset(); }"
|
||||||
|
@keydown.space="if(showUserDropdown && userHighlightIndex >= 0 && filteredUsers[userHighlightIndex]) { $event.preventDefault(); selectUser(filteredUsers[userHighlightIndex]); }"
|
||||||
|
placeholder="사용자 검색 (이름, 사번)"
|
||||||
:disabled="formData.status !== 'assigned'"
|
:disabled="formData.status !== 'assigned'"
|
||||||
:class="formData.status !== 'assigned' ? 'bg-slate-100 border-dashed cursor-not-allowed opacity-75' : 'bg-white'"
|
:class="formData.status !== 'assigned' ? 'bg-slate-100 border-dashed cursor-not-allowed opacity-75' : 'bg-white'"
|
||||||
class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm font-bold text-slate-700">
|
||||||
|
|
@ -458,16 +463,19 @@
|
||||||
<!-- User Search Dropdown -->
|
<!-- User Search Dropdown -->
|
||||||
<div x-show="showUserDropdown" x-transition
|
<div x-show="showUserDropdown" x-transition
|
||||||
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
|
class="absolute z-[120] mt-2 w-full bg-white rounded-2xl shadow-2xl border border-slate-100 max-h-60 overflow-y-auto overflow-x-hidden no-scrollbar">
|
||||||
<template x-for="user in filteredUsers" :key="user.id">
|
<template x-for="(user, index) in filteredUsers" :key="user.id">
|
||||||
<div @click="selectUser(user)"
|
<div @click="selectUser(user)"
|
||||||
|
:class="userHighlightIndex === index ? 'bg-blue-50' : ''"
|
||||||
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
|
class="px-4 py-3 hover:bg-blue-50 cursor-pointer flex items-center justify-between transition-colors group">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600"
|
<div class="text-sm font-bold text-slate-700 transition-colors"
|
||||||
|
:class="userHighlightIndex === index ? 'text-blue-600' : 'group-hover:text-blue-600'"
|
||||||
x-text="user.name"></div>
|
x-text="user.name"></div>
|
||||||
<div class="text-[10px] text-slate-400 font-medium"
|
<div class="text-[10px] text-slate-400 font-medium"
|
||||||
x-text="user.dept_name || '부서 정보 없음'"></div>
|
x-text="user.dept_name || '부서 정보 없음'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[10px] font-black text-slate-300 group-hover:text-blue-400"
|
<div class="text-[10px] font-black text-slate-300 transition-colors"
|
||||||
|
:class="userHighlightIndex === index ? 'text-blue-400' : 'group-hover:text-blue-400'"
|
||||||
x-text="user.emp_id"></div>
|
x-text="user.emp_id"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -608,6 +616,7 @@
|
||||||
disposal_recipient: '', disposal_date: '', real_user: '', disposal_user_id: ''
|
disposal_recipient: '', disposal_date: '', real_user: '', disposal_user_id: ''
|
||||||
},
|
},
|
||||||
userSearchQuery: '',
|
userSearchQuery: '',
|
||||||
|
userHighlightIndex: -1,
|
||||||
showUserDropdown: false,
|
showUserDropdown: false,
|
||||||
disposalSearchQuery: '',
|
disposalSearchQuery: '',
|
||||||
showDisposalDropdown: false,
|
showDisposalDropdown: false,
|
||||||
|
|
@ -660,6 +669,7 @@
|
||||||
this.formData.current_user_id = user.id;
|
this.formData.current_user_id = user.id;
|
||||||
this.userSearchQuery = `${user.name} (${user.emp_id})`;
|
this.userSearchQuery = `${user.name} (${user.emp_id})`;
|
||||||
this.showUserDropdown = false;
|
this.showUserDropdown = false;
|
||||||
|
this.userHighlightIndex = -1;
|
||||||
this.updateAssignedUserName();
|
this.updateAssignedUserName();
|
||||||
// Do not auto-change status to assigned here because user might be in stock mode
|
// Do not auto-change status to assigned here because user might be in stock mode
|
||||||
},
|
},
|
||||||
|
|
@ -667,12 +677,34 @@
|
||||||
clearUser() {
|
clearUser() {
|
||||||
this.formData.current_user_id = '';
|
this.formData.current_user_id = '';
|
||||||
this.userSearchQuery = '';
|
this.userSearchQuery = '';
|
||||||
|
this.userHighlightIndex = -1;
|
||||||
this.updateAssignedUserName();
|
this.updateAssignedUserName();
|
||||||
if (this.formData.status === 'assigned') {
|
if (this.formData.status === 'assigned') {
|
||||||
this.formData.status = 'stock';
|
this.formData.status = 'stock';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
adjustScroll() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const container = this.$el.querySelector('.max-h-60');
|
||||||
|
if (!container) return;
|
||||||
|
const items = container.querySelectorAll('.cursor-pointer');
|
||||||
|
const activeItem = items[this.userHighlightIndex];
|
||||||
|
if (!activeItem) return;
|
||||||
|
|
||||||
|
const containerTop = container.scrollTop;
|
||||||
|
const containerBottom = containerTop + container.clientHeight;
|
||||||
|
const elemTop = activeItem.offsetTop;
|
||||||
|
const elemBottom = elemTop + activeItem.offsetHeight;
|
||||||
|
|
||||||
|
if (elemTop < containerTop) {
|
||||||
|
container.scrollTop = elemTop;
|
||||||
|
} else if (elemBottom > containerBottom) {
|
||||||
|
container.scrollTop = elemBottom - container.clientHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
get isAssetTagDuplicate() {
|
get isAssetTagDuplicate() {
|
||||||
const tag = (this.formData.asset_tag || '').trim();
|
const tag = (this.formData.asset_tag || '').trim();
|
||||||
if (!tag) return false;
|
if (!tag) return false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue