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:
@@ -274,9 +274,22 @@
|
||||
<div class="flex mt-2">
|
||||
<button class="btn btn-primary" id="saveBtn" onclick="saveRecipe()">Mentés</button>
|
||||
<a href="/recipes" class="btn btn-secondary">Mégsem</a>
|
||||
<button class="btn btn-danger" onclick="confirmDelete()" style="margin-left:auto;">Recept törlése</button>
|
||||
<span id="saveStatus"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
<div id="deleteModal" style="position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:1000;display:none;align-items:center;justify-content:center;">
|
||||
<div class="card" style="max-width:450px;width:90%;">
|
||||
<h2 style="color:#f85149;margin-bottom:1rem;">Recept törlése</h2>
|
||||
<p id="deleteModalText">Biztosan törölni szeretnéd ezt a receptet? Ez a művelet nem vonható vissza!</p>
|
||||
<div class="flex" style="justify-content:flex-end;gap:0.5rem;margin-top:1rem;">
|
||||
<button class="btn btn-secondary" onclick="closeDeleteModal()">Mégsem</button>
|
||||
<button class="btn btn-danger" onclick="executeDelete()" id="confirmDeleteBtn">Törlés</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
@@ -559,6 +572,46 @@ async function saveRecipe() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Delete ===== */
|
||||
function confirmDelete() {
|
||||
const title = document.getElementById('recipeTitle').value.trim() || 'ezt a receptet';
|
||||
document.getElementById('deleteModalText').textContent =
|
||||
'Biztosan törölni szeretnéd a következő receptet: „' + title + '"? Ez a művelet nem vonható vissza!';
|
||||
document.getElementById('deleteModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
document.getElementById('deleteModal').style.display = 'none';
|
||||
}
|
||||
|
||||
async function executeDelete() {
|
||||
const btn = document.getElementById('confirmDeleteBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> Törlés...';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/recipes/' + BACKEND + '/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids: [RECIPE_ID] }),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.ok) {
|
||||
window.location.href = '/recipes';
|
||||
} else {
|
||||
closeDeleteModal();
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Törlés';
|
||||
showToast(data.error || 'Hiba történt.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
closeDeleteModal();
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Törlés';
|
||||
showToast('Hiba: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Toast ===== */
|
||||
function showToast(msg, type) {
|
||||
const el = document.createElement('div');
|
||||
|
||||
+95
-14
@@ -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');
|
||||
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;
|
||||
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 + ')">✎</button>' +
|
||||
'<button class="sdi-delete" title="Törlés" onclick="event.stopPropagation();confirmSingleDelete(\'' +
|
||||
escHtml(String(r.id)).replace(/'/g, "\\'") + '\', \'' +
|
||||
escHtml(r.name).replace(/'/g, "\\'") + '\')">🗑</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');
|
||||
executeSearch();
|
||||
window.location.href = '/recipes/' + currentBackend + '/' + encodeURIComponent(item.id) + '/edit';
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Tag filter ===== */
|
||||
|
||||
Reference in New Issue
Block a user