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
+145 -12
View File
@@ -119,22 +119,29 @@ $dtColumns = collect($columns)
<h5 class="offcanvas-title">Column Visibility</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body">
@foreach($columns as $index => $col)
@if((!isset($col['visibility']) || $col['visibility'] !== false) && $col['key'] !== 'checkbox')
<div class="form-check form-switch mb-2">
<input type="checkbox" class="form-check-input column-toggle"
data-column-index="{{ $index }}"
id="{{ $id }}-col-{{ $index }}"
{{ !isset($col['visible']) || $col['visible'] !== false ? 'checked' : '' }}>
<label class="form-check-label" for="{{ $id }}-col-{{ $index }}">{{ $col['label'] }}</label>
</div>
@endif
@endforeach
<div class="offcanvas-body p-3">
<div id="{{ $id }}-column-list">
@foreach($columns as $index => $col)
@if((!isset($col['visibility']) || $col['visibility'] !== false) && $col['key'] !== 'checkbox')
<div class="d-flex align-items-center gap-2 mb-2 p-2 rounded column-item bg-white border" data-column-key="{{ $col['key'] }}" data-column-index="{{ $index }}" style="cursor: grab; transition: all 0.15s ease;">
<i data-lucide="grip-vertical" class="text-muted" style="width: 16px; height: 16px; flex-shrink: 0;"></i>
<div class="form-check form-switch mb-0 flex-grow-1">
<input type="checkbox" class="form-check-input column-toggle"
data-column-key="{{ $col['key'] }}"
data-column-index="{{ $index }}"
id="{{ $id }}-col-{{ $index }}"
{{ !isset($col['visible']) || $col['visible'] !== false ? 'checked' : '' }}>
<label class="form-check-label user-select-none" for="{{ $id }}-col-{{ $index }}">{{ $col['label'] }}</label>
</div>
</div>
@endif
@endforeach
</div>
</div>
</div>
@push('custom-scripts')
<script src="{{ asset('build/plugins/sortablejs/Sortable.min.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
$.fn.DataTable.ext.pager.numbers_length = 7;
@@ -235,15 +242,118 @@ $dtColumns = collect($columns)
lucide.createIcons();
}
}, 50);
var storedOrder = getStoredOrder();
if (storedOrder && storedOrder.length > 0) {
var $table = $('#{{ $id }}');
var $tbody = $table.find('tbody');
var $rows = $tbody.find('tr');
if ($rows.length > 0) {
$rows.each(function() {
var cells = $(this).find('td').toArray();
var newCells = storedOrder.map(function(idx) { return cells[idx]; });
$(this).find('td').remove();
newCells.forEach(function(td) { $(this).append(td); }, $(this));
});
}
}
}
});
var sortableInstance = null;
var tableId = '{{ $id }}';
var storageKey = 'dt_column_order_' + tableId;
function getStoredOrder() {
var stored = localStorage.getItem(storageKey);
return stored ? JSON.parse(stored) : null;
}
function saveOrderToStorage(newOrder) {
localStorage.setItem(storageKey, JSON.stringify(newOrder));
}
function reorderTableColumns(newOrder) {
saveOrderToStorage(newOrder);
var $table = $('#{{ $id }}');
var $theadRows = $table.find('thead tr');
$theadRows.each(function() {
var headers = $(this).find('th').toArray();
var newHeaders = newOrder.map(function(idx) { return headers[idx]; });
$(this).find('th').remove();
newHeaders.forEach(function(th) { $(this).append(th); }, $(this));
});
var $tbody = $table.find('tbody');
var $rows = $tbody.find('tr');
if ($rows.length > 0) {
$rows.each(function() {
var cells = $(this).find('td').toArray();
var newCells = newOrder.map(function(idx) { return cells[idx]; });
$(this).find('td').remove();
newCells.forEach(function(td) { $(this).append(td); }, $(this));
});
}
reorderOffcanvasList(newOrder);
}
function reorderOffcanvasList(newOrder) {
var $list = $('#{{ $id }}-column-list');
var items = $list.find('.column-item').toArray();
var newItems = newOrder.map(function(idx) { return items[idx]; });
$list.find('.column-item').remove();
newItems.forEach(function(item) { $list.append(item); });
}
function applyStoredOrder() {
var storedOrder = getStoredOrder();
if (storedOrder && storedOrder.length > 0) {
reorderTableColumns(storedOrder);
}
}
applyStoredOrder();
var sortableInstance = null;
function initSortable() {
var sortableList = document.getElementById('{{ $id }}-column-list');
if (!sortableList) return;
sortableInstance = new Sortable(sortableList, {
animation: 150,
ghostClass: 'bg-light',
delay: 100,
onEnd: function (evt) {
var items = sortableList.querySelectorAll('.column-item');
var newOrder = [];
items.forEach(function(item) {
newOrder.push(parseInt(item.dataset.columnIndex));
});
reorderTableColumns(newOrder);
}
});
}
$('#{{ $id }}-column-offcanvas').on('change', '.column-toggle', function () {
var columnKey = $(this).data('column-key');
var columnIdx = $(this).data('column-index');
var column = table.column(columnIdx);
column.visible($(this).is(':checked'));
});
var offcanvasEl = document.getElementById('{{ $id }}-column-offcanvas');
if (offcanvasEl) {
offcanvasEl.addEventListener('shown.bs.offcanvas', function() {
initSortable();
if (window.lucide) lucide.createIcons();
});
}
$('#{{ $id }} tbody').on('dblclick', 'tr', function () {
const editLink = $(this).find('a.btn-outline-primary, a[href*="edit"]').first();
if (editLink.length && editLink.attr('href')) {
@@ -554,5 +664,28 @@ $dtColumns = collect($columns)
gap: 0.5rem;
margin: 0;
}
.column-item {
cursor: grab !important;
}
.column-item:hover {
background-color: #f8f9fa !important;
border-color: #dee2e6 !important;
}
.column-item:active {
background-color: #e9ecef !important;
cursor: grabbing !important;
}
.column-item.sortable-ghost {
background-color: #e3f2fd !important;
opacity: 0.8;
}
.column-item.sortable-chosen {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
</style>
@endpush