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 tagInput = tagForm ? tagForm.querySelector('input[name="name"]') : null;
|
||||||
const tagAutocomplete = document.getElementById('tagAutocomplete');
|
const tagAutocomplete = document.getElementById('tagAutocomplete');
|
||||||
const tagActionFeedback = document.getElementById('tagActionFeedback');
|
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 provenanceSection = document.getElementById('modalProvenanceSection');
|
||||||
const provenanceList = document.getElementById('modalProvenanceList');
|
const provenanceList = document.getElementById('modalProvenanceList');
|
||||||
const suggestionsSection = document.getElementById('modalSuggestionsSection');
|
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();
|
const fd = new FormData();
|
||||||
fd.append('name', name);
|
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 r = await fetch(`/image/${imageId}/tags/add`, { method: 'POST', body: fd });
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
if (j.ok) {
|
if (j.ok) {
|
||||||
@@ -615,6 +625,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return false;
|
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
|
// Autocomplete helpers
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -681,8 +720,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const id = getEditorImageId();
|
const id = getEditorImageId();
|
||||||
const name = (tagInput?.value || '').trim();
|
const name = (tagInput?.value || '').trim();
|
||||||
if (!id || !name) return;
|
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 = '';
|
if (tagInput) tagInput.value = '';
|
||||||
|
clearFandomPicker();
|
||||||
// Reset the reserved area to the top-tags view so it's ready for the next add.
|
// Reset the reserved area to the top-tags view so it's ready for the next add.
|
||||||
fetchAutocomplete('');
|
fetchAutocomplete('');
|
||||||
}
|
}
|
||||||
@@ -693,8 +738,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fetchAutocomplete(tagInput.value.trim());
|
fetchAutocomplete(tagInput.value.trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search as user types.
|
// Search as user types + toggle the fandom picker when the shortcut is 'character:'.
|
||||||
tagInput.addEventListener('input', () => {
|
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);
|
clearTimeout(autocompleteDebounce);
|
||||||
autocompleteDebounce = setTimeout(() => {
|
autocompleteDebounce = setTimeout(() => {
|
||||||
fetchAutocomplete(tagInput.value.trim());
|
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) {
|
if (tagForm) {
|
||||||
tagForm.addEventListener('submit', async (e) => {
|
tagForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -793,6 +888,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// NEW: load tags for this image into the modal editor
|
// NEW: load tags for this image into the modal editor
|
||||||
setEditorImageId(imageId);
|
setEditorImageId(imageId);
|
||||||
if (tagInput) tagInput.value = '';
|
if (tagInput) tagInput.value = '';
|
||||||
|
clearFandomPicker();
|
||||||
loadTags(imageId);
|
loadTags(imageId);
|
||||||
loadSeriesInfo(imageId);
|
loadSeriesInfo(imageId);
|
||||||
// Prefill the reserved autocomplete area with top tags so it's never empty.
|
// Prefill the reserved autocomplete area with top tags so it's never empty.
|
||||||
@@ -812,6 +908,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
|
clearFandomPicker();
|
||||||
// Get the image ID before clearing so we can refresh its tags
|
// Get the image ID before clearing so we can refresh its tags
|
||||||
const imageId = getEditorImageId();
|
const imageId = getEditorImageId();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user