From 0ecbdf6f39e8482fd319721636d74db007cd409d Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Fri, 17 Oct 2025 10:52:02 +0800 Subject: [PATCH] feat(context-menu): prevent duplicate NSFW selector initialization Add initialization tracking to prevent multiple event listener attachments in context menu components. Use dataset.initialized flag to ensure NSFW selector events are only set up once per component instance. In ModelContextMenuMixin, replace DOM elements and reattach event listeners to avoid duplicates when components are reinitialized. This fixes issues where multiple click handlers could be attached to the same elements. --- .../ContextMenu/CheckpointContextMenu.js | 5 ++-- .../ContextMenu/EmbeddingContextMenu.js | 5 ++-- .../components/ContextMenu/LoraContextMenu.js | 5 ++-- .../ContextMenu/ModelContextMenuMixin.js | 30 ++++++++++++++----- .../ContextMenu/RecipeContextMenu.js | 5 ++-- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/static/js/components/ContextMenu/CheckpointContextMenu.js b/static/js/components/ContextMenu/CheckpointContextMenu.js index 748f27aa..cc9661e9 100644 --- a/static/js/components/ContextMenu/CheckpointContextMenu.js +++ b/static/js/components/ContextMenu/CheckpointContextMenu.js @@ -11,9 +11,10 @@ export class CheckpointContextMenu extends BaseContextMenu { this.modelType = 'checkpoint'; this.resetAndReload = resetAndReload; - // Initialize NSFW Level Selector events - if (this.nsfwSelector) { + // Initialize NSFW Level Selector events only if not already initialized + if (this.nsfwSelector && !this.nsfwSelector.dataset.initialized) { this.initNSFWSelector(); + this.nsfwSelector.dataset.initialized = 'true'; } } diff --git a/static/js/components/ContextMenu/EmbeddingContextMenu.js b/static/js/components/ContextMenu/EmbeddingContextMenu.js index 0629bff9..ab26f1a9 100644 --- a/static/js/components/ContextMenu/EmbeddingContextMenu.js +++ b/static/js/components/ContextMenu/EmbeddingContextMenu.js @@ -11,9 +11,10 @@ export class EmbeddingContextMenu extends BaseContextMenu { this.modelType = 'embedding'; this.resetAndReload = resetAndReload; - // Initialize NSFW Level Selector events - if (this.nsfwSelector) { + // Initialize NSFW Level Selector events only if not already initialized + if (this.nsfwSelector && !this.nsfwSelector.dataset.initialized) { this.initNSFWSelector(); + this.nsfwSelector.dataset.initialized = 'true'; } } diff --git a/static/js/components/ContextMenu/LoraContextMenu.js b/static/js/components/ContextMenu/LoraContextMenu.js index 4b72cafa..bce9c8cc 100644 --- a/static/js/components/ContextMenu/LoraContextMenu.js +++ b/static/js/components/ContextMenu/LoraContextMenu.js @@ -12,9 +12,10 @@ export class LoraContextMenu extends BaseContextMenu { this.modelType = 'lora'; this.resetAndReload = resetAndReload; - // Initialize NSFW Level Selector events - if (this.nsfwSelector) { + // Initialize NSFW Level Selector events only if not already initialized + if (this.nsfwSelector && !this.nsfwSelector.dataset.initialized) { this.initNSFWSelector(); + this.nsfwSelector.dataset.initialized = 'true'; } } diff --git a/static/js/components/ContextMenu/ModelContextMenuMixin.js b/static/js/components/ContextMenu/ModelContextMenuMixin.js index 1c2c0a9a..17c9339b 100644 --- a/static/js/components/ContextMenu/ModelContextMenuMixin.js +++ b/static/js/components/ContextMenu/ModelContextMenuMixin.js @@ -8,9 +8,12 @@ import { bulkManager } from '../../managers/BulkManager.js'; export const ModelContextMenuMixin = { // NSFW Selector methods initNSFWSelector() { - // Close button + // Remove any existing event listeners by cloning and replacing elements + // This is a simple way to ensure we don't have duplicate event listeners const closeBtn = this.nsfwSelector.querySelector('.close-nsfw-selector'); - closeBtn.addEventListener('click', () => { + const newCloseBtn = closeBtn.cloneNode(true); + closeBtn.parentNode.replaceChild(newCloseBtn, closeBtn); + newCloseBtn.addEventListener('click', () => { this.nsfwSelector.style.display = 'none'; this.resetNSFWSelectorState(); }); @@ -18,8 +21,12 @@ export const ModelContextMenuMixin = { // Level buttons const levelButtons = this.nsfwSelector.querySelectorAll('.nsfw-level-btn'); levelButtons.forEach(btn => { - btn.addEventListener('click', async () => { - const level = parseInt(btn.dataset.level); + // Remove any existing event listeners by cloning and replacing the button + const newBtn = btn.cloneNode(true); + btn.parentNode.replaceChild(newBtn, btn); + + newBtn.addEventListener('click', async () => { + const level = parseInt(newBtn.dataset.level); const mode = this.nsfwSelector.dataset.mode || 'single'; if (mode === 'bulk') { @@ -56,15 +63,24 @@ export const ModelContextMenuMixin = { }); }); - // Close when clicking outside - document.addEventListener('click', (e) => { + // Close when clicking outside - use a named function so we can remove it later + const outsideClickListener = (e) => { if (this.nsfwSelector.style.display === 'block' && !this.nsfwSelector.contains(e.target) && !e.target.closest('.context-menu-item[data-action="set-nsfw"], .context-menu-item[data-action="set-content-rating"]')) { this.nsfwSelector.style.display = 'none'; this.resetNSFWSelectorState(); } - }); + }; + + // Remove previous listener if it exists + if (this._outsideClickListener) { + document.removeEventListener('click', this._outsideClickListener); + } + + // Store and add new listener + this._outsideClickListener = outsideClickListener; + document.addEventListener('click', this._outsideClickListener); }, resetNSFWSelectorState() { diff --git a/static/js/components/ContextMenu/RecipeContextMenu.js b/static/js/components/ContextMenu/RecipeContextMenu.js index bb8b8e69..e5bb53b8 100644 --- a/static/js/components/ContextMenu/RecipeContextMenu.js +++ b/static/js/components/ContextMenu/RecipeContextMenu.js @@ -11,9 +11,10 @@ export class RecipeContextMenu extends BaseContextMenu { this.nsfwSelector = document.getElementById('nsfwLevelSelector'); this.modelType = 'recipe'; - // Initialize NSFW Level Selector events - if (this.nsfwSelector) { + // Initialize NSFW Level Selector events only if not already initialized + if (this.nsfwSelector && !this.nsfwSelector.dataset.initialized) { this.initNSFWSelector(); + this.nsfwSelector.dataset.initialized = 'true'; } }