/** * Inventory Rendering Module * Handles UI rendering for inventory v2 system */ import { extensionSettings, $inventoryContainer } from '../../core/state.js'; import { getInventoryRenderOptions, restoreFormStates } from '../interaction/inventoryActions.js'; import { updateInventoryItem } from '../interaction/inventoryEdit.js'; import { parseItems } from '../../utils/itemParser.js'; import { i18n } from '../../core/i18n.js'; import { itemHasLinkedSkills, navigateToLinkedSkills } from './skills.js'; // Type imports /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ /** * Converts a location name to a safe ID for use in HTML element IDs. * Must match the logic used in inventoryActions.js. * @param {string} locationName - The location name * @returns {string} Safe ID string */ export function getLocationId(locationName) { // Remove all non-alphanumeric characters except spaces, then replace spaces with hyphens return locationName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '-'); } /** * Generates the skill link indicator for an inventory item * @param {string} itemName - The item name * @returns {string} HTML string for the link indicator (empty if no links) */ function getSkillLinkIndicator(itemName) { if (!extensionSettings.enableItemSkillLinks || !extensionSettings.showSkills) { return ''; } if (itemHasLinkedSkills(itemName)) { return ``; } return ''; } /** * Renders the inventory sub-tab navigation (On Person, Stored, Assets) * @param {string} activeTab - Currently active sub-tab ('onPerson', 'stored', 'assets') * @returns {string} HTML for sub-tab navigation */ export function renderInventorySubTabs(activeTab = 'onPerson') { return `
`; } /** * Gets the description for an item from structured inventory data * @param {string} field - Field type ('onPerson', 'stored', 'assets', 'simplified') * @param {number} index - Item index * @param {string} [location] - Location name for stored items * @returns {string} Item description or empty string */ function getItemDescription(field, index, location = null) { const inv3 = extensionSettings.inventoryV3; if (!inv3) return ''; let items; if (field === 'onPerson') { items = inv3.onPerson; } else if (field === 'assets') { items = inv3.assets; } else if (field === 'stored' && location) { items = inv3.stored?.[location]; } else if (field === 'simplified') { items = inv3.simplified; } if (!items || !Array.isArray(items) || !items[index]) return ''; const item = items[index]; return (typeof item === 'object' ? item.description : '') || ''; } /** * Renders the "On Person" inventory view with list or grid display * @param {string} onPersonItems - Current on-person items (comma-separated string) * @param {string} viewMode - View mode ('list' or 'grid') * @returns {string} HTML for on-person view with items and add button */ export function renderOnPersonView(onPersonItems, viewMode = 'list') { const items = parseItems(onPersonItems); let itemsHtml = ''; if (items.length === 0) { itemsHtml = `
${i18n.getTranslation('inventory.onPerson.empty')}
`; } else { if (viewMode === 'grid') { // Grid view: card-style items itemsHtml = items.map((item, index) => { const desc = getItemDescription('onPerson', index); return `
${escapeHtml(item)} ${getSkillLinkIndicator(item)}
${escapeHtml(desc)}
`}).join(''); } else { // List view: full-width rows itemsHtml = items.map((item, index) => { const desc = getItemDescription('onPerson', index); return `
${escapeHtml(item)} ${getSkillLinkIndicator(item)}
${escapeHtml(desc)}
`}).join(''); } } const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view'; return `

${i18n.getTranslation('inventory.onPerson.title')}

${itemsHtml}
`; } /** * Renders the "Stored" inventory view with collapsible locations and list/grid views * @param {Object.} stored - Stored items by location * @param {string[]} collapsedLocations - Array of collapsed location names * @param {string} viewMode - View mode ('list' or 'grid') * @returns {string} HTML for stored inventory with all locations */ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'list') { const locations = Object.keys(stored || {}); let html = `

${i18n.getTranslation('inventory.stored.title')}

`; if (locations.length === 0) { html += `
${i18n.getTranslation('inventory.stored.empty')}
`; } else { for (const location of locations) { const itemString = stored[location]; const items = parseItems(itemString); const isCollapsed = collapsedLocations.includes(location); const locationId = getLocationId(location); let itemsHtml = ''; if (items.length === 0) { itemsHtml = `
${i18n.getTranslation('inventory.stored.noItems')}
`; } else { if (viewMode === 'grid') { // Grid view: card-style items itemsHtml = items.map((item, index) => { const desc = getItemDescription('stored', index, location); return `
${escapeHtml(item)} ${getSkillLinkIndicator(item)}
${escapeHtml(desc)}
`}).join(''); } else { // List view: full-width rows itemsHtml = items.map((item, index) => { const desc = getItemDescription('stored', index, location); return `
${escapeHtml(item)} ${getSkillLinkIndicator(item)}
${escapeHtml(desc)}
`}).join(''); } } const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view'; html += `
${escapeHtml(location)}
${itemsHtml}
`; } } html += `
`; return html; } /** * Renders the "Assets" inventory view with list or grid display * @param {string} assets - Current assets (vehicles, property, equipment) * @param {string} viewMode - View mode ('list' or 'grid') * @returns {string} HTML for assets view with items and add button */ export function renderAssetsView(assets, viewMode = 'list') { const items = parseItems(assets); let itemsHtml = ''; if (items.length === 0) { itemsHtml = `
${i18n.getTranslation('inventory.assets.empty')}
`; } else { if (viewMode === 'grid') { // Grid view: card-style items itemsHtml = items.map((item, index) => { const desc = getItemDescription('assets', index); return `
${escapeHtml(item)} ${getSkillLinkIndicator(item)}
${escapeHtml(desc)}
`}).join(''); } else { // List view: full-width rows itemsHtml = items.map((item, index) => { const desc = getItemDescription('assets', index); return `
${escapeHtml(item)} ${getSkillLinkIndicator(item)}
${escapeHtml(desc)}
`}).join(''); } } const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view'; return `

${i18n.getTranslation('inventory.assets.title')}

${itemsHtml}
${i18n.getTranslation('inventory.assets.description')}
`; } /** * Generates inventory HTML (internal helper) * @param {InventoryV2} inventory - Inventory data to render * @param {Object} options - Rendering options * @param {string} options.activeSubTab - Currently active sub-tab ('onPerson', 'stored', 'assets') * @param {string[]} options.collapsedLocations - Collapsed storage locations * @returns {string} Complete HTML for inventory tab content */ function generateInventoryHTML(inventory, options = {}) { const { activeSubTab = 'onPerson', collapsedLocations = [] } = options; // Handle legacy v1 format - convert to v2 for display let v2Inventory = inventory; if (typeof inventory === 'string') { v2Inventory = { version: 2, onPerson: inventory, stored: {}, assets: 'None' }; } // Ensure v2 structure has all required fields if (!v2Inventory || typeof v2Inventory !== 'object') { v2Inventory = { version: 2, onPerson: 'None', stored: {}, assets: 'None' }; } // Additional safety check: ensure required properties exist and are correct type if (!v2Inventory.onPerson || typeof v2Inventory.onPerson !== 'string') { v2Inventory.onPerson = 'None'; } if (!v2Inventory.stored || typeof v2Inventory.stored !== 'object' || Array.isArray(v2Inventory.stored)) { v2Inventory.stored = {}; } if (!v2Inventory.assets || typeof v2Inventory.assets !== 'string') { v2Inventory.assets = 'None'; } let html = `
${renderInventorySubTabs(activeSubTab)}
`; // Get view modes from settings (default to 'list') const viewModes = extensionSettings.inventoryViewModes || { onPerson: 'list', stored: 'list', assets: 'list' }; // Render the active view switch (activeSubTab) { case 'onPerson': html += renderOnPersonView(v2Inventory.onPerson, viewModes.onPerson); break; case 'stored': html += renderStoredView(v2Inventory.stored, collapsedLocations, viewModes.stored); break; case 'assets': html += renderAssetsView(v2Inventory.assets, viewModes.assets); break; default: html += renderOnPersonView(v2Inventory.onPerson, viewModes.onPerson); } html += `
`; return html; } /** * Updates the inventory display in the DOM (used by inventoryActions) * @param {string} containerId - ID of container element to update * @param {Object} options - Rendering options (passed to generateInventoryHTML) */ export function updateInventoryDisplay(containerId, options = {}) { const container = document.getElementById(containerId); if (!container) { console.warn(`[RPG Companion] Inventory container not found: ${containerId}`); return; } const inventory = extensionSettings.userStats.inventory; const html = generateInventoryHTML(inventory, options); container.innerHTML = html; // Restore form states after re-rendering restoreFormStates(); } /** * Renders the simplified (single-list) inventory view * Used when useSimplifiedInventory setting is enabled * @param {string} itemsString - All items as a comma-separated string * @param {string} viewMode - View mode ('list' or 'grid') * @returns {string} HTML for simplified inventory view */ export function renderSimplifiedInventoryView(itemsString, viewMode = 'list') { const items = parseItems(itemsString); let itemsHtml = ''; if (items.length === 0) { itemsHtml = `
${i18n.getTranslation('inventory.simplified.empty')}
`; } else { if (viewMode === 'grid') { // Grid view: card-style items (same as onPerson) itemsHtml = items.map((item, index) => { const desc = getItemDescription('simplified', index); return `
${escapeHtml(item)} ${getSkillLinkIndicator(item)}
${escapeHtml(desc)}
`}).join(''); } else { // List view: full-width rows (same as onPerson) itemsHtml = items.map((item, index) => { const desc = getItemDescription('simplified', index); return `
${escapeHtml(item)} ${getSkillLinkIndicator(item)}
${escapeHtml(desc)}
`}).join(''); } } const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view'; return `

${i18n.getTranslation('inventory.simplified.title')}

${itemsHtml}
`; } /** * Checks if we have structured inventory data (v3 format) * @returns {boolean} */ function hasStructuredInventory() { const inv = extensionSettings.inventoryV3; return inv && ( (inv.onPerson && inv.onPerson.length > 0) || (inv.assets && inv.assets.length > 0) || (inv.stored && Object.keys(inv.stored).length > 0) || (inv.simplified && inv.simplified.length > 0) ); } /** * Main inventory rendering function (matches pattern of other render functions) * Gets data from state/settings and updates DOM directly. * Call this after AI generation, character changes, or swipes. */ export function renderInventory() { // Early return if container doesn't exist or section is hidden if (!$inventoryContainer || !extensionSettings.showInventory) { return; } let html; // Convert structured inventory (v3) to legacy format if present // This ensures we always use the original renderer let inventory = extensionSettings.userStats.inventory; if (hasStructuredInventory()) { const inv = extensionSettings.inventoryV3; // Convert structured items to comma-separated strings const itemsToString = (items) => { if (!items || items.length === 0) return 'None'; return items.map(i => typeof i === 'string' ? i : i.name).join(', '); }; inventory = { version: 2, onPerson: itemsToString(inv.onPerson), stored: Object.fromEntries( Object.entries(inv.stored || {}).map(([k, v]) => [k, itemsToString(v)]) ), assets: itemsToString(inv.assets), // For simplified mode items: itemsToString(inv.simplified) }; } // Check if we should render simplified inventory if (extensionSettings.useSimplifiedInventory) { const itemsString = inventory.items || inventory.onPerson || 'None'; const viewModes = extensionSettings.inventoryViewModes || {}; const viewMode = viewModes.simplified || viewModes.onPerson || 'list'; html = renderSimplifiedInventoryView(itemsString, viewMode); } else { const options = getInventoryRenderOptions(); html = generateInventoryHTML(inventory, options); } $inventoryContainer.html(html); // Restore form states after re-rendering (fixes Bug #1) restoreFormStates(); // Event listener for editing item names (mobile-friendly contenteditable) $inventoryContainer.find('.rpg-item-name.rpg-editable').on('blur', function() { const field = $(this).data('field'); const index = parseInt($(this).data('index')); const location = $(this).data('location'); const newName = $(this).text().trim(); updateInventoryItem(field, index, newName, location); }); // Event listener for editing item descriptions (structured mode) $inventoryContainer.find('.rpg-item-description.rpg-editable').on('blur', function() { const field = $(this).data('field'); const index = parseInt($(this).data('index')); const location = $(this).data('location'); const newDesc = $(this).text().trim(); updateStructuredItemDescription(field, index, newDesc, location); }); } /** * Updates an item's description in structured inventory * @param {string} field - 'onPerson', 'stored', or 'assets' * @param {number} index - Item index * @param {string} newDescription - New description * @param {string} [location] - Location for stored items */ function updateStructuredItemDescription(field, index, newDescription, location) { const inv = extensionSettings.inventoryV3; if (!inv) return; let item; if (field === 'onPerson' && inv.onPerson?.[index]) { item = inv.onPerson[index]; } else if (field === 'assets' && inv.assets?.[index]) { item = inv.assets[index]; } else if (field === 'stored' && location && inv.stored?.[location]?.[index]) { item = inv.stored[location][index]; } if (item) { item.description = newDescription; // Save changes import('../../core/persistence.js').then(({ saveSettings, saveChatData }) => { saveSettings(); saveChatData(); }); } } /** * Escapes HTML special characters to prevent XSS * @param {string} text - Text to escape * @returns {string} Escaped text */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }