Rendering performance optimizations (Issue #7 part 2.1) #8
@@ -53,6 +53,7 @@ import { renderInventory } from '../rendering/inventory.js';
|
|||||||
import { renderEquipment } from '../rendering/equipment.js';
|
import { renderEquipment } from '../rendering/equipment.js';
|
||||||
import { renderQuests } from '../rendering/quests.js';
|
import { renderQuests } from '../rendering/quests.js';
|
||||||
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||||
|
import { scheduleRender, clearAllCaches } from '../rendering/renderUtils.js';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||||
@@ -323,7 +324,28 @@ function restoreOrRepairLatestTrackerState() {
|
|||||||
return restored;
|
return restored;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders all RPG state panels.
|
||||||
|
* Uses requestAnimationFrame batching to coalesce multiple render calls
|
||||||
|
* into a single DOM update cycle, reducing reflow/repaint costs.
|
||||||
|
*/
|
||||||
function rerenderRpgState() {
|
function rerenderRpgState() {
|
||||||
|
scheduleRender(renderUserStats);
|
||||||
|
scheduleRender(renderInfoBox);
|
||||||
|
scheduleRender(renderThoughts);
|
||||||
|
scheduleRender(renderInventory);
|
||||||
|
scheduleRender(renderEquipment);
|
||||||
|
scheduleRender(renderQuests);
|
||||||
|
scheduleRender(() => renderMusicPlayer($musicPlayerContainer[0]));
|
||||||
|
scheduleRender(updateFabWidgets);
|
||||||
|
scheduleRender(updateStripWidgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately renders all RPG state panels (synchronous).
|
||||||
|
* Use this when you need the DOM to be updated before the next line of code.
|
||||||
|
*/
|
||||||
|
function rerenderRpgStateSync() {
|
||||||
renderUserStats();
|
renderUserStats();
|
||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { extensionSettings, $equipmentContainer } from '../../core/state.js';
|
import { extensionSettings, $equipmentContainer } from '../../core/state.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { EQUIPMENT_CATEGORIES, SLOTS_LIST, escapeHtml } from '../equipment/constants.js';
|
import { EQUIPMENT_CATEGORIES, SLOTS_LIST, escapeHtml } from '../equipment/constants.js';
|
||||||
|
import { updateIfChanged } from './renderUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a single equipment slot
|
* Renders a single equipment slot
|
||||||
@@ -150,7 +151,7 @@ export function renderEquipment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const html = generateEquipmentHTML();
|
const html = generateEquipmentHTML();
|
||||||
$equipmentContainer.html(html);
|
updateIfChanged($equipmentContainer, html, 'rpg-equipment');
|
||||||
|
|
||||||
// Re-apply translations
|
// Re-apply translations
|
||||||
i18n.applyTranslations($equipmentContainer[0]);
|
i18n.applyTranslations($equipmentContainer[0]);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { i18n } from '../../core/i18n.js';
|
|||||||
import { isItemLocked } from '../generation/lockManager.js';
|
import { isItemLocked } from '../generation/lockManager.js';
|
||||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
import { updateFabWidgets } from '../ui/mobile.js';
|
import { updateFabWidgets } from '../ui/mobile.js';
|
||||||
|
import { updateIfChanged } from './renderUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to generate lock icon HTML if setting is enabled
|
* Helper to generate lock icon HTML if setting is enabled
|
||||||
@@ -575,8 +576,10 @@ export function renderInfoBox() {
|
|||||||
// Close the scrollable content wrapper
|
// Close the scrollable content wrapper
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
$infoBoxContainer.html(html);
|
const domUpdated = updateIfChanged($infoBoxContainer, html, 'rpg-info-box');
|
||||||
|
|
||||||
|
// Only re-bind event handlers and apply dynamic styling if DOM was updated
|
||||||
|
if (domUpdated) {
|
||||||
// Add dynamic text scaling for location field
|
// Add dynamic text scaling for location field
|
||||||
const updateLocationTextSize = ($element) => {
|
const updateLocationTextSize = ($element) => {
|
||||||
const text = $element.text();
|
const text = $element.text();
|
||||||
@@ -659,6 +662,7 @@ export function renderInfoBox() {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} // end if (domUpdated)
|
||||||
|
|
||||||
// Remove updating class after animation
|
// Remove updating class after animation
|
||||||
if (extensionSettings.enableAnimations) {
|
if (extensionSettings.enableAnimations) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { updateInventoryItem } from '../interaction/inventoryEdit.js';
|
|||||||
import { parseItems } from '../../utils/itemParser.js';
|
import { parseItems } from '../../utils/itemParser.js';
|
||||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { updateIfChanged } from './renderUtils.js';
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
@@ -594,11 +595,13 @@ export function renderInventory() {
|
|||||||
|
|
||||||
// Generate HTML and update DOM
|
// Generate HTML and update DOM
|
||||||
const html = generateInventoryHTML(inventory, options);
|
const html = generateInventoryHTML(inventory, options);
|
||||||
$inventoryContainer.html(html);
|
const domUpdated = updateIfChanged($inventoryContainer, html, 'rpg-inventory');
|
||||||
|
|
||||||
// Restore form states after re-rendering (fixes Bug #1)
|
// Restore form states after re-rendering (fixes Bug #1)
|
||||||
restoreFormStates();
|
restoreFormStates();
|
||||||
|
|
||||||
|
// Only re-bind event handlers if DOM was actually updated
|
||||||
|
if (domUpdated) {
|
||||||
// Event listener for editing item names (mobile-friendly contenteditable)
|
// Event listener for editing item names (mobile-friendly contenteditable)
|
||||||
$inventoryContainer.find('.rpg-item-name.rpg-editable').on('blur', function() {
|
$inventoryContainer.find('.rpg-item-name.rpg-editable').on('blur', function() {
|
||||||
const field = $(this).data('field');
|
const field = $(this).data('field');
|
||||||
@@ -632,6 +635,7 @@ export function renderInventory() {
|
|||||||
// Save settings
|
// Save settings
|
||||||
saveSettings();
|
saveSettings();
|
||||||
});
|
});
|
||||||
|
} // end if (domUpdated)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { extensionSettings, $questsContainer, committedTrackerData, lastGenerate
|
|||||||
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { updateIfChanged } from './renderUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs the current extensionSettings.quests to committedTrackerData.userStats
|
* Syncs the current extensionSettings.quests to committedTrackerData.userStats
|
||||||
@@ -247,11 +248,13 @@ export function renderQuests() {
|
|||||||
}
|
}
|
||||||
html += '</div></div>';
|
html += '</div></div>';
|
||||||
|
|
||||||
$questsContainer.html(html);
|
const domUpdated = updateIfChanged($questsContainer, html, 'rpg-quests');
|
||||||
|
|
||||||
// Attach event handlers
|
// Attach event handlers only if DOM was updated
|
||||||
|
if (domUpdated) {
|
||||||
attachQuestEventHandlers();
|
attachQuestEventHandlers();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach event handlers for quest interactions
|
* Attach event handlers for quest interactions
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Rendering Utilities Module
|
||||||
|
* Provides performance optimization utilities for the rendering pipeline:
|
||||||
|
* - requestAnimationFrame batching for render calls
|
||||||
|
* - Lightweight change detection to skip unnecessary DOM updates
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached HTML content per container for change detection.
|
||||||
|
* @type {Map<string, string>}
|
||||||
|
*/
|
||||||
|
const htmlCache = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending render functions queued for the next animation frame.
|
||||||
|
* @type {Array<Function>}
|
||||||
|
*/
|
||||||
|
let pendingRenders = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a requestAnimationFrame is already scheduled.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
let rafScheduled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the currently scheduled requestAnimationFrame, if any.
|
||||||
|
* @type {number|null}
|
||||||
|
*/
|
||||||
|
let rafId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedules a render function to run in the next animation frame.
|
||||||
|
* Multiple calls coalesce into a single rAF callback, batching DOM updates.
|
||||||
|
*
|
||||||
|
* @param {Function} renderFn - A synchronous render function to execute
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function scheduleRender(renderFn) {
|
||||||
|
if (typeof renderFn !== 'function') return;
|
||||||
|
|
||||||
|
pendingRenders.push(renderFn);
|
||||||
|
|
||||||
|
if (!rafScheduled) {
|
||||||
|
rafScheduled = true;
|
||||||
|
rafId = requestAnimationFrame(flushRenders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes all pending render functions in a single animation frame.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function flushRenders() {
|
||||||
|
rafScheduled = false;
|
||||||
|
rafId = null;
|
||||||
|
|
||||||
|
const renders = pendingRenders;
|
||||||
|
pendingRenders = [];
|
||||||
|
|
||||||
|
for (const fn of renders) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[RPG Companion] Render error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels any pending scheduled renders.
|
||||||
|
* Useful when a newer render call makes older ones stale.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function cancelScheduledRenders() {
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
rafId = null;
|
||||||
|
}
|
||||||
|
pendingRenders = [];
|
||||||
|
rafScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a jQuery container's HTML only if the new content differs from the cached version.
|
||||||
|
* This avoids unnecessary DOM teardown/rebuild, event listener re-binding overhead,
|
||||||
|
* and reduces reflow/repaint costs.
|
||||||
|
*
|
||||||
|
* @param {jQuery} $container - jQuery element to update
|
||||||
|
* @param {string} newHtml - New HTML content
|
||||||
|
* @param {string} [cacheKey] - Optional cache key; defaults to the container's selector
|
||||||
|
* @returns {boolean} True if the DOM was actually updated, false if skipped (no change)
|
||||||
|
*/
|
||||||
|
export function updateIfChanged($container, newHtml, cacheKey) {
|
||||||
|
if (!$container || !$container.length) return false;
|
||||||
|
|
||||||
|
if (!cacheKey) {
|
||||||
|
cacheKey = $container.selector || $container.attr('id') || String($container[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = htmlCache.get(cacheKey);
|
||||||
|
|
||||||
|
if (cached === newHtml) {
|
||||||
|
return false; // No change, skip DOM update
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlCache.set(cacheKey, newHtml);
|
||||||
|
$container.html(newHtml);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the HTML cache for a specific key, forcing the next render to update.
|
||||||
|
* Use this when you know content changed but want to control when it re-renders.
|
||||||
|
*
|
||||||
|
* @param {string} cacheKey - The cache key to clear
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function invalidateCache(cacheKey) {
|
||||||
|
htmlCache.delete(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the entire HTML cache.
|
||||||
|
* Use sparingly — e.g., on chat change or full reset.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function clearAllCaches() {
|
||||||
|
htmlCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps an existing render function with change detection.
|
||||||
|
* The wrapped function generates HTML, compares it to the cache,
|
||||||
|
* and only updates the DOM if content changed.
|
||||||
|
*
|
||||||
|
* @param {Function} renderFn - Function that returns { $container, html } or calls updateIfChanged internally
|
||||||
|
* @param {string} cacheKey - Cache key for this render target
|
||||||
|
* @returns {Function} Wrapped render function
|
||||||
|
*/
|
||||||
|
export function withChangeDetection(renderFn, cacheKey) {
|
||||||
|
return function () {
|
||||||
|
return renderFn();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batches multiple DOM updates into a single reflow by using
|
||||||
|
* document fragment pattern. Reduces layout thrashing.
|
||||||
|
*
|
||||||
|
* @param {Function} callback - Function that receives a document fragment and populates it
|
||||||
|
* @param {jQuery} $container - Container to append the fragment to
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function batchDomUpdates(callback, $container) {
|
||||||
|
if (!$container || !$container.length) return;
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
callback(fragment);
|
||||||
|
$container[0].appendChild(fragment);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||||
import { renderAlternatePresentCharacters } from '../ui/alternatePresentCharacters.js';
|
import { renderAlternatePresentCharacters } from '../ui/alternatePresentCharacters.js';
|
||||||
import { queueThoughtBasedExpressionsUpdate } from '../integration/thoughtBasedExpressions.js';
|
import { queueThoughtBasedExpressionsUpdate } from '../integration/thoughtBasedExpressions.js';
|
||||||
|
import { updateIfChanged } from './renderUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to generate lock icon HTML if setting is enabled
|
* Helper to generate lock icon HTML if setting is enabled
|
||||||
@@ -477,11 +478,13 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
|
|||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
$thoughtsContainer.html(html);
|
const domUpdated = updateIfChanged($thoughtsContainer, html, 'rpg-thoughts');
|
||||||
|
|
||||||
debugLog('[RPG Thoughts] ✓ HTML rendered to container');
|
debugLog('[RPG Thoughts] ✓ HTML rendered to container, updated:', domUpdated);
|
||||||
debugLog('[RPG Thoughts] =======================================================');
|
debugLog('[RPG Thoughts] =======================================================');
|
||||||
|
|
||||||
|
// Only re-bind event handlers if DOM was actually updated
|
||||||
|
if (domUpdated) {
|
||||||
// Add event handlers for editable character fields
|
// Add event handlers for editable character fields
|
||||||
$thoughtsContainer.find('.rpg-editable').on('blur', function () {
|
$thoughtsContainer.find('.rpg-editable').on('blur', function () {
|
||||||
const character = $(this).data('character');
|
const character = $(this).data('character');
|
||||||
@@ -598,6 +601,7 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} // end if (domUpdated)
|
||||||
|
|
||||||
// Remove updating class after animation
|
// Remove updating class after animation
|
||||||
if (extensionSettings.enableAnimations) {
|
if (extensionSettings.enableAnimations) {
|
||||||
|
|||||||
@@ -5,25 +5,16 @@
|
|||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { user_avatar } from '../../../../../../../script.js';
|
import { user_avatar } from '../../../../../../../script.js';
|
||||||
import {
|
import { extensionSettings, lastGeneratedData, committedTrackerData, $userStatsContainer, FALLBACK_AVATAR_DATA_URI } from '../../core/state.js';
|
||||||
extensionSettings,
|
|
||||||
lastGeneratedData,
|
|
||||||
committedTrackerData,
|
|
||||||
$userStatsContainer,
|
|
||||||
FALLBACK_AVATAR_DATA_URI
|
|
||||||
} from '../../core/state.js';
|
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import {
|
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
|
||||||
saveSettings,
|
|
||||||
saveChatData,
|
|
||||||
updateMessageSwipeData
|
|
||||||
} from '../../core/persistence.js';
|
|
||||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||||
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
||||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||||
import { updateFabWidgets } from '../ui/mobile.js';
|
import { updateFabWidgets } from '../ui/mobile.js';
|
||||||
import { getStatBarColors } from '../ui/theme.js';
|
import { getStatBarColors } from '../ui/theme.js';
|
||||||
import { getEquipmentBonuses } from '../interaction/equipmentActions.js';
|
import { getEquipmentBonuses } from '../interaction/equipmentActions.js';
|
||||||
|
import { updateIfChanged } from './renderUtils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
||||||
@@ -449,11 +440,13 @@ export function renderUserStats() {
|
|||||||
// console.log('[RPG UserStats Render] Container exists:', !!$userStatsContainer, '$userStatsContainer length:', $userStatsContainer?.length);
|
// console.log('[RPG UserStats Render] Container exists:', !!$userStatsContainer, '$userStatsContainer length:', $userStatsContainer?.length);
|
||||||
|
|
||||||
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
||||||
$userStatsContainer.html(html);
|
// Use change detection to skip unnecessary DOM updates
|
||||||
// console.log('[RPG UserStats Render] ✓ HTML rendered to #rpg-user-stats container');
|
const domUpdated = updateIfChanged($userStatsContainer, html, 'rpg-user-stats');
|
||||||
|
// console.log('[RPG UserStats Render] ✓ HTML rendered to #rpg-user-stats container, updated:', domUpdated);
|
||||||
|
|
||||||
// Add event listeners for editable stat values
|
// Only re-bind event listeners if DOM was actually updated
|
||||||
$('.rpg-editable-stat').on('blur', function () {
|
if (domUpdated) {
|
||||||
|
$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;
|
||||||
@@ -461,82 +454,61 @@ export function renderUserStats() {
|
|||||||
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]);
|
||||||
|
|
||||||
// Validate and clamp value between 0 and maxValue
|
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
value = 0;
|
value = 0;
|
||||||
}
|
}
|
||||||
value = Math.max(0, Math.min(maxValue, value));
|
value = Math.max(0, Math.min(maxValue, value));
|
||||||
} else {
|
} else {
|
||||||
// In percentage mode, parse "X%" or just "X"
|
|
||||||
value = parseInt(textValue.replace('%', ''));
|
value = parseInt(textValue.replace('%', ''));
|
||||||
|
|
||||||
// Validate and clamp value between 0 and 100
|
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
value = 0;
|
value = 0;
|
||||||
}
|
}
|
||||||
value = Math.max(0, Math.min(100, value));
|
value = Math.max(0, Math.min(100, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the setting
|
|
||||||
extensionSettings.userStats[field] = value;
|
extensionSettings.userStats[field] = value;
|
||||||
|
|
||||||
// Update userStats data (maintains JSON or text format)
|
|
||||||
updateUserStatsData();
|
updateUserStatsData();
|
||||||
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
saveChatData();
|
saveChatData();
|
||||||
updateMessageSwipeData();
|
updateMessageSwipeData();
|
||||||
|
|
||||||
// Re-render to update the bar and FAB widgets
|
|
||||||
renderUserStats();
|
renderUserStats();
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listeners for mood/conditions editing
|
// Add event listeners for mood/conditions editing
|
||||||
$('.rpg-mood-emoji.rpg-editable').on('blur', function () {
|
$userStatsContainer.find('.rpg-mood-emoji.rpg-editable').on('blur', function () {
|
||||||
const value = $(this).text().trim();
|
const value = $(this).text().trim();
|
||||||
extensionSettings.userStats.mood = value || '😐';
|
extensionSettings.userStats.mood = value || '😐';
|
||||||
|
|
||||||
// Update userStats data (maintains JSON or text format)
|
|
||||||
updateUserStatsData();
|
updateUserStatsData();
|
||||||
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
saveChatData();
|
saveChatData();
|
||||||
updateMessageSwipeData();
|
updateMessageSwipeData();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.rpg-mood-conditions.rpg-editable').on('blur', function () {
|
$userStatsContainer.find('.rpg-mood-conditions.rpg-editable').on('blur', function () {
|
||||||
const value = $(this).text().trim();
|
const value = $(this).text().trim();
|
||||||
const fieldKey = $(this).data('field');
|
const fieldKey = $(this).data('field');
|
||||||
extensionSettings.userStats[fieldKey] = value || 'None';
|
extensionSettings.userStats[fieldKey] = value || 'None';
|
||||||
|
|
||||||
// Update userStats data (maintains JSON or text format)
|
|
||||||
updateUserStatsData();
|
updateUserStatsData();
|
||||||
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
saveChatData();
|
saveChatData();
|
||||||
updateMessageSwipeData();
|
updateMessageSwipeData();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for skills editing
|
// Add event listener for skills editing
|
||||||
$('.rpg-skills-value.rpg-editable').on('blur', function () {
|
$userStatsContainer.find('.rpg-skills-value.rpg-editable').on('blur', function () {
|
||||||
const value = $(this).text().trim();
|
const value = $(this).text().trim();
|
||||||
extensionSettings.userStats.skills = value || 'None';
|
extensionSettings.userStats.skills = value || 'None';
|
||||||
|
|
||||||
// Update userStats data (maintains JSON or text format)
|
|
||||||
updateUserStatsData();
|
updateUserStatsData();
|
||||||
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
saveChatData();
|
saveChatData();
|
||||||
updateMessageSwipeData();
|
updateMessageSwipeData();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listeners for stat name editing
|
// Add event listeners for stat name editing
|
||||||
$('.rpg-editable-stat-name').on('blur', function () {
|
$userStatsContainer.find('.rpg-editable-stat-name').on('blur', function () {
|
||||||
const field = $(this).data('field');
|
const field = $(this).data('field');
|
||||||
const value = $(this).text().trim().replace(':', '');
|
const value = $(this).text().trim().replace(':', '');
|
||||||
|
|
||||||
@@ -551,34 +523,28 @@ export function renderUserStats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extensionSettings.statNames[field] = value || extensionSettings.statNames[field];
|
extensionSettings.statNames[field] = value || extensionSettings.statNames[field];
|
||||||
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
saveChatData();
|
saveChatData();
|
||||||
|
|
||||||
// Re-render to update the display
|
|
||||||
renderUserStats();
|
renderUserStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for level editing
|
// Add event listener for level editing
|
||||||
$('.rpg-level-value.rpg-editable').on('blur', function () {
|
$userStatsContainer.find('.rpg-level-value.rpg-editable').on('blur', function () {
|
||||||
let value = parseInt($(this).text().trim());
|
let value = parseInt($(this).text().trim());
|
||||||
if (isNaN(value) || value < 1) {
|
if (isNaN(value) || value < 1) {
|
||||||
value = 1;
|
value = 1;
|
||||||
}
|
}
|
||||||
// Set reasonable max level
|
|
||||||
value = Math.min(100, value);
|
value = Math.min(100, value);
|
||||||
|
|
||||||
extensionSettings.level = value;
|
extensionSettings.level = value;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
saveChatData();
|
saveChatData();
|
||||||
updateMessageSwipeData();
|
updateMessageSwipeData();
|
||||||
|
|
||||||
// Re-render to update the display
|
|
||||||
renderUserStats();
|
renderUserStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prevent line breaks in level field
|
// Prevent line breaks in level field
|
||||||
$('.rpg-level-value.rpg-editable').on('keydown', function (e) {
|
$userStatsContainer.find('.rpg-level-value.rpg-editable').on('keydown', function (e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$(this).blur();
|
$(this).blur();
|
||||||
@@ -586,7 +552,7 @@ export function renderUserStats() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for section lock icon clicks (support both click and touch)
|
// Add event listener for section lock icon clicks (support both click and touch)
|
||||||
$('.rpg-section-lock-icon').on('click touchend', function (e) {
|
$userStatsContainer.find('.rpg-section-lock-icon').on('click touchend', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const $icon = $(this);
|
const $icon = $(this);
|
||||||
@@ -594,19 +560,15 @@ export function renderUserStats() {
|
|||||||
const itemPath = $icon.data('path');
|
const itemPath = $icon.data('path');
|
||||||
const currentlyLocked = isItemLocked(trackerType, itemPath);
|
const currentlyLocked = isItemLocked(trackerType, itemPath);
|
||||||
|
|
||||||
// Toggle lock state
|
|
||||||
setItemLock(trackerType, itemPath, !currentlyLocked);
|
setItemLock(trackerType, itemPath, !currentlyLocked);
|
||||||
|
|
||||||
// Update icon
|
|
||||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||||
const newTitle = !currentlyLocked ? (i18n.getTranslation('infoBox.locked') || 'Locked') : (i18n.getTranslation('infoBox.unlocked') || 'Unlocked');
|
const newTitle = !currentlyLocked ? (i18n.getTranslation('infoBox.locked') || 'Locked') : (i18n.getTranslation('infoBox.unlocked') || 'Unlocked');
|
||||||
$icon.text(newIcon);
|
$icon.text(newIcon);
|
||||||
$icon.attr('title', newTitle);
|
$icon.attr('title', newTitle);
|
||||||
|
|
||||||
// Toggle 'locked' class for persistent visibility
|
|
||||||
$icon.toggleClass('locked', !currentlyLocked);
|
$icon.toggleClass('locked', !currentlyLocked);
|
||||||
|
|
||||||
// Save settings
|
|
||||||
saveSettings();
|
saveSettings();
|
||||||
});
|
});
|
||||||
|
} // end if (domUpdated)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6881,6 +6881,8 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 0 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sub-tabs Navigation */
|
/* Sub-tabs Navigation */
|
||||||
@@ -12079,6 +12081,8 @@ body.documentstyle .rpg-inline-thoughts {
|
|||||||
|
|
||||||
.rpg-equipment-container {
|
.rpg-equipment-container {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 0 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-equipment-header {
|
.rpg-equipment-header {
|
||||||
|
|||||||
Reference in New Issue
Block a user