major updates to theming, creation of showcase view and polish of existing systems including tagging editting.
This commit is contained in:
@@ -12,6 +12,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const tagList = document.getElementById('modalTagList');
|
||||
const tagForm = document.getElementById('modalTagForm');
|
||||
const tagInput = tagForm ? tagForm.querySelector('input[name="name"]') : null;
|
||||
const tagAutocomplete = document.getElementById('tagAutocomplete');
|
||||
|
||||
// Autocomplete state
|
||||
let autocompleteItems = [];
|
||||
let autocompleteSelectedIndex = -1;
|
||||
let autocompleteDebounce = null;
|
||||
|
||||
let images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||
let currentIndex = -1;
|
||||
@@ -36,18 +42,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function getEditorImageId() {
|
||||
return tagEditor ? tagEditor.dataset.imageId : '';
|
||||
}
|
||||
function getTagIcon(kind) {
|
||||
const icons = {
|
||||
artist: '🎨',
|
||||
archive: '🗜️',
|
||||
character: '👤',
|
||||
series: '📺',
|
||||
rating: '⚠️'
|
||||
};
|
||||
return icons[kind] || '#';
|
||||
}
|
||||
|
||||
function getTagDisplayName(name) {
|
||||
// Remove prefix if present (e.g., "artist:Name" -> "Name")
|
||||
return name.includes(':') ? name.split(':', 2)[1] : name;
|
||||
}
|
||||
|
||||
function renderTags(tags) {
|
||||
if (!tagList) return;
|
||||
tagList.innerHTML = '';
|
||||
(tags || []).forEach(t => {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'tag-chip';
|
||||
const label = (t.kind === 'artist')
|
||||
? `🎨 ${t.name.split(':', 1)[0] === 'artist' ? t.name.split(':', 2)[1] : t.name}`
|
||||
: (t.kind === 'archive')
|
||||
? `🗜️ ${t.name.split(':', 1)[0] === 'archive' ? t.name.split(':', 2)[1] : t.name}`
|
||||
: `#${t.name}`;
|
||||
chip.innerHTML = `${label} <button class="x" data-name="${t.name}" title="Remove">×</button>`;
|
||||
const icon = getTagIcon(t.kind);
|
||||
const displayName = getTagDisplayName(t.name);
|
||||
chip.innerHTML = `${icon} ${displayName} <button class="x" data-name="${t.name}" title="Remove">×</button>`;
|
||||
tagList.appendChild(chip);
|
||||
});
|
||||
}
|
||||
@@ -84,12 +103,129 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Autocomplete helpers
|
||||
// ---------------------------
|
||||
function hideAutocomplete() {
|
||||
if (tagAutocomplete) {
|
||||
tagAutocomplete.classList.remove('active');
|
||||
tagAutocomplete.innerHTML = '';
|
||||
}
|
||||
autocompleteItems = [];
|
||||
autocompleteSelectedIndex = -1;
|
||||
}
|
||||
|
||||
function renderAutocomplete(tags) {
|
||||
if (!tagAutocomplete) return;
|
||||
autocompleteItems = tags;
|
||||
autocompleteSelectedIndex = -1;
|
||||
|
||||
if (tags.length === 0) {
|
||||
tagAutocomplete.innerHTML = '<div class="tag-autocomplete-empty">No matching tags</div>';
|
||||
tagAutocomplete.classList.add('active');
|
||||
return;
|
||||
}
|
||||
|
||||
tagAutocomplete.innerHTML = tags.map((t, i) => {
|
||||
const icon = getTagIcon(t.kind);
|
||||
const displayName = getTagDisplayName(t.name);
|
||||
return `<div class="tag-autocomplete-item" data-index="${i}" data-name="${t.name}">
|
||||
<span>${icon} ${displayName}</span>
|
||||
<span class="tag-kind">${t.kind || 'user'}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
tagAutocomplete.classList.add('active');
|
||||
}
|
||||
|
||||
function selectAutocompleteItem(index) {
|
||||
const items = tagAutocomplete?.querySelectorAll('.tag-autocomplete-item');
|
||||
if (!items) return;
|
||||
items.forEach((el, i) => {
|
||||
el.classList.toggle('selected', i === index);
|
||||
});
|
||||
autocompleteSelectedIndex = index;
|
||||
}
|
||||
|
||||
async function fetchAutocomplete(query) {
|
||||
try {
|
||||
const url = query
|
||||
? `/api/tags/search?q=${encodeURIComponent(query)}&limit=8`
|
||||
: `/api/tags/search?limit=8`;
|
||||
const r = await fetch(url);
|
||||
const j = await r.json();
|
||||
renderAutocomplete(j.tags || []);
|
||||
} catch {
|
||||
hideAutocomplete();
|
||||
}
|
||||
}
|
||||
|
||||
if (tagInput) {
|
||||
// Show autocomplete on focus
|
||||
tagInput.addEventListener('focus', () => {
|
||||
const val = tagInput.value.trim();
|
||||
fetchAutocomplete(val);
|
||||
});
|
||||
|
||||
// Hide autocomplete on blur (with delay for click)
|
||||
tagInput.addEventListener('blur', () => {
|
||||
setTimeout(hideAutocomplete, 200);
|
||||
});
|
||||
|
||||
// Search as user types
|
||||
tagInput.addEventListener('input', () => {
|
||||
clearTimeout(autocompleteDebounce);
|
||||
autocompleteDebounce = setTimeout(() => {
|
||||
fetchAutocomplete(tagInput.value.trim());
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
tagInput.addEventListener('keydown', (e) => {
|
||||
if (!tagAutocomplete?.classList.contains('active')) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
const nextIndex = Math.min(autocompleteSelectedIndex + 1, autocompleteItems.length - 1);
|
||||
selectAutocompleteItem(nextIndex);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const prevIndex = Math.max(autocompleteSelectedIndex - 1, 0);
|
||||
selectAutocompleteItem(prevIndex);
|
||||
} else if (e.key === 'Enter' && autocompleteSelectedIndex >= 0) {
|
||||
e.preventDefault();
|
||||
const selected = autocompleteItems[autocompleteSelectedIndex];
|
||||
if (selected) {
|
||||
tagInput.value = selected.name;
|
||||
hideAutocomplete();
|
||||
tagForm.dispatchEvent(new Event('submit'));
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Click on autocomplete item
|
||||
if (tagAutocomplete) {
|
||||
tagAutocomplete.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.tag-autocomplete-item');
|
||||
if (!item) return;
|
||||
const name = item.dataset.name;
|
||||
if (tagInput && name) {
|
||||
tagInput.value = name;
|
||||
hideAutocomplete();
|
||||
tagForm.dispatchEvent(new Event('submit'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (tagForm) {
|
||||
tagForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const id = getEditorImageId();
|
||||
const name = (tagInput.value || '').trim();
|
||||
if (!id || !name) return;
|
||||
hideAutocomplete();
|
||||
await addTag(id, name);
|
||||
tagInput.value = '';
|
||||
});
|
||||
@@ -306,8 +442,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
|
||||
images.forEach((img, index) => {
|
||||
img.addEventListener('click', () => openModal(index));
|
||||
// Use event delegation on the document for .img-clickable clicks
|
||||
// This allows dynamically added elements (e.g., from shuffle) to work
|
||||
document.addEventListener('click', (e) => {
|
||||
const clickable = e.target.closest('.img-clickable');
|
||||
if (!clickable) return;
|
||||
|
||||
// Re-query images to get current state (may have changed from shuffle)
|
||||
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||
const index = images.indexOf(clickable);
|
||||
if (index !== -1) {
|
||||
openModal(index);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for showcase updates to refresh the images array
|
||||
document.addEventListener('showcaseUpdated', () => {
|
||||
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
|
||||
Reference in New Issue
Block a user