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.
This commit is contained in:
2026-07-12 13:14:19 +02:00
parent 24c964c38d
commit f8894a9f3c
+111 -143
View File
@@ -446,161 +446,129 @@ export function renderUserStats() {
// Only re-bind event listeners if DOM was actually updated // Only re-bind event listeners if DOM was actually updated
if (domUpdated) { if (domUpdated) {
$('.rpg-editable-stat').on('blur', function () { $userStatsContainer.find('.rpg-editable-stat').on('blur', function () {
const field = $(this).data('field'); const field = $(this).data('field');
const mode = $(this).data('mode'); const mode = $(this).data('mode');
const maxValue = parseInt($(this).data('max')) || 100; const maxValue = parseInt($(this).data('max')) || 100;
const textValue = $(this).text().trim(); const textValue = $(this).text().trim();
let value; let value;
if (mode === 'number') { if (mode === 'number') {
// In number mode, parse "X/MAX" or just "X" const parts = textValue.split('/');
const parts = textValue.split('/'); value = parseInt(parts[0]);
value = parseInt(parts[0]); if (isNaN(value)) {
value = 0;
// Validate and clamp value between 0 and maxValue }
if (isNaN(value)) { value = Math.max(0, Math.min(maxValue, value));
value = 0; } 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 extensionSettings.userStats[field] = value;
if (isNaN(value)) { updateUserStatsData();
value = 0; 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.statNames[field] = value || extensionSettings.statNames[field];
extensionSettings.userStats[field] = value; saveSettings();
saveChatData();
renderUserStats();
});
// Update userStats data (maintains JSON or text format) // Add event listener for level editing
updateUserStatsData(); $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(); extensionSettings.level = value;
saveChatData(); saveSettings();
updateMessageSwipeData(); saveChatData();
updateMessageSwipeData();
renderUserStats();
});
// Re-render to update the bar and FAB widgets // Prevent line breaks in level field
renderUserStats(); $userStatsContainer.find('.rpg-level-value.rpg-editable').on('keydown', function (e) {
updateFabWidgets(); if (e.key === 'Enter') {
}); e.preventDefault();
$(this).blur();
}
});
// Add event listeners for mood/conditions editing // Add event listener for section lock icon clicks (support both click and touch)
$('.rpg-mood-emoji.rpg-editable').on('blur', function () { $userStatsContainer.find('.rpg-section-lock-icon').on('click touchend', function (e) {
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') {
e.preventDefault(); 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) setItemLock(trackerType, itemPath, !currentlyLocked);
$('.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);
// Toggle lock state const newIcon = !currentlyLocked ? '🔒' : '🔓';
setItemLock(trackerType, itemPath, !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 saveSettings();
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();
});
} // end if (domUpdated) } // end if (domUpdated)
} }