refactor: reimplement command palette using a custom overlay instead of the native dialog element

This commit is contained in:
pbao9
2026-02-15 02:52:20 +07:00
parent ab8831b71a
commit 046f61df7e
7 changed files with 345 additions and 122 deletions
@@ -24,111 +24,113 @@ extractRoutes($sidebar, $menuItems);
$validMenuItems = collect($menuItems)->filter(fn($item) => Route::has($item['route']))->values();
@endphp
<dialog id="cmdk-dialog" class="cmdk-dialog">
<div class="cmdk">
<input type="text" class="cmdk-input" placeholder="Search pages...">
<div class="cmdk-list">
@forelse($validMenuItems as $item)
<a href="{{ route($item['route'], $item['params']) }}"
class="cmdk-item"
data-value="{{ strtolower($item['title']) }}">
<span>{{ $item['title'] }}</span>
@if($item['group'])
<span class="cmdk-group">{{ $item['group'] }}</span>
@endif
</a>
@empty
<div class="cmdk-empty">No pages found</div>
@endforelse
<div id="command-palette-container" style="display: none;">
<div class="command-palette-backdrop" id="cmdk-backdrop"></div>
<div class="command-palette-wrapper" id="cmdk-wrapper">
<div class="cmdk">
<input type="text" class="cmdk-input" placeholder="Search pages..." id="cmdk-input">
<div class="cmdk-list" id="cmdk-list">
@forelse($validMenuItems as $item)
<a href="{{ route($item['route'], $item['params']) }}"
class="cmdk-item"
data-value="{{ strtolower($item['title']) }}">
<span>{{ $item['title'] }}</span>
@if($item['group'])
<span class="cmdk-group">{{ $item['group'] }}</span>
@endif
</a>
@empty
<div class="cmdk-empty">No pages found</div>
@endforelse
</div>
</div>
</div>
</dialog>
</div>
@push('custom-scripts')
<script>
(function() {
const searchInput = document.getElementById('navbarForm');
const dialog = document.getElementById('cmdk-dialog');
var container = document.getElementById('command-palette-container');
var backdrop = document.getElementById('cmdk-backdrop');
var wrapper = document.getElementById('cmdk-wrapper');
var input = document.getElementById('cmdk-input');
var searchInput = document.getElementById('navbarForm');
var cmdkItems = document.querySelectorAll('.cmdk-item');
console.log('Search input:', searchInput);
console.log('Dialog:', dialog);
if (!searchInput) {
console.error('navbarForm not found');
return;
}
if (!dialog) {
console.error('cmdk-dialog not found');
return;
}
const cmdkInput = dialog.querySelector('.cmdk-input');
const cmdkItems = dialog.querySelectorAll('.cmdk-item');
console.log('CMDK items:', cmdkItems.length);
searchInput.addEventListener('focus', function() {
console.log('Focus triggered');
dialog.showModal();
});
searchInput.addEventListener('input', function(e) {
const query = e.target.value.toLowerCase();
console.log('Search query:', query);
if (!dialog.hasAttribute('open')) {
dialog.showModal();
}
cmdkItems.forEach(function(item) {
const value = item.dataset.value || item.textContent.toLowerCase();
const match = query === '' || value.includes(query);
item.style.display = match ? '' : 'none';
});
});
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
dialog.showModal();
cmdkInput.focus();
}
});
dialog.addEventListener('click', function(e) {
if (e.target === dialog) {
dialog.close();
searchInput.blur();
}
});
dialog.addEventListener('close', function() {
searchInput.value = '';
function showPalette() {
container.style.display = 'block';
input.value = searchInput ? searchInput.value : '';
input.focus();
cmdkItems.forEach(function(item) {
item.style.display = '';
});
}
function hidePalette() {
container.style.display = 'none';
if (searchInput) searchInput.blur();
}
if (searchInput) {
searchInput.addEventListener('focus', showPalette);
searchInput.addEventListener('input', function(e) {
if (container.style.display === 'none') {
showPalette();
}
var query = e.target.value.toLowerCase();
cmdkItems.forEach(function(item) {
var value = item.dataset.value || item.textContent.toLowerCase();
item.style.display = (query === '' || value.includes(query)) ? '' : 'none';
});
});
}
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
showPalette();
}
if (e.key === 'Escape' && container.style.display === 'block') {
hidePalette();
}
});
if (backdrop) {
backdrop.addEventListener('click', hidePalette);
}
})();
</script>
<style>
.cmdk-dialog {
padding: 0;
border: none;
background: transparent;
max-width: 700px;
#command-palette-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
border-radius: 12px;
z-index: 1056;
height: 100%;
z-index: 9999;
}
.cmdk-dialog::backdrop {
.command-palette-backdrop {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.command-palette-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 700px;
padding: 0 20px;
}
.cmdk {
background: var(--bs-body-bg, #fff);
border-radius: 12px;
@@ -166,8 +168,7 @@ $validMenuItems = collect($menuItems)->filter(fn($item) => Route::has($item['rou
transition: background 0.15s;
}
.cmdk-item:hover,
.cmdk-item[data-selected="true"] {
.cmdk-item:hover {
background: var(--bs-tertiary-bg, #f1f5f9);
}
@@ -195,8 +196,7 @@ $validMenuItems = collect($menuItems)->filter(fn($item) => Route::has($item['rou
color: #f8fafc;
}
[data-bs-theme="dark"] .cmdk-item:hover,
[data-bs-theme="dark"] .cmdk-item[data-selected="true"] {
[data-bs-theme="dark"] .cmdk-item:hover {
background: #334155;
}
</style>