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">
|
<div class="flex mt-2">
|
||||||
<button class="btn btn-primary" id="saveBtn" onclick="saveRecipe()">Mentés</button>
|
<button class="btn btn-primary" id="saveBtn" onclick="saveRecipe()">Mentés</button>
|
||||||
<a href="/recipes" class="btn btn-secondary">Mégsem</a>
|
<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>
|
<span id="saveStatus"></span>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% 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 ===== */
|
/* ===== Toast ===== */
|
||||||
function showToast(msg, type) {
|
function showToast(msg, type) {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
|
|||||||
+97
-16
@@ -62,11 +62,40 @@
|
|||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.9rem;
|
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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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 {
|
.tag-filter-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -365,6 +394,8 @@ let pendingDeleteIds = []; // set by confirmDelete / confirmBulkDelete
|
|||||||
let backendTags = []; // [{name, id}, ...]
|
let backendTags = []; // [{name, id}, ...]
|
||||||
let activeFilterTagIds = [];
|
let activeFilterTagIds = [];
|
||||||
let activeFilterTagNames = {}; // id -> name, for chip display
|
let activeFilterTagNames = {}; // id -> name, for chip display
|
||||||
|
let searchSuggestions = []; // [{id, name}, ...] from last suggestion fetch
|
||||||
|
let searchHighlightIdx = -1; // -1 = none highlighted
|
||||||
|
|
||||||
/* ===== Escaping ===== */
|
/* ===== Escaping ===== */
|
||||||
function escHtml(s) {
|
function escHtml(s) {
|
||||||
@@ -393,23 +424,52 @@ function switchBackend(b) {
|
|||||||
/* ===== Search ===== */
|
/* ===== Search ===== */
|
||||||
function onSearchInput() {
|
function onSearchInput() {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
|
searchHighlightIdx = -1;
|
||||||
searchTimeout = setTimeout(() => {
|
searchTimeout = setTimeout(() => {
|
||||||
loadSearchSuggestions();
|
loadSearchSuggestions();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSearchKeydown(e) {
|
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();
|
e.preventDefault();
|
||||||
document.getElementById('searchDropdown').classList.remove('open');
|
searchHighlightIdx = Math.min(searchHighlightIdx + 1, searchSuggestions.length - 1);
|
||||||
executeSearch();
|
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') {
|
} 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() {
|
function executeSearch() {
|
||||||
document.getElementById('searchDropdown').classList.remove('open');
|
document.getElementById('searchDropdown').classList.remove('open');
|
||||||
|
searchHighlightIdx = -1;
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
selectedIds.clear();
|
selectedIds.clear();
|
||||||
loadRecipes();
|
loadRecipes();
|
||||||
@@ -418,7 +478,7 @@ function executeSearch() {
|
|||||||
async function loadSearchSuggestions() {
|
async function loadSearchSuggestions() {
|
||||||
const q = document.getElementById('recipeSearch').value.trim();
|
const q = document.getElementById('recipeSearch').value.trim();
|
||||||
const dropdown = document.getElementById('searchDropdown');
|
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 });
|
const params = new URLSearchParams({ page: 1, per_page: 8, search: q });
|
||||||
if (activeFilterTagIds.length) params.set('tag_ids', activeFilterTagIds.join(','));
|
if (activeFilterTagIds.length) params.set('tag_ids', activeFilterTagIds.join(','));
|
||||||
@@ -428,24 +488,45 @@ async function loadSearchSuggestions() {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!data.ok || data.items.length === 0) {
|
if (!data.ok || data.items.length === 0) {
|
||||||
dropdown.classList.remove('open');
|
dropdown.classList.remove('open');
|
||||||
|
searchSuggestions = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let html = '';
|
searchSuggestions = data.items;
|
||||||
for (const r of data.items) {
|
searchHighlightIdx = -1;
|
||||||
html += '<div class="search-dropdown-item" onclick="selectSearchItem(\'' +
|
renderSearchDropdown();
|
||||||
escHtml(r.name).replace(/'/g, "\\'") + '\')">' + escHtml(r.name) + '</div>';
|
|
||||||
}
|
|
||||||
dropdown.innerHTML = html;
|
|
||||||
dropdown.classList.add('open');
|
dropdown.classList.add('open');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dropdown.classList.remove('open');
|
dropdown.classList.remove('open');
|
||||||
|
searchSuggestions = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectSearchItem(name) {
|
function renderSearchDropdown() {
|
||||||
document.getElementById('recipeSearch').value = name;
|
const dropdown = document.getElementById('searchDropdown');
|
||||||
document.getElementById('searchDropdown').classList.remove('open');
|
let html = '';
|
||||||
executeSearch();
|
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');
|
||||||
|
window.location.href = '/recipes/' + currentBackend + '/' + encodeURIComponent(item.id) + '/edit';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Tag filter ===== */
|
/* ===== Tag filter ===== */
|
||||||
|
|||||||
Reference in New Issue
Block a user