Search dropdown: keyboard navigation, edit/delete actions; edit page delete

- Search dropdown now supports arrow up/down to highlight items,
  Enter navigates to edit page of highlighted recipe
- Each dropdown item shows edit (pencil) and delete (trash) buttons
- Edit page now has a "Recept törlése" button with confirmation modal
- Successful delete on edit page redirects back to /recipes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 08:55:43 +01:00
parent bb89d97716
commit 6aa7ed3c71
2 changed files with 150 additions and 16 deletions
+97 -16
View File
@@ -62,11 +62,40 @@
padding: 0.5rem 0.75rem;
cursor: pointer;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.search-dropdown-item:hover, .search-dropdown-item.highlighted { background: var(--accent-glow); }
.search-dropdown-item .sdi-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-dropdown-item:hover { background: var(--accent-glow); }
.search-dropdown-item .sdi-actions {
display: flex;
gap: 0.3rem;
flex-shrink: 0;
}
.search-dropdown-item .sdi-actions button {
background: none;
border: none;
cursor: pointer;
font-size: 0.85rem;
padding: 0.15rem 0.35rem;
border-radius: 4px;
color: var(--text-dim);
transition: background 0.1s, color 0.1s;
}
.search-dropdown-item .sdi-actions button:hover {
background: var(--surface);
color: var(--text);
}
.search-dropdown-item .sdi-actions .sdi-delete:hover {
background: rgba(218,54,51,0.2);
color: #f85149;
}
.tag-filter-wrap {
position: relative;
}
@@ -365,6 +394,8 @@ let pendingDeleteIds = []; // set by confirmDelete / confirmBulkDelete
let backendTags = []; // [{name, id}, ...]
let activeFilterTagIds = [];
let activeFilterTagNames = {}; // id -> name, for chip display
let searchSuggestions = []; // [{id, name}, ...] from last suggestion fetch
let searchHighlightIdx = -1; // -1 = none highlighted
/* ===== Escaping ===== */
function escHtml(s) {
@@ -393,23 +424,52 @@ function switchBackend(b) {
/* ===== Search ===== */
function onSearchInput() {
clearTimeout(searchTimeout);
searchHighlightIdx = -1;
searchTimeout = setTimeout(() => {
loadSearchSuggestions();
}, 300);
}
function onSearchKeydown(e) {
if (e.key === 'Enter') {
const dropdown = document.getElementById('searchDropdown');
const isOpen = dropdown.classList.contains('open');
if (e.key === 'ArrowDown' && isOpen) {
e.preventDefault();
document.getElementById('searchDropdown').classList.remove('open');
executeSearch();
searchHighlightIdx = Math.min(searchHighlightIdx + 1, searchSuggestions.length - 1);
updateSearchHighlight();
} else if (e.key === 'ArrowUp' && isOpen) {
e.preventDefault();
searchHighlightIdx = Math.max(searchHighlightIdx - 1, -1);
updateSearchHighlight();
} else if (e.key === 'Enter') {
e.preventDefault();
if (isOpen && searchHighlightIdx >= 0 && searchHighlightIdx < searchSuggestions.length) {
// Navigate to edit page of highlighted item
const item = searchSuggestions[searchHighlightIdx];
dropdown.classList.remove('open');
window.location.href = '/recipes/' + currentBackend + '/' + encodeURIComponent(item.id) + '/edit';
} else {
dropdown.classList.remove('open');
executeSearch();
}
} else if (e.key === 'Escape') {
document.getElementById('searchDropdown').classList.remove('open');
dropdown.classList.remove('open');
searchHighlightIdx = -1;
}
}
function updateSearchHighlight() {
const items = document.querySelectorAll('#searchDropdown .search-dropdown-item');
items.forEach((el, i) => el.classList.toggle('highlighted', i === searchHighlightIdx));
if (searchHighlightIdx >= 0 && items[searchHighlightIdx]) {
items[searchHighlightIdx].scrollIntoView({ block: 'nearest' });
}
}
function executeSearch() {
document.getElementById('searchDropdown').classList.remove('open');
searchHighlightIdx = -1;
currentPage = 1;
selectedIds.clear();
loadRecipes();
@@ -418,7 +478,7 @@ function executeSearch() {
async function loadSearchSuggestions() {
const q = document.getElementById('recipeSearch').value.trim();
const dropdown = document.getElementById('searchDropdown');
if (!q) { dropdown.classList.remove('open'); return; }
if (!q) { dropdown.classList.remove('open'); searchSuggestions = []; return; }
const params = new URLSearchParams({ page: 1, per_page: 8, search: q });
if (activeFilterTagIds.length) params.set('tag_ids', activeFilterTagIds.join(','));
@@ -428,24 +488,45 @@ async function loadSearchSuggestions() {
const data = await resp.json();
if (!data.ok || data.items.length === 0) {
dropdown.classList.remove('open');
searchSuggestions = [];
return;
}
let html = '';
for (const r of data.items) {
html += '<div class="search-dropdown-item" onclick="selectSearchItem(\'' +
escHtml(r.name).replace(/'/g, "\\'") + '\')">' + escHtml(r.name) + '</div>';
}
dropdown.innerHTML = html;
searchSuggestions = data.items;
searchHighlightIdx = -1;
renderSearchDropdown();
dropdown.classList.add('open');
} catch (e) {
dropdown.classList.remove('open');
searchSuggestions = [];
}
}
function selectSearchItem(name) {
document.getElementById('recipeSearch').value = name;
document.getElementById('searchDropdown').classList.remove('open');
executeSearch();
function renderSearchDropdown() {
const dropdown = document.getElementById('searchDropdown');
let html = '';
searchSuggestions.forEach((r, i) => {
const editUrl = '/recipes/' + currentBackend + '/' + encodeURIComponent(r.id) + '/edit';
const cls = i === searchHighlightIdx ? ' highlighted' : '';
html += '<div class="search-dropdown-item' + cls + '" data-idx="' + i + '" ' +
'onmouseenter="searchHighlightIdx=' + i + ';updateSearchHighlight()" ' +
'onclick="navigateToEdit(' + i + ')">' +
'<span class="sdi-name">' + escHtml(r.name) + '</span>' +
'<span class="sdi-actions">' +
'<button title="Szerkesztés" onclick="event.stopPropagation();navigateToEdit(' + i + ')">&#9998;</button>' +
'<button class="sdi-delete" title="Törlés" onclick="event.stopPropagation();confirmSingleDelete(\'' +
escHtml(String(r.id)).replace(/'/g, "\\'") + '\', \'' +
escHtml(r.name).replace(/'/g, "\\'") + '\')">&#128465;</button>' +
'</span></div>';
});
dropdown.innerHTML = html;
}
function navigateToEdit(idx) {
if (idx >= 0 && idx < searchSuggestions.length) {
const item = searchSuggestions[idx];
document.getElementById('searchDropdown').classList.remove('open');
window.location.href = '/recipes/' + currentBackend + '/' + encodeURIComponent(item.id) + '/edit';
}
}
/* ===== Tag filter ===== */