/** * 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'; // 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, '-'); } /** * 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 `
`; } /** * 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) => `
${escapeHtml(item)}
`).join(''); } else { // List view: full-width rows itemsHtml = items.map((item, index) => `
${escapeHtml(item)}
`).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) => `
${escapeHtml(item)}
`).join(''); } else { // List view: full-width rows itemsHtml = items.map((item, index) => `
${escapeHtml(item)}
`).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) => `
${escapeHtml(item)}
`).join(''); } else { // List view: full-width rows itemsHtml = items.map((item, index) => `
${escapeHtml(item)}
`).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(); } /** * 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; } // Get inventory data from settings const inventory = extensionSettings.userStats.inventory; // Get current render options (active tab, collapsed locations) const options = getInventoryRenderOptions(); // Generate HTML and update DOM const 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); }); } /** * 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; }