feat(modal): fandom picker wiring
Reveals the picker on 'character:' prefix; autocomplete via /api/tags/search?kind=fandom; submits fandom_id (preferred) or fandom_name alongside the character name. Clears on image switch and modal close. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+100
-3
@@ -14,6 +14,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const tagInput = tagForm ? tagForm.querySelector('input[name="name"]') : null;
|
||||
const tagAutocomplete = document.getElementById('tagAutocomplete');
|
||||
const tagActionFeedback = document.getElementById('tagActionFeedback');
|
||||
const fandomPickerWrapper = document.getElementById('fandomPickerWrapper');
|
||||
const fandomPickerInput = document.getElementById('fandomPickerInput');
|
||||
const fandomPickerAutocomplete = document.getElementById('fandomPickerAutocomplete');
|
||||
const fandomPickerClear = document.getElementById('fandomPickerClear');
|
||||
let fandomPickerDebounce = null;
|
||||
const provenanceSection = document.getElementById('modalProvenanceSection');
|
||||
const provenanceList = document.getElementById('modalProvenanceList');
|
||||
const suggestionsSection = document.getElementById('modalSuggestionsSection');
|
||||
@@ -590,9 +595,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
async function addTag(imageId, name) {
|
||||
async function addTag(imageId, name, { fandomId, fandomName } = {}) {
|
||||
const fd = new FormData();
|
||||
fd.append('name', name);
|
||||
if (fandomId) {
|
||||
fd.append('fandom_id', String(fandomId));
|
||||
} else if (fandomName) {
|
||||
fd.append('fandom_name', fandomName);
|
||||
}
|
||||
const r = await fetch(`/image/${imageId}/tags/add`, { method: 'POST', body: fd });
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
@@ -615,6 +625,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Fandom picker helpers
|
||||
// ---------------------------
|
||||
function clearFandomAutocomplete() {
|
||||
if (fandomPickerAutocomplete) fandomPickerAutocomplete.innerHTML = '';
|
||||
}
|
||||
|
||||
function clearFandomPicker() {
|
||||
if (!fandomPickerInput) return;
|
||||
fandomPickerInput.value = '';
|
||||
delete fandomPickerInput.dataset.fandomId;
|
||||
clearFandomAutocomplete();
|
||||
fandomPickerWrapper?.classList.remove('visible');
|
||||
fandomPickerWrapper?.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
function renderFandomAutocomplete(rows) {
|
||||
if (!fandomPickerAutocomplete) return;
|
||||
if (!rows || rows.length === 0) {
|
||||
fandomPickerAutocomplete.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
fandomPickerAutocomplete.innerHTML = rows.map(r => {
|
||||
const name = String(r.name || '').replace(/"/g, '"');
|
||||
const displayName = String(r.display_name || r.name || '').replace(/</g, '<');
|
||||
return `<div class="tag-autocomplete-item" data-fandom-id="${r.id}" data-fandom-name="${name}">${displayName}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Autocomplete helpers
|
||||
// ---------------------------
|
||||
@@ -681,8 +720,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const id = getEditorImageId();
|
||||
const name = (tagInput?.value || '').trim();
|
||||
if (!id || !name) return;
|
||||
await addTag(id, name);
|
||||
const fandomId = fandomPickerInput?.dataset?.fandomId;
|
||||
const fandomName = (fandomPickerInput?.value || '').trim();
|
||||
const ok = await addTag(id, name, {
|
||||
fandomId: fandomId ? Number(fandomId) : null,
|
||||
fandomName: fandomName || null,
|
||||
});
|
||||
if (tagInput) tagInput.value = '';
|
||||
clearFandomPicker();
|
||||
// Reset the reserved area to the top-tags view so it's ready for the next add.
|
||||
fetchAutocomplete('');
|
||||
}
|
||||
@@ -693,8 +738,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchAutocomplete(tagInput.value.trim());
|
||||
});
|
||||
|
||||
// Search as user types.
|
||||
// Search as user types + toggle the fandom picker when the shortcut is 'character:'.
|
||||
tagInput.addEventListener('input', () => {
|
||||
const lower = (tagInput.value || '').toLowerCase();
|
||||
const isCharacter = lower.startsWith('character:');
|
||||
if (fandomPickerWrapper) {
|
||||
fandomPickerWrapper.classList.toggle('visible', isCharacter);
|
||||
fandomPickerWrapper.setAttribute('aria-hidden', String(!isCharacter));
|
||||
}
|
||||
if (!isCharacter) {
|
||||
// Typing anything else cancels a previously-staged fandom id/name.
|
||||
if (fandomPickerInput) {
|
||||
fandomPickerInput.value = '';
|
||||
delete fandomPickerInput.dataset.fandomId;
|
||||
clearFandomAutocomplete();
|
||||
}
|
||||
}
|
||||
clearTimeout(autocompleteDebounce);
|
||||
autocompleteDebounce = setTimeout(() => {
|
||||
fetchAutocomplete(tagInput.value.trim());
|
||||
@@ -742,6 +801,42 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (fandomPickerInput) {
|
||||
fandomPickerInput.addEventListener('input', () => {
|
||||
clearTimeout(fandomPickerDebounce);
|
||||
const term = fandomPickerInput.value.trim();
|
||||
// Any typing invalidates a previously-selected fandom id.
|
||||
delete fandomPickerInput.dataset.fandomId;
|
||||
if (!term) {
|
||||
clearFandomAutocomplete();
|
||||
return;
|
||||
}
|
||||
fandomPickerDebounce = setTimeout(() => {
|
||||
fetch(`/api/tags/search?kind=fandom&q=${encodeURIComponent(term)}&limit=8`)
|
||||
.then(r => r.json())
|
||||
.then(body => renderFandomAutocomplete(body.tags || []))
|
||||
.catch(() => clearFandomAutocomplete());
|
||||
}, 150);
|
||||
});
|
||||
}
|
||||
|
||||
if (fandomPickerAutocomplete) {
|
||||
fandomPickerAutocomplete.addEventListener('mousedown', (e) => {
|
||||
const row = e.target.closest('[data-fandom-id]');
|
||||
if (!row) return;
|
||||
e.preventDefault(); // keep focus on the input
|
||||
if (fandomPickerInput) {
|
||||
fandomPickerInput.value = row.dataset.fandomName;
|
||||
fandomPickerInput.dataset.fandomId = row.dataset.fandomId;
|
||||
}
|
||||
clearFandomAutocomplete();
|
||||
});
|
||||
}
|
||||
|
||||
if (fandomPickerClear) {
|
||||
fandomPickerClear.addEventListener('click', clearFandomPicker);
|
||||
}
|
||||
|
||||
if (tagForm) {
|
||||
tagForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -793,6 +888,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// NEW: load tags for this image into the modal editor
|
||||
setEditorImageId(imageId);
|
||||
if (tagInput) tagInput.value = '';
|
||||
clearFandomPicker();
|
||||
loadTags(imageId);
|
||||
loadSeriesInfo(imageId);
|
||||
// Prefill the reserved autocomplete area with top tags so it's never empty.
|
||||
@@ -812,6 +908,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
clearFandomPicker();
|
||||
// Get the image ID before clearing so we can refresh its tags
|
||||
const imageId = getEditorImageId();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user