From 968d6d1d1ff4c410c5b0c44b8702c48eb28f1927 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Fri, 19 Jun 2026 16:31:27 +0800 Subject: [PATCH] feat(tags): unify recipe modal tag UI with model modal - Replace recipe modal's custom tag display/edit with shared renderCompactTags/setupTagEditMode from ModelTags and utils - Remove 300+ lines of duplicated tag display and editing code - Parameterize setupTagEditMode with saveHandler/onSaved/showSuggestions options for recipe-specific save flow (updateRecipeMetadata + dirty state) - Scope all DOM queries in ModelTags.js via options.container / this.closest to prevent cross-modal element conflicts - Fix edit button alignment (justify-content: flex-start) - Fix tag tooltip selector scoping in setupTagTooltip - Add width: 100% to #recipeTagsContainer for edit container full width --- static/css/components/lora-modal/tag.css | 9 +- static/css/components/recipe-modal.css | 122 +------- static/js/components/RecipeModal.js | 275 +++--------------- static/js/components/shared/ModelTags.js | 194 +++++++----- static/js/components/shared/utils.js | 8 +- templates/components/recipe_modal.html | 9 +- .../contextMenu.interactions.test.js | 114 ++------ 7 files changed, 194 insertions(+), 537 deletions(-) diff --git a/static/css/components/lora-modal/tag.css b/static/css/components/lora-modal/tag.css index 3674025a..4ee49d43 100644 --- a/static/css/components/lora-modal/tag.css +++ b/static/css/components/lora-modal/tag.css @@ -17,6 +17,8 @@ flex-wrap: nowrap; gap: 6px; align-items: center; + min-width: 0; + overflow: hidden; } .model-tag-compact { @@ -28,6 +30,9 @@ font-size: 0.75em; color: var(--text-color); white-space: nowrap; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; } /* Style for empty tags placeholder */ @@ -118,8 +123,9 @@ /* Model Tags Edit Mode */ .model-tags-header { display: flex; - justify-content: space-between; + justify-content: flex-start; align-items: center; + overflow: hidden; } .edit-tags-btn { @@ -132,6 +138,7 @@ border-radius: var(--border-radius-xs); transition: var(--transition-base); margin-left: var(--space-1); + flex-shrink: 0; } .edit-tags-btn.visible, diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index 08981032..3445dc18 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -9,6 +9,10 @@ position: relative; } +#recipeTagsContainer { + width: 100%; +} + .recipe-modal-header h2 { margin: 0 0 var(--space-1); padding: var(--space-1); @@ -95,127 +99,11 @@ min-width: 0; } -.content-editor.tags-editor input { - font-size: 0.9em; -} - /* Remove obsolete button styles */ .editor-actions { display: none; } -/* Special styling for tags content */ -.tags-content { - display: flex; - align-items: center; - flex-wrap: nowrap; - gap: 8px; -} - -.tags-display { - display: flex; - flex-wrap: nowrap; - gap: 6px; - align-items: center; - flex: 1; - min-width: 0; - overflow: hidden; -} - -.no-tags { - font-size: 0.85em; - color: var(--text-color); - opacity: 0.6; - font-style: italic; -} - -/* Recipe Tags styles */ -.recipe-tags-container { - position: relative; - margin-top: 0; - margin-bottom: 10px; -} - -.recipe-tags-compact { - display: flex; - flex-wrap: nowrap; - gap: 6px; - align-items: center; -} - -.recipe-tag-compact { - background: var(--surface-subtle); - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: var(--border-radius-xs); - padding: 2px 8px; - font-size: 0.75em; - color: var(--text-color); - white-space: nowrap; -} - -[data-theme="dark"] .recipe-tag-compact { - background: var(--surface-subtle); - border: 1px solid var(--lora-border); -} - -.recipe-tag-more { - background: var(--lora-accent); - color: var(--lora-text); - border-radius: var(--border-radius-xs); - padding: 2px 8px; - font-size: 0.75em; - cursor: pointer; - white-space: nowrap; - font-weight: 500; -} - -.recipe-tags-tooltip { - position: absolute; - top: calc(100% + 8px); - left: 0; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-sm); - box-shadow: var(--shadow-dropdown); - padding: 10px 14px; - max-width: 400px; - z-index: 10; - opacity: 0; - visibility: hidden; - transform: translateY(-4px); - transition: var(--transition-base); - pointer-events: none; -} - -.recipe-tags-tooltip.visible { - opacity: 1; - visibility: visible; - transform: translateY(0); - pointer-events: auto; -} - -.tooltip-content { - display: flex; - flex-wrap: wrap; - gap: 6px; - max-height: 200px; - overflow-y: auto; -} - -.tooltip-tag { - background: var(--surface-hover); - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: var(--border-radius-xs); - padding: 3px 8px; - font-size: 0.75em; - color: var(--text-color); -} - -[data-theme="dark"] .tooltip-tag { - background: var(--surface-hover); - border: 1px solid var(--lora-border); -} - #recipeModal .modal-content { display: flex; flex-direction: column; @@ -1153,7 +1041,7 @@ max-height: 2.4em; } - .recipe-tags-container { + #recipeTagsContainer { margin-bottom: 6px; } diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index 02f5528d..e3598326 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -7,6 +7,8 @@ import { fetchRecipeDetails, updateRecipeMetadata } from '../api/recipeApi.js'; import { downloadManager } from '../managers/DownloadManager.js'; import { MODEL_TYPES } from '../api/apiConfig.js'; import { openMediaViewer } from './shared/MediaViewer.js'; +import { renderCompactTags, setupTagTooltip } from './shared/utils.js'; +import { setupTagEditMode } from './shared/ModelTags.js'; const ALLOWED_GEN_PARAM_KEYS = new Set([ 'prompt', @@ -139,14 +141,6 @@ class RecipeModal { this.saveTitleEdit(); } - // Handle tags edit - const tagsEditor = document.getElementById('recipeTagsEditor'); - if (tagsEditor && tagsEditor.classList.contains('active') && - !tagsEditor.contains(event.target) && - !event.target.closest('.edit-icon')) { - this.saveTagsEdit(); - } - // Handle reconnect input const reconnectContainers = document.querySelectorAll('.lora-reconnect-container'); reconnectContainers.forEach(container => { @@ -236,98 +230,10 @@ class RecipeModal { this.filePath = hydratedRecipe.file_path; this.listFilePath = hydratedRecipe.file_path; - // Set recipe tags if they exist - const tagsCompactElement = document.getElementById('recipeTagsCompact'); - const tagsTooltipContent = document.getElementById('recipeTagsTooltipContent'); - - if (tagsCompactElement) { - // Add tags container with edit functionality - tagsCompactElement.innerHTML = ` -
-
- -
-
- -
- `; - - const tagsDisplay = tagsCompactElement.querySelector('.tags-display'); - - if (hydratedRecipe.tags && hydratedRecipe.tags.length > 0) { - // Limit displayed tags to 5, show a "+X more" button if needed - const maxVisibleTags = 5; - const visibleTags = hydratedRecipe.tags.slice(0, maxVisibleTags); - const remainingTags = hydratedRecipe.tags.length > maxVisibleTags ? hydratedRecipe.tags.slice(maxVisibleTags) : []; - - // Add visible tags - visibleTags.forEach(tag => { - const tagElement = document.createElement('div'); - tagElement.className = 'recipe-tag-compact'; - tagElement.textContent = tag; - tagsDisplay.appendChild(tagElement); - }); - - // Add "more" button if needed - if (remainingTags.length > 0) { - const moreButton = document.createElement('div'); - moreButton.className = 'recipe-tag-more'; - moreButton.textContent = `+${remainingTags.length} more`; - tagsDisplay.appendChild(moreButton); - - // Add tooltip functionality - moreButton.addEventListener('mouseenter', () => { - document.getElementById('recipeTagsTooltip').classList.add('visible'); - }); - - moreButton.addEventListener('mouseleave', () => { - setTimeout(() => { - if (!document.getElementById('recipeTagsTooltip').matches(':hover')) { - document.getElementById('recipeTagsTooltip').classList.remove('visible'); - } - }, 300); - }); - - document.getElementById('recipeTagsTooltip').addEventListener('mouseleave', () => { - document.getElementById('recipeTagsTooltip').classList.remove('visible'); - }); - - // Add all tags to tooltip - if (tagsTooltipContent) { - tagsTooltipContent.innerHTML = ''; - hydratedRecipe.tags.forEach(tag => { - const tooltipTag = document.createElement('div'); - tooltipTag.className = 'tooltip-tag'; - tooltipTag.textContent = tag; - tagsTooltipContent.appendChild(tooltipTag); - }); - } - } - } else { - tagsDisplay.innerHTML = '
No tags
'; - } - - // Add event listeners for tags editing - const editTagsIcon = tagsCompactElement.querySelector('.edit-icon'); - const tagsInput = tagsCompactElement.querySelector('.tags-input'); - - // Set current tags in the input - if (hydratedRecipe.tags && hydratedRecipe.tags.length > 0) { - tagsInput.value = hydratedRecipe.tags.join(', '); - } - - editTagsIcon.addEventListener('click', () => this.showTagsEditor()); - - // Add key event listener for Enter key - tagsInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - this.saveTagsEdit(); - } else if (e.key === 'Escape') { - e.preventDefault(); - this.cancelTagsEdit(); - } - }); + // Render tags using shared utility + const tagsContainer = document.getElementById('recipeTagsContainer'); + if (tagsContainer) { + this.updateTagsDisplay(tagsContainer, hydratedRecipe.tags || []); } // Set recipe image @@ -609,17 +515,35 @@ class RecipeModal { } syncTagsDisplay(tags) { - const tagsContainer = document.getElementById('recipeTagsCompact'); - if (!tagsContainer) { - return; - } + const container = document.getElementById('recipeTagsContainer'); + if (!container) return; + this.updateTagsDisplay(container, tags || []); + } - this.updateTagsDisplay(tagsContainer, tags || []); + // Re-render tags display using shared utility, wire edit mode with ModelTags + updateTagsDisplay(container, tags) { + const filePath = this.filePath || ''; - const tagsInput = tagsContainer.querySelector('.tags-input'); - if (tagsInput) { - tagsInput.value = tags && tags.length > 0 ? tags.join(', ') : ''; - } + container.innerHTML = renderCompactTags(tags, filePath); + + // Setup tooltip for all tags + setupTagTooltip(container); + + // Wire edit button using shared tag editing (no suggestions for recipes) + setupTagEditMode(null, { + container: container, + showSuggestions: false, + normalizeTag: false, + saveHandler: async (filePath, tags) => { + await updateRecipeMetadata(filePath, { tags }, this.getMetadataUpdateOptions()); + }, + onSaved: (tags) => { + this.currentRecipe.tags = tags; + this.commitField('tags'); + const c = document.getElementById('recipeTagsContainer'); + if (c) this.updateTagsDisplay(c, tags); + }, + }); } syncPromptField(field, value, placeholder) { @@ -976,139 +900,6 @@ class RecipeModal { } } - // Tags editing methods - showTagsEditor() { - const tagsContainer = document.getElementById('recipeTagsCompact'); - if (tagsContainer) { - tagsContainer.querySelector('.editable-content').classList.add('hide'); - const editor = tagsContainer.querySelector('#recipeTagsEditor'); - editor.classList.add('active'); - const input = editor.querySelector('input'); - input.oninput = () => this.markFieldDirty('tags'); - input.focus(); - } - } - - saveTagsEdit() { - const tagsContainer = document.getElementById('recipeTagsCompact'); - if (tagsContainer) { - const editor = tagsContainer.querySelector('#recipeTagsEditor'); - const input = editor.querySelector('input'); - const tagsText = input.value.trim(); - - // Parse tags - let newTags = []; - if (tagsText) { - newTags = tagsText.split(',') - .map(tag => tag.trim()) - .filter(tag => tag.length > 0); - } - - // Check if tags changed - const oldTags = this.currentRecipe.tags || []; - const tagsChanged = - newTags.length !== oldTags.length || - newTags.some((tag, index) => tag !== oldTags[index]); - - if (tagsChanged) { - // Update the recipe on the server - updateRecipeMetadata(this.filePath, { tags: newTags }, this.getMetadataUpdateOptions()) - .then(data => { - // Show success toast - showToast('toast.recipes.tagsUpdated', {}, 'success'); - - // Update the current recipe object - this.currentRecipe.tags = newTags; - this.commitField('tags'); - - // Update tags in the UI - this.updateTagsDisplay(tagsContainer, newTags); - }) - .catch(error => { - // Error is handled in the API function - this.clearFieldDirty('tags'); - }); - } else { - this.clearFieldDirty('tags'); - } - - // Hide editor - editor.classList.remove('active'); - tagsContainer.querySelector('.editable-content').classList.remove('hide'); - } - } - - // Helper method to update tags display - updateTagsDisplay(tagsContainer, tags) { - const tagsDisplay = tagsContainer.querySelector('.tags-display'); - tagsDisplay.innerHTML = ''; - - if (tags.length > 0) { - // Limit displayed tags to 5, show a "+X more" button if needed - const maxVisibleTags = 5; - const visibleTags = tags.slice(0, maxVisibleTags); - const remainingTags = tags.length > maxVisibleTags ? tags.slice(maxVisibleTags) : []; - - // Add visible tags - visibleTags.forEach(tag => { - const tagElement = document.createElement('div'); - tagElement.className = 'recipe-tag-compact'; - tagElement.textContent = tag; - tagsDisplay.appendChild(tagElement); - }); - - // Add "more" button if needed - if (remainingTags.length > 0) { - const moreButton = document.createElement('div'); - moreButton.className = 'recipe-tag-more'; - moreButton.textContent = `+${remainingTags.length} more`; - tagsDisplay.appendChild(moreButton); - - // Update tooltip content - const tooltipContent = document.getElementById('recipeTagsTooltipContent'); - if (tooltipContent) { - tooltipContent.innerHTML = ''; - tags.forEach(tag => { - const tooltipTag = document.createElement('div'); - tooltipTag.className = 'tooltip-tag'; - tooltipTag.textContent = tag; - tooltipContent.appendChild(tooltipTag); - }); - } - - // Re-add tooltip functionality - moreButton.addEventListener('mouseenter', () => { - document.getElementById('recipeTagsTooltip').classList.add('visible'); - }); - - moreButton.addEventListener('mouseleave', () => { - setTimeout(() => { - if (!document.getElementById('recipeTagsTooltip').matches(':hover')) { - document.getElementById('recipeTagsTooltip').classList.remove('visible'); - } - }, 300); - }); - } - } else { - tagsDisplay.innerHTML = '
No tags
'; - } - } - - cancelTagsEdit() { - const tagsContainer = document.getElementById('recipeTagsCompact'); - if (tagsContainer) { - // Reset input value - const editor = tagsContainer.querySelector('#recipeTagsEditor'); - const input = editor.querySelector('input'); - input.value = this.currentRecipe.tags ? this.currentRecipe.tags.join(', ') : ''; - this.clearFieldDirty('tags'); - - // Hide editor - editor.classList.remove('active'); - tagsContainer.querySelector('.editable-content').classList.remove('hide'); - } - } - setupPromptEditors() { const promptConfigs = [ { diff --git a/static/js/components/shared/ModelTags.js b/static/js/components/shared/ModelTags.js index 922af248..b6226626 100644 --- a/static/js/components/shared/ModelTags.js +++ b/static/js/components/shared/ModelTags.js @@ -29,6 +29,14 @@ let priorityTagSuggestionsLoaded = false; let priorityTagSuggestionsPromise = null; let activeTagDragState = null; +// Configurable options for tag editing (set by setupTagEditMode) +let tagEditOptions = { + showSuggestions: true, + saveHandler: null, + onSaved: null, + normalizeTag: true, +}; + function normalizeModelTypeKey(modelType) { if (!modelType) { return ''; @@ -140,13 +148,30 @@ let saveTagsHandler = null; /** * Set up tag editing mode + * @param {string|null} modelType - Model type for suggestions (e.g. 'loras', 'checkpoints') + * @param {Object} [options] - Optional configuration + * @param {boolean} [options.showSuggestions=true] - Show priority tag suggestions dropdown + * @param {Function} [options.saveHandler] - Custom save function, async (filePath, tags) => {} + * @param {Function} [options.onSaved] - Called after successful save, (tags) => {} + * @param {boolean} [options.normalizeTag=true] - Lowercase tag on add */ -export function setupTagEditMode(modelType = null) { - const editBtn = document.querySelector('.edit-tags-btn'); +export function setupTagEditMode(modelType = null, options = {}) { + // Store options for use by saveTags and addNewTag + tagEditOptions = { + showSuggestions: options.showSuggestions !== false, + saveHandler: options.saveHandler || null, + onSaved: options.onSaved || null, + normalizeTag: options.normalizeTag !== false, + }; + + const root = options.container || document; + const editBtn = root.querySelector('.edit-tags-btn'); if (!editBtn) return; - setActiveModelTypeKey(modelType); - ensurePriorityTagSuggestions(); + if (tagEditOptions.showSuggestions) { + setActiveModelTypeKey(modelType); + ensurePriorityTagSuggestions(); + } // Store original tags for restoring on cancel let originalTags = []; @@ -158,7 +183,8 @@ export function setupTagEditMode(modelType = null) { // Create new handler and store reference const editBtnClickHandler = function() { - const tagsSection = document.querySelector('.model-tags-container'); + const tagsSection = this.closest('.model-tags-container'); + if (!tagsSection) return; const isEditMode = tagsSection.classList.toggle('edit-mode'); const filePath = this.dataset.filePath; @@ -193,16 +219,18 @@ export function setupTagEditMode(modelType = null) { tagsSection.appendChild(editContainer); // Setup the tag input field behavior - setupTagInput(); + setupTagInput(tagsSection); // Create and add preset suggestions dropdown - const tagForm = editContainer.querySelector('.metadata-add-form'); - const suggestionsDropdown = createSuggestionsDropdown(originalTags); - tagForm.appendChild(suggestionsDropdown); + if (tagEditOptions.showSuggestions) { + const tagForm = editContainer.querySelector('.metadata-add-form'); + const suggestionsDropdown = createSuggestionsDropdown(originalTags); + tagForm.appendChild(suggestionsDropdown); + } // Setup delete buttons for existing tags setupDeleteButtons(); - setupTagDragAndDrop(); + setupTagDragAndDrop(tagsSection); // Transfer click event from original button to the cloned one const newEditBtn = editContainer.querySelector('.metadata-header-btn'); @@ -218,7 +246,7 @@ export function setupTagEditMode(modelType = null) { // Just show the existing edit container tagsEditContainer.style.display = 'block'; editBtn.style.display = 'none'; - setupTagDragAndDrop(); + setupTagDragAndDrop(tagsSection); } } else { // Exit edit mode @@ -255,7 +283,7 @@ export function setupTagEditMode(modelType = null) { saveTagsHandler = function(e) { if (e.target.classList.contains('save-tags-btn') || e.target.closest('.save-tags-btn')) { - saveTags(); + saveTags(e.target); } }; @@ -267,19 +295,28 @@ export function setupTagEditMode(modelType = null) { /** * Save tags + * @param {Element} [triggerElement] - The element that triggered the save (e.g. save button) */ -async function saveTags() { - const editBtn = document.querySelector('.edit-tags-btn'); - if (!editBtn) return; +async function saveTags(triggerElement = null) { + let editBtn; + let scope; + if (triggerElement) { + scope = triggerElement.closest('.model-tags-container'); + editBtn = scope ? scope.querySelector('.edit-tags-btn') : document.querySelector('.edit-tags-btn'); + } else { + scope = document.querySelector('.model-tags-container'); + editBtn = scope ? scope.querySelector('.edit-tags-btn') : null; + } + if (!editBtn || !scope) return; const filePath = editBtn.dataset.filePath; - const tagElements = document.querySelectorAll('.metadata-item'); + const tagElements = scope.querySelectorAll('.metadata-item'); let tags = Array.from(tagElements).map(tag => tag.dataset.tag); // Flush uncommitted input as a tag so it's not silently lost on save - const tagInput = document.querySelector('.metadata-input'); + const tagInput = scope.querySelector('.metadata-input'); if (tagInput) { - const pendingTag = tagInput.value.trim().toLowerCase(); + const pendingTag = tagEditOptions.normalizeTag ? tagInput.value.trim().toLowerCase() : tagInput.value.trim(); if (pendingTag && !tags.includes(pendingTag)) { tags.push(pendingTag); } @@ -287,7 +324,7 @@ async function saveTags() { } // Get original tags to compare - const originalTagElements = document.querySelectorAll('.tooltip-tag'); + const originalTagElements = scope.querySelectorAll('.tooltip-tag'); const originalTags = Array.from(originalTagElements).map(tag => tag.textContent); // Check if tags have actually changed @@ -301,59 +338,68 @@ async function saveTags() { } try { - // Save tags metadata - await getModelApiClient().saveModelMetadata(filePath, { tags: tags }); + // Use custom save handler if provided, otherwise default model API + if (tagEditOptions.saveHandler) { + await tagEditOptions.saveHandler(filePath, tags); + } else { + await getModelApiClient().saveModelMetadata(filePath, { tags: tags }); + } // Set flag to skip restoring original tags when exiting edit mode editBtn.dataset.skipRestore = "true"; - // Update the compact tags display - const compactTagsContainer = document.querySelector('.model-tags-container'); - if (compactTagsContainer) { - // Generate new compact tags HTML - const compactTagsDisplay = compactTagsContainer.querySelector('.model-tags-compact'); - - if (compactTagsDisplay) { - // Clear current tags - compactTagsDisplay.innerHTML = ''; + // Use custom onSaved if provided (e.g. for recipe dirty state + re-render) + if (tagEditOptions.onSaved) { + tagEditOptions.onSaved(tags); + } else { + // Update the compact tags display + const compactTagsContainer = scope; + if (compactTagsContainer) { + // Generate new compact tags HTML + const compactTagsDisplay = compactTagsContainer.querySelector('.model-tags-compact'); - // Add visible tags (up to 5) - const visibleTags = tags.slice(0, 5); - visibleTags.forEach(tag => { - const span = document.createElement('span'); - span.className = 'model-tag-compact'; - span.textContent = tag; - compactTagsDisplay.appendChild(span); - }); + if (compactTagsDisplay) { + // Clear current tags + compactTagsDisplay.innerHTML = ''; + + // Add visible tags (up to 5) + const visibleTags = tags.slice(0, 5); + visibleTags.forEach(tag => { + const span = document.createElement('span'); + span.className = 'model-tag-compact'; + span.textContent = tag; + compactTagsDisplay.appendChild(span); + }); + + // Add more indicator if needed + const remainingCount = Math.max(0, tags.length - 5); + if (remainingCount > 0) { + const more = document.createElement('span'); + more.className = 'model-tag-more'; + more.dataset.count = remainingCount; + more.textContent = `+${remainingCount}`; + compactTagsDisplay.appendChild(more); + } + } - // Add more indicator if needed - const remainingCount = Math.max(0, tags.length - 5); - if (remainingCount > 0) { - const more = document.createElement('span'); - more.className = 'model-tag-more'; - more.dataset.count = remainingCount; - more.textContent = `+${remainingCount}`; - compactTagsDisplay.appendChild(more); + // Update tooltip content + const tooltipContent = compactTagsContainer.querySelector('.tooltip-content'); + if (tooltipContent) { + tooltipContent.innerHTML = ''; + + tags.forEach(tag => { + const span = document.createElement('span'); + span.className = 'tooltip-tag'; + span.textContent = tag; + tooltipContent.appendChild(span); + }); } } - // Update tooltip content - const tooltipContent = compactTagsContainer.querySelector('.tooltip-content'); - if (tooltipContent) { - tooltipContent.innerHTML = ''; - - tags.forEach(tag => { - const span = document.createElement('span'); - span.className = 'tooltip-tag'; - span.textContent = tag; - tooltipContent.appendChild(span); - }); - } + // Exit edit mode + editBtn.click(); } - // Exit edit mode - editBtn.click(); - showToast('modelTags.messages.updated', {}, 'success'); } catch (error) { console.error('Error saving tags:', error); @@ -470,16 +516,19 @@ function renderPriorityTagSuggestions(container, existingTags = []) { /** * Set up tag input behavior + * @param {Element} scopeContainer - The .model-tags-container element */ -function setupTagInput() { - const tagInput = document.querySelector('.metadata-input'); +function setupTagInput(scopeContainer) { + const tagInput = scopeContainer + ? scopeContainer.querySelector('.metadata-input') + : document.querySelector('.metadata-input'); if (tagInput) { tagInput.focus(); tagInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); - addNewTag(this.value); + addNewTag(this.value, this); this.value = ''; // Clear input after adding } }); @@ -504,9 +553,12 @@ function setupDeleteButtons() { /** * Enable drag-and-drop sorting for tag items + * @param {Element} [scopeContainer] - Optional scoped .model-tags-container element */ -function setupTagDragAndDrop() { - const container = document.querySelector(METADATA_ITEMS_CONTAINER_SELECTOR); +function setupTagDragAndDrop(scopeContainer) { + const container = scopeContainer + ? scopeContainer.querySelector(METADATA_ITEMS_CONTAINER_SELECTOR) + : document.querySelector(METADATA_ITEMS_CONTAINER_SELECTOR); if (!container) { return; } @@ -712,12 +764,14 @@ function finishPointerDrag() { /** * Add a new tag * @param {string} tag - Tag to add + * @param {Element} [scopeElement] - Element within the correct .model-tags-container for scoping */ -function addNewTag(tag) { - tag = tag.trim().toLowerCase(); +function addNewTag(tag, scopeElement = null) { + tag = tagEditOptions.normalizeTag ? tag.trim().toLowerCase() : tag.trim(); if (!tag) return; - const tagsContainer = document.querySelector('.metadata-items'); + const scope = scopeElement ? scopeElement.closest('.model-tags-container') : document; + const tagsContainer = scope.querySelector('.metadata-items'); if (!tagsContainer) return; // Validation: Check length @@ -762,7 +816,7 @@ function addNewTag(tag) { }); tagsContainer.appendChild(newTag); - setupTagDragAndDrop(); + setupTagDragAndDrop(scope); // Update status of items in the suggestions dropdown updateSuggestionsDropdown(); diff --git a/static/js/components/shared/utils.js b/static/js/components/shared/utils.js index a0226e05..e8b4a38b 100644 --- a/static/js/components/shared/utils.js +++ b/static/js/components/shared/utils.js @@ -78,10 +78,12 @@ export function renderCompactTags(tags, filePath = '') { /** * Set up tag tooltip functionality + * @param {Element} [scopeContainer] - Optional container to scope the querySelector */ -export function setupTagTooltip() { - const tagsContainer = document.querySelector('.model-tags-container'); - const tooltip = document.querySelector('.model-tags-tooltip'); +export function setupTagTooltip(scopeContainer = null) { + const root = scopeContainer || document; + const tagsContainer = root.querySelector('.model-tags-container'); + const tooltip = root.querySelector('.model-tags-tooltip'); if (tagsContainer && tooltip) { tagsContainer.addEventListener('mouseenter', () => { diff --git a/templates/components/recipe_modal.html b/templates/components/recipe_modal.html index 41327356..a9271bac 100644 --- a/templates/components/recipe_modal.html +++ b/templates/components/recipe_modal.html @@ -6,13 +6,8 @@

Recipe Details

- -
-
-
-
-
-
+ +