From f8894a9f3cf4145259b4d79830f8d08d006daeed Mon Sep 17 00:00:00 2001 From: ARIA Date: Sun, 12 Jul 2026 13:14:19 +0200 Subject: [PATCH] Fix: Scope event handlers to container in userStats.js Fixes critical bug identified by Zephyre_Review: event handlers used global jQuery selectors instead of container-scoped selectors, causing potential event handler leaks and scope mismatches. --- src/systems/rendering/userStats.js | 254 +++++++++++++---------------- 1 file changed, 111 insertions(+), 143 deletions(-) diff --git a/src/systems/rendering/userStats.js b/src/systems/rendering/userStats.js index 6e311b2..07ea2eb 100644 --- a/src/systems/rendering/userStats.js +++ b/src/systems/rendering/userStats.js @@ -446,161 +446,129 @@ export function renderUserStats() { // Only re-bind event listeners if DOM was actually updated if (domUpdated) { - $('.rpg-editable-stat').on('blur', function () { - const field = $(this).data('field'); - const mode = $(this).data('mode'); - const maxValue = parseInt($(this).data('max')) || 100; - const textValue = $(this).text().trim(); - let value; + $userStatsContainer.find('.rpg-editable-stat').on('blur', function () { + const field = $(this).data('field'); + const mode = $(this).data('mode'); + const maxValue = parseInt($(this).data('max')) || 100; + const textValue = $(this).text().trim(); + let value; - if (mode === 'number') { - // In number mode, parse "X/MAX" or just "X" - const parts = textValue.split('/'); - value = parseInt(parts[0]); - - // Validate and clamp value between 0 and maxValue - if (isNaN(value)) { - value = 0; + if (mode === 'number') { + const parts = textValue.split('/'); + value = parseInt(parts[0]); + if (isNaN(value)) { + value = 0; + } + value = Math.max(0, Math.min(maxValue, value)); + } else { + value = parseInt(textValue.replace('%', '')); + if (isNaN(value)) { + value = 0; + } + value = Math.max(0, Math.min(100, value)); } - value = Math.max(0, Math.min(maxValue, value)); - } else { - // In percentage mode, parse "X%" or just "X" - value = parseInt(textValue.replace('%', '')); - // Validate and clamp value between 0 and 100 - if (isNaN(value)) { - value = 0; + extensionSettings.userStats[field] = value; + updateUserStatsData(); + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + renderUserStats(); + updateFabWidgets(); + }); + + // Add event listeners for mood/conditions editing + $userStatsContainer.find('.rpg-mood-emoji.rpg-editable').on('blur', function () { + const value = $(this).text().trim(); + extensionSettings.userStats.mood = value || '😐'; + updateUserStatsData(); + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + }); + + $userStatsContainer.find('.rpg-mood-conditions.rpg-editable').on('blur', function () { + const value = $(this).text().trim(); + const fieldKey = $(this).data('field'); + extensionSettings.userStats[fieldKey] = value || 'None'; + updateUserStatsData(); + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + }); + + // Add event listener for skills editing + $userStatsContainer.find('.rpg-skills-value.rpg-editable').on('blur', function () { + const value = $(this).text().trim(); + extensionSettings.userStats.skills = value || 'None'; + updateUserStatsData(); + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + }); + + // Add event listeners for stat name editing + $userStatsContainer.find('.rpg-editable-stat-name').on('blur', function () { + const field = $(this).data('field'); + const value = $(this).text().trim().replace(':', ''); + + if (!extensionSettings.statNames) { + extensionSettings.statNames = { + health: 'Health', + satiety: 'Satiety', + energy: 'Energy', + hygiene: 'Hygiene', + arousal: 'Arousal' + }; } - value = Math.max(0, Math.min(100, value)); - } - // Update the setting - extensionSettings.userStats[field] = value; + extensionSettings.statNames[field] = value || extensionSettings.statNames[field]; + saveSettings(); + saveChatData(); + renderUserStats(); + }); - // Update userStats data (maintains JSON or text format) - updateUserStatsData(); + // Add event listener for level editing + $userStatsContainer.find('.rpg-level-value.rpg-editable').on('blur', function () { + let value = parseInt($(this).text().trim()); + if (isNaN(value) || value < 1) { + value = 1; + } + value = Math.min(100, value); - saveSettings(); - saveChatData(); - updateMessageSwipeData(); + extensionSettings.level = value; + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + renderUserStats(); + }); - // Re-render to update the bar and FAB widgets - renderUserStats(); - updateFabWidgets(); - }); + // Prevent line breaks in level field + $userStatsContainer.find('.rpg-level-value.rpg-editable').on('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + $(this).blur(); + } + }); - // Add event listeners for mood/conditions editing - $('.rpg-mood-emoji.rpg-editable').on('blur', function () { - const value = $(this).text().trim(); - extensionSettings.userStats.mood = value || '😐'; - - // Update userStats data (maintains JSON or text format) - updateUserStatsData(); - - saveSettings(); - saveChatData(); - updateMessageSwipeData(); - }); - - $('.rpg-mood-conditions.rpg-editable').on('blur', function () { - const value = $(this).text().trim(); - const fieldKey = $(this).data('field'); - extensionSettings.userStats[fieldKey] = value || 'None'; - - // Update userStats data (maintains JSON or text format) - updateUserStatsData(); - - saveSettings(); - saveChatData(); - updateMessageSwipeData(); - }); - - // Add event listener for skills editing - $('.rpg-skills-value.rpg-editable').on('blur', function () { - const value = $(this).text().trim(); - extensionSettings.userStats.skills = value || 'None'; - - // Update userStats data (maintains JSON or text format) - updateUserStatsData(); - - saveSettings(); - saveChatData(); - updateMessageSwipeData(); - }); - - // Add event listeners for stat name editing - $('.rpg-editable-stat-name').on('blur', function () { - const field = $(this).data('field'); - const value = $(this).text().trim().replace(':', ''); - - if (!extensionSettings.statNames) { - extensionSettings.statNames = { - health: 'Health', - satiety: 'Satiety', - energy: 'Energy', - hygiene: 'Hygiene', - arousal: 'Arousal' - }; - } - - extensionSettings.statNames[field] = value || extensionSettings.statNames[field]; - - saveSettings(); - saveChatData(); - - // Re-render to update the display - renderUserStats(); - }); - - // Add event listener for level editing - $('.rpg-level-value.rpg-editable').on('blur', function () { - let value = parseInt($(this).text().trim()); - if (isNaN(value) || value < 1) { - value = 1; - } - // Set reasonable max level - value = Math.min(100, value); - - extensionSettings.level = value; - saveSettings(); - saveChatData(); - updateMessageSwipeData(); - - // Re-render to update the display - renderUserStats(); - }); - - // Prevent line breaks in level field - $('.rpg-level-value.rpg-editable').on('keydown', function (e) { - if (e.key === 'Enter') { + // Add event listener for section lock icon clicks (support both click and touch) + $userStatsContainer.find('.rpg-section-lock-icon').on('click touchend', function (e) { e.preventDefault(); - $(this).blur(); - } - }); + e.stopPropagation(); + const $icon = $(this); + const trackerType = $icon.data('tracker'); + const itemPath = $icon.data('path'); + const currentlyLocked = isItemLocked(trackerType, itemPath); - // Add event listener for section lock icon clicks (support both click and touch) - $('.rpg-section-lock-icon').on('click touchend', function (e) { - e.preventDefault(); - e.stopPropagation(); - const $icon = $(this); - const trackerType = $icon.data('tracker'); - const itemPath = $icon.data('path'); - const currentlyLocked = isItemLocked(trackerType, itemPath); + setItemLock(trackerType, itemPath, !currentlyLocked); - // Toggle lock state - setItemLock(trackerType, itemPath, !currentlyLocked); + const newIcon = !currentlyLocked ? '🔒' : '🔓'; + const newTitle = !currentlyLocked ? (i18n.getTranslation('infoBox.locked') || 'Locked') : (i18n.getTranslation('infoBox.unlocked') || 'Unlocked'); + $icon.text(newIcon); + $icon.attr('title', newTitle); + $icon.toggleClass('locked', !currentlyLocked); - // Update icon - const newIcon = !currentlyLocked ? '🔒' : '🔓'; - const newTitle = !currentlyLocked ? (i18n.getTranslation('infoBox.locked') || 'Locked') : (i18n.getTranslation('infoBox.unlocked') || 'Unlocked'); - $icon.text(newIcon); - $icon.attr('title', newTitle); - - // Toggle 'locked' class for persistent visibility - $icon.toggleClass('locked', !currentlyLocked); - - // Save settings - saveSettings(); - }); + saveSettings(); + }); } // end if (domUpdated) }