DRYing the project
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Use shared TagUtils module
|
||||
const { getTagIcon, getTagDisplayName, getTagDisplayNameForOverlay, escapeHtml, createTagChipHtml } = window.TagUtils || {};
|
||||
|
||||
const state = {
|
||||
isSelectMode: false,
|
||||
selectedIds: new Set(),
|
||||
@@ -263,7 +266,7 @@
|
||||
|
||||
dom.commonTags.innerHTML = tags.map(t => {
|
||||
const icon = getTagIcon(t.kind);
|
||||
const display = t.name.includes(':') ? t.name.split(':')[1] : t.name;
|
||||
const display = getTagDisplayName(t.name, t.kind);
|
||||
const escapedName = escapeHtml(t.name);
|
||||
const escapedDisplay = escapeHtml(display);
|
||||
return `<span class="tag-chip removable" data-name="${escapedName}">
|
||||
@@ -471,7 +474,7 @@
|
||||
|
||||
dom.autocomplete.innerHTML = tags.map((t, i) => {
|
||||
const icon = getTagIcon(t.kind);
|
||||
const displayName = t.name.includes(':') ? t.name.split(':')[1] : t.name;
|
||||
const displayName = getTagDisplayName(t.name, t.kind);
|
||||
return `<div class="tag-autocomplete-item" data-index="${i}" data-name="${escapeHtml(t.name)}">
|
||||
<span>${icon} ${escapeHtml(displayName)}</span>
|
||||
<span class="tag-kind">${t.kind || 'user'}</span>
|
||||
@@ -491,41 +494,9 @@
|
||||
autocompleteSelectedIndex = index;
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Helpers
|
||||
// ---------------------------
|
||||
function getTagIcon(kind) {
|
||||
const icons = {
|
||||
artist: '🎨',
|
||||
archive: '🗜️',
|
||||
character: '👤',
|
||||
series: '📺',
|
||||
fandom: '🎭',
|
||||
rating: '⚠️',
|
||||
source: '🌐',
|
||||
post: '📌'
|
||||
};
|
||||
return icons[kind] || '#';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Tag Refresh for Gallery Items
|
||||
// ---------------------------
|
||||
function getTagDisplayName(name, kind) {
|
||||
// Strip prefix for known prefixed kinds
|
||||
if (['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) {
|
||||
return name.includes(':') ? name.split(':', 2)[1] : name;
|
||||
}
|
||||
// Regular tags get a # prefix
|
||||
return '#' + name;
|
||||
}
|
||||
|
||||
async function refreshItemTags(imageId) {
|
||||
// Find the gallery item
|
||||
const item = document.querySelector(`.img-clickable[data-id="${imageId}"]`);
|
||||
@@ -555,14 +526,8 @@
|
||||
item.appendChild(overlay);
|
||||
}
|
||||
|
||||
// Render tags with icons
|
||||
overlay.innerHTML = visibleTags.map(t => {
|
||||
const displayName = getTagDisplayName(t.name, t.kind);
|
||||
const icon = getTagIcon(t.kind);
|
||||
const encodedName = encodeURIComponent(t.name);
|
||||
const prefix = icon !== '#' ? icon + ' ' : '#';
|
||||
return `<a class="tag-chip" href="/gallery?tag=${encodedName}" title="Filter by ${escapeHtml(t.name)}" onclick="event.stopPropagation()">${prefix}${escapeHtml(displayName)}</a>`;
|
||||
}).join('');
|
||||
// Render tags with icons using shared utility
|
||||
overlay.innerHTML = visibleTags.map(t => createTagChipHtml(t)).join('');
|
||||
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh tags for image', imageId, e);
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Use shared TagUtils module
|
||||
const { getTagIcon, getTagDisplayName, escapeHtml, createTagChipHtml } = window.TagUtils || {};
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
SCROLL_THRESHOLD: 1200, // px from bottom to trigger load
|
||||
@@ -187,21 +190,6 @@
|
||||
return section;
|
||||
}
|
||||
|
||||
// Tag icon mapping (matches showcase.js and bulk-select.js)
|
||||
function getTagIcon(kind) {
|
||||
const icons = {
|
||||
artist: '🎨',
|
||||
archive: '🗜️',
|
||||
character: '👤',
|
||||
series: '📺',
|
||||
fandom: '🎭',
|
||||
rating: '⚠️',
|
||||
source: '🌐',
|
||||
post: '📌'
|
||||
};
|
||||
return icons[kind] || '#';
|
||||
}
|
||||
|
||||
// Create a single image element
|
||||
function createImageElement(img) {
|
||||
const item = document.createElement('div');
|
||||
@@ -211,15 +199,10 @@
|
||||
item.dataset.type = img.is_video ? 'video' : 'image';
|
||||
item.dataset.filename = img.filename;
|
||||
|
||||
// Build tag overlay HTML with icons
|
||||
// Build tag overlay HTML with icons using shared utility
|
||||
let tagsHtml = '';
|
||||
if (img.tags && img.tags.length > 0) {
|
||||
const tagChips = img.tags.map(t => {
|
||||
const displayName = t.name.includes(':') ? t.name.split(':')[1] : t.name;
|
||||
const icon = getTagIcon(t.kind);
|
||||
const prefix = icon !== '#' ? icon + ' ' : '#';
|
||||
return `<a class="tag-chip" href="/gallery?tag=${encodeURIComponent(t.name)}" title="Filter by ${t.name}" onclick="event.stopPropagation()">${prefix}${displayName}</a>`;
|
||||
}).join('');
|
||||
const tagChips = img.tags.map(t => createTagChipHtml(t)).join('');
|
||||
tagsHtml = `<div class="tag-overlay">${tagChips}</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
// JS-managed masonry with column distribution for infinite scroll
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Use shared TagUtils module
|
||||
const { formatTagHtml, createTagChipHtml } = window.TagUtils || {};
|
||||
|
||||
const container = document.getElementById('showcaseGrid');
|
||||
if (!container) return;
|
||||
|
||||
@@ -156,15 +161,8 @@
|
||||
if (visibleTags.length > 0) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'tag-overlay';
|
||||
visibleTags.forEach(t => {
|
||||
const chip = document.createElement('a');
|
||||
chip.className = 'tag-chip';
|
||||
chip.href = `/gallery?tag=${encodeURIComponent(t.name)}`;
|
||||
chip.title = `Filter by ${t.name}`;
|
||||
chip.onclick = (e) => e.stopPropagation();
|
||||
chip.innerHTML = formatTag(t);
|
||||
overlay.appendChild(chip);
|
||||
});
|
||||
// Use shared utility to create tag chips
|
||||
overlay.innerHTML = visibleTags.map(t => createTagChipHtml(t)).join('');
|
||||
item.appendChild(overlay);
|
||||
}
|
||||
|
||||
@@ -274,33 +272,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function formatTag(tag) {
|
||||
const escape = (s) => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
const icons = {
|
||||
artist: '🎨',
|
||||
archive: '🗜️',
|
||||
character: '👤',
|
||||
series: '📺',
|
||||
fandom: '🎭',
|
||||
rating: '⚠️',
|
||||
source: '🌐',
|
||||
post: '📌'
|
||||
};
|
||||
|
||||
const icon = icons[tag.kind];
|
||||
if (icon) {
|
||||
const label = tag.name.includes(':') ? tag.name.split(':', 2)[1] : tag.name;
|
||||
return `${icon} ${escape(label)}`;
|
||||
} else {
|
||||
return `#${escape(tag.name)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Start
|
||||
init();
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// /app/static/js/tag-utils.js
|
||||
// Shared tag utility functions used across gallery, modal, and bulk editor
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Get emoji icon for a tag kind
|
||||
* @param {string} kind - Tag kind (artist, archive, character, etc.)
|
||||
* @returns {string} Emoji icon or '#' for user tags
|
||||
*/
|
||||
function getTagIcon(kind) {
|
||||
const icons = {
|
||||
artist: '🎨',
|
||||
archive: '🗜️',
|
||||
character: '👤',
|
||||
series: '📺',
|
||||
fandom: '🎭',
|
||||
rating: '⚠️',
|
||||
source: '🌐',
|
||||
post: '📌'
|
||||
};
|
||||
return icons[kind] || '#';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for a tag, stripping the prefix if present
|
||||
* @param {string} name - Full tag name (e.g., "artist:Name")
|
||||
* @param {string} [kind] - Optional tag kind for context
|
||||
* @returns {string} Display name without prefix
|
||||
*/
|
||||
function getTagDisplayName(name, kind) {
|
||||
// For prefixed kinds, strip the prefix
|
||||
if (kind && ['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) {
|
||||
return name.includes(':') ? name.split(':', 2)[1] : name;
|
||||
}
|
||||
// If no kind specified but has colon, assume it's prefixed
|
||||
if (!kind && name.includes(':')) {
|
||||
return name.split(':', 2)[1];
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for tag overlay (used in gallery items)
|
||||
* Returns name with # prefix for unprefixed user tags
|
||||
* @param {string} name - Full tag name
|
||||
* @param {string} kind - Tag kind
|
||||
* @returns {string} Display name for overlay
|
||||
*/
|
||||
function getTagDisplayNameForOverlay(name, kind) {
|
||||
if (['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) {
|
||||
return name.includes(':') ? name.split(':', 2)[1] : name;
|
||||
}
|
||||
return '#' + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML special characters
|
||||
* @param {string} str - String to escape
|
||||
* @returns {string} HTML-escaped string
|
||||
*/
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a tag for display with icon and name
|
||||
* @param {Object} tag - Tag object with name and kind
|
||||
* @returns {string} Formatted HTML string
|
||||
*/
|
||||
function formatTagHtml(tag) {
|
||||
const icon = getTagIcon(tag.kind);
|
||||
const displayName = getTagDisplayName(tag.name, tag.kind);
|
||||
const escaped = escapeHtml(displayName);
|
||||
|
||||
if (icon !== '#') {
|
||||
return `${icon} ${escaped}`;
|
||||
}
|
||||
return `#${escaped}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tag chip HTML for gallery overlays
|
||||
* @param {Object} tag - Tag object with name and kind
|
||||
* @param {boolean} [stopPropagation=true] - Add onclick to stop propagation
|
||||
* @returns {string} HTML string for tag chip link
|
||||
*/
|
||||
function createTagChipHtml(tag, stopPropagation = true) {
|
||||
const icon = getTagIcon(tag.kind);
|
||||
const displayName = getTagDisplayNameForOverlay(tag.name, tag.kind);
|
||||
const encodedName = encodeURIComponent(tag.name);
|
||||
const escapedTitle = escapeHtml(tag.name);
|
||||
const escapedDisplay = escapeHtml(displayName);
|
||||
const prefix = icon !== '#' ? icon + ' ' : '#';
|
||||
const onclick = stopPropagation ? ' onclick="event.stopPropagation()"' : '';
|
||||
|
||||
return `<a class="tag-chip" href="/gallery?tag=${encodedName}" title="Filter by ${escapedTitle}"${onclick}>${prefix}${escapedDisplay}</a>`;
|
||||
}
|
||||
|
||||
// Export to window.TagUtils
|
||||
window.TagUtils = {
|
||||
getTagIcon,
|
||||
getTagDisplayName,
|
||||
getTagDisplayNameForOverlay,
|
||||
escapeHtml,
|
||||
formatTagHtml,
|
||||
createTagChipHtml
|
||||
};
|
||||
})();
|
||||
@@ -57,30 +57,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// ---------------------------
|
||||
// Tag editor helpers
|
||||
// ---------------------------
|
||||
// Use shared TagUtils module
|
||||
const { getTagIcon, getTagDisplayName, getTagDisplayNameForOverlay, escapeHtml, createTagChipHtml } = window.TagUtils || {};
|
||||
|
||||
function setEditorImageId(id) {
|
||||
if (tagEditor) tagEditor.dataset.imageId = id || '';
|
||||
}
|
||||
function getEditorImageId() {
|
||||
return tagEditor ? tagEditor.dataset.imageId : '';
|
||||
}
|
||||
function getTagIcon(kind) {
|
||||
const icons = {
|
||||
artist: '🎨',
|
||||
archive: '🗜️',
|
||||
character: '👤',
|
||||
series: '📺',
|
||||
fandom: '🎭',
|
||||
rating: '⚠️',
|
||||
source: '🌐',
|
||||
post: '📌'
|
||||
};
|
||||
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;
|
||||
@@ -108,13 +93,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// ---------------------------
|
||||
// Gallery item tag refresh (fallback if bulk-select.js not loaded)
|
||||
// ---------------------------
|
||||
function getTagDisplayNameForOverlay(name, kind) {
|
||||
if (['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) {
|
||||
return name.includes(':') ? name.split(':', 2)[1] : name;
|
||||
}
|
||||
return '#' + name;
|
||||
}
|
||||
|
||||
async function refreshItemTagsFallback(imageId) {
|
||||
const item = document.querySelector(`.img-clickable[data-id="${imageId}"]`);
|
||||
if (!item) return;
|
||||
@@ -138,15 +116,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
item.appendChild(overlay);
|
||||
}
|
||||
|
||||
overlay.innerHTML = visibleTags.map(t => {
|
||||
const displayName = getTagDisplayNameForOverlay(t.name, t.kind);
|
||||
const icon = getTagIcon(t.kind);
|
||||
const encodedName = encodeURIComponent(t.name);
|
||||
const escapedName = t.name.replace(/"/g, '"');
|
||||
const escapedDisplay = displayName.replace(/</g, '<').replace(/>/g, '>');
|
||||
const prefix = icon !== '#' ? icon + ' ' : '#';
|
||||
return `<a class="tag-chip" href="/gallery?tag=${encodedName}" title="Filter by ${escapedName}" onclick="event.stopPropagation()">${prefix}${escapedDisplay}</a>`;
|
||||
}).join('');
|
||||
overlay.innerHTML = visibleTags.map(t => createTagChipHtml(t)).join('');
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh tags for image', imageId, e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user