diff --git a/src/core/state.js b/src/core/state.js index db73901..295f611 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -61,7 +61,12 @@ export let extensionSettings = { cha: 10 }, lastDiceRoll: null, // Store last dice roll result - collapsedInventoryLocations: [] // Array of collapsed storage location names + collapsedInventoryLocations: [], // Array of collapsed storage location names + inventoryViewModes: { + onPerson: 'list', // 'list' or 'grid' view mode for On Person section + stored: 'list', // 'list' or 'grid' view mode for Stored section + assets: 'list' // 'list' or 'grid' view mode for Assets section + } }; /** diff --git a/src/systems/interaction/inventoryActions.js b/src/systems/interaction/inventoryActions.js index 2ea5c5e..80934c0 100644 --- a/src/systems/interaction/inventoryActions.js +++ b/src/systems/interaction/inventoryActions.js @@ -7,6 +7,7 @@ import { extensionSettings, lastGeneratedData } from '../../core/state.js'; import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js'; import { buildInventorySummary } from '../generation/promptBuilder.js'; import { renderInventory } from '../rendering/inventory.js'; +import { parseItems, serializeItems } from '../../utils/itemParser.js'; // Type imports /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ @@ -43,64 +44,142 @@ function updateLastGeneratedDataInventory() { } /** - * Handles blur event for contenteditable "On Person" field. - * Saves changes when user finishes editing. - * @param {HTMLElement} element - The contenteditable element + * Shows the inline form for adding a new item. + * @param {string} field - Field name ('onPerson', 'stored', 'assets') + * @param {string} [location] - Location name (required for 'stored' field) */ -export function handleOnPersonBlur(element) { - const inventory = extensionSettings.userStats.inventory; - const newValue = element.textContent.trim() || 'None'; +export function showAddItemForm(field, location) { + let formId; + let inputId; - // Only save if value actually changed - if (newValue !== inventory.onPerson) { - inventory.onPerson = newValue; - - updateLastGeneratedDataInventory(); - saveSettings(); - saveChatData(); - updateMessageSwipeData(); + if (field === 'stored') { + const locationId = location.replace(/\s+/g, '-'); + formId = `rpg-add-item-form-stored-${locationId}`; + inputId = `.rpg-location-item-input[data-location="${location}"]`; + } else { + formId = `rpg-add-item-form-${field}`; + inputId = `#rpg-new-item-${field}`; } + + const form = $(`#${formId}`); + const input = $(inputId); + + form.show(); + input.val('').focus(); } /** - * Handles blur event for contenteditable stored location field. - * Saves changes when user finishes editing. - * @param {HTMLElement} element - The contenteditable element - * @param {string} locationName - Name of the storage location + * Hides the inline form for adding a new item. + * @param {string} field - Field name ('onPerson', 'stored', 'assets') + * @param {string} [location] - Location name (required for 'stored' field) */ -export function handleStoredLocationBlur(element, locationName) { - const inventory = extensionSettings.userStats.inventory; - const newValue = element.textContent.trim() || 'None'; +export function hideAddItemForm(field, location) { + let formId; + let inputId; - // Only save if value actually changed - if (newValue !== inventory.stored[locationName]) { - inventory.stored[locationName] = newValue; - - updateLastGeneratedDataInventory(); - saveSettings(); - saveChatData(); - updateMessageSwipeData(); + if (field === 'stored') { + const locationId = location.replace(/\s+/g, '-'); + formId = `rpg-add-item-form-stored-${locationId}`; + inputId = `.rpg-location-item-input[data-location="${location}"]`; + } else { + formId = `rpg-add-item-form-${field}`; + inputId = `#rpg-new-item-${field}`; } + + const form = $(`#${formId}`); + const input = $(inputId); + + form.hide(); + input.val(''); } /** - * Handles blur event for contenteditable "Assets" field. - * Saves changes when user finishes editing. - * @param {HTMLElement} element - The contenteditable element + * Adds a new item to the inventory. + * @param {string} field - Field name ('onPerson', 'stored', 'assets') + * @param {string} [location] - Location name (required for 'stored' field) */ -export function handleAssetsBlur(element) { +export function saveAddItem(field, location) { const inventory = extensionSettings.userStats.inventory; - const newValue = element.textContent.trim() || 'None'; + let inputId; - // Only save if value actually changed - if (newValue !== inventory.assets) { - inventory.assets = newValue; - - updateLastGeneratedDataInventory(); - saveSettings(); - saveChatData(); - updateMessageSwipeData(); + if (field === 'stored') { + inputId = `.rpg-location-item-input[data-location="${location}"]`; + } else { + inputId = `#rpg-new-item-${field}`; } + + const input = $(inputId); + const itemName = input.val().trim(); + + if (!itemName) { + hideAddItemForm(field, location); + return; + } + + // Get current items, add new one, serialize back + let currentString; + if (field === 'stored') { + currentString = inventory.stored[location] || 'None'; + } else { + currentString = inventory[field] || 'None'; + } + + const items = parseItems(currentString); + items.push(itemName); + const newString = serializeItems(items); + + // Save back to inventory + if (field === 'stored') { + inventory.stored[location] = newString; + } else { + inventory[field] = newString; + } + + updateLastGeneratedDataInventory(); + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + + // Hide form and re-render + hideAddItemForm(field, location); + renderInventory(); +} + +/** + * Removes an item from the inventory. + * @param {string} field - Field name ('onPerson', 'stored', 'assets') + * @param {number} itemIndex - Index of item to remove + * @param {string} [location] - Location name (required for 'stored' field) + */ +export function removeItem(field, itemIndex, location) { + const inventory = extensionSettings.userStats.inventory; + + // Get current items, remove the one at index, serialize back + let currentString; + if (field === 'stored') { + currentString = inventory.stored[location] || 'None'; + } else { + currentString = inventory[field] || 'None'; + } + + const items = parseItems(currentString); + items.splice(itemIndex, 1); // Remove item at index + const newString = serializeItems(items); + + // Save back to inventory + if (field === 'stored') { + inventory.stored[location] = newString; + } else { + inventory[field] = newString; + } + + updateLastGeneratedDataInventory(); + saveSettings(); + saveChatData(); + updateMessageSwipeData(); + + // Re-render + renderInventory(); } /** @@ -240,6 +319,31 @@ export function switchInventoryTab(tabName) { renderInventory(); } +/** + * Switches the view mode for an inventory section. + * @param {string} field - Field name ('onPerson', 'stored', 'assets') + * @param {string} mode - View mode ('list' or 'grid') + */ +export function switchViewMode(field, mode) { + // Ensure inventoryViewModes exists + if (!extensionSettings.inventoryViewModes) { + extensionSettings.inventoryViewModes = { + onPerson: 'list', + stored: 'list', + assets: 'list' + }; + } + + // Update view mode + extensionSettings.inventoryViewModes[field] = mode; + + // Save settings + saveSettings(); + + // Re-render inventory UI + renderInventory(); +} + /** * Initializes all event listeners for inventory interactions. * Uses event delegation to handle dynamically created elements. @@ -250,21 +354,52 @@ export function initInventoryEventListeners() { collapsedLocations = extensionSettings.collapsedInventoryLocations; } - // Contenteditable blur handlers (inline editing) - $(document).on('blur', '.rpg-inventory-text[contenteditable="true"]', function() { + // Add item button - shows inline form + $(document).on('click', '.rpg-inventory-add-btn[data-action="add-item"]', function(e) { + e.preventDefault(); const field = $(this).data('field'); - const element = this; + const location = $(this).data('location'); + showAddItemForm(field, location); + }); - if (field === 'onPerson') { - handleOnPersonBlur(element); - } else if (field === 'stored') { - const location = $(this).data('location'); - handleStoredLocationBlur(element, location); - } else if (field === 'assets') { - handleAssetsBlur(element); + // Add item inline form - save button + $(document).on('click', '.rpg-inline-btn[data-action="save-add-item"]', function(e) { + e.preventDefault(); + const field = $(this).data('field'); + const location = $(this).data('location'); + saveAddItem(field, location); + }); + + // Add item inline form - cancel button + $(document).on('click', '.rpg-inline-btn[data-action="cancel-add-item"]', function(e) { + e.preventDefault(); + const field = $(this).data('field'); + const location = $(this).data('location'); + hideAddItemForm(field, location); + }); + + // Add item inline form - enter key to save + $(document).on('keypress', '.rpg-inline-input', function(e) { + if (e.which === 13) { // Enter key + e.preventDefault(); + const $btn = $(this).closest('.rpg-inline-form').find('[data-action="save-add-item"]'); + if ($btn.length > 0) { + const field = $btn.data('field'); + const location = $btn.data('location'); + saveAddItem(field, location); + } } }); + // Remove item button + $(document).on('click', '.rpg-item-remove[data-action="remove-item"]', function(e) { + e.preventDefault(); + const field = $(this).data('field'); + const itemIndex = parseInt($(this).data('index')); + const location = $(this).data('location'); + removeItem(field, itemIndex, location); + }); + // Add location button - shows inline form $(document).on('click', '.rpg-inventory-add-btn[data-action="add-location"]', function(e) { e.preventDefault(); @@ -326,6 +461,14 @@ export function initInventoryEventListeners() { switchInventoryTab(tab); }); + // View mode switching + $(document).on('click', '.rpg-view-btn[data-action="switch-view"]', function(e) { + e.preventDefault(); + const field = $(this).data('field'); + const view = $(this).data('view'); + switchViewMode(field, view); + }); + console.log('[RPG Companion] Inventory event listeners initialized'); } diff --git a/src/systems/rendering/inventory.js b/src/systems/rendering/inventory.js index 19fd9dd..5fec662 100644 --- a/src/systems/rendering/inventory.js +++ b/src/systems/rendering/inventory.js @@ -5,6 +5,7 @@ import { extensionSettings, $inventoryContainer } from '../../core/state.js'; import { getInventoryRenderOptions } from '../interaction/inventoryActions.js'; +import { parseItems } from '../../utils/itemParser.js'; // Type imports /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ @@ -31,40 +32,108 @@ export function renderInventorySubTabs(activeTab = 'onPerson') { } /** - * Renders the "On Person" inventory view - * @param {string} onPersonItems - Current on-person items - * @returns {string} HTML for on-person view with edit controls + * 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) { - const displayText = onPersonItems || 'None'; +export function renderOnPersonView(onPersonItems, viewMode = 'list') { + const items = parseItems(onPersonItems); + + let itemsHtml = ''; + if (items.length === 0) { + itemsHtml = '
No items carried
'; + } 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 `

Items Currently Carried

+
+
+ + +
+ +
-
${escapeHtml(displayText)}
+ +
+ ${itemsHtml} +
`; } /** - * Renders the "Stored" inventory view with collapsible locations + * 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 = []) { +export function renderStoredView(stored, collapsedLocations = [], viewMode = 'list') { const locations = Object.keys(stored || {}); let html = `

Storage Locations

- +
+
+ + +
+ +