feat(inventory): add inline editing for inventory items

- Created inventoryEdit.js module with updateInventoryItem() function
- Made all inventory item names editable with contenteditable (mobile-friendly)
- Added rpg-editable class to 6 item rendering locations:
  * On Person (grid and list views)
  * Stored (grid and list views)
  * Assets (grid and list views)
- Added blur event listener to save changes on edit
- Validates and sanitizes edited names using sanitizeItemName()
- Syncs changes to lastGeneratedData and committedTrackerData (AI-visible)
- Shows full item text when editing (not truncated)
- Consistent UX with other editable fields in extension (stats, character traits, etc.)
- Re-renders inventory after successful edit or reverts on invalid input
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-20 09:25:55 +11:00
parent 9b6d0d41cd
commit 428d6fb40e
2 changed files with 120 additions and 6 deletions
+104
View File
@@ -0,0 +1,104 @@
/**
* Inventory Item Editing Module
* Handles inline editing of inventory item names
*/
import { extensionSettings, lastGeneratedData, committedTrackerData } 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';
import { sanitizeItemName } from '../../utils/security.js';
/**
* Updates an existing inventory item's name.
* Validates, sanitizes, and persists the change.
*
* @param {string} field - Field name ('onPerson', 'stored', 'assets')
* @param {number} index - Index of item in the array
* @param {string} newName - New name for the item
* @param {string} [location] - Location name (required for 'stored' field)
*/
export function updateInventoryItem(field, index, newName, location) {
const inventory = extensionSettings.userStats.inventory;
// Validate and sanitize the new item name
const sanitizedName = sanitizeItemName(newName);
if (!sanitizedName) {
console.warn('[RPG Companion] Invalid item name, reverting change');
// Re-render to revert the change in UI
renderInventory();
return;
}
// Get current items for the field
let currentString;
if (field === 'stored') {
if (!location) {
console.error('[RPG Companion] Location required for stored items');
return;
}
currentString = inventory.stored[location] || 'None';
} else {
currentString = inventory[field] || 'None';
}
// Parse current items
const items = parseItems(currentString);
// Validate index
if (index < 0 || index >= items.length) {
console.error(`[RPG Companion] Invalid item index: ${index}`);
return;
}
// Update the item at this index
items[index] = sanitizedName;
// Serialize back to string
const newItemString = serializeItems(items);
// Update the inventory
if (field === 'stored') {
inventory.stored[location] = newItemString;
} else {
inventory[field] = newItemString;
}
// Update lastGeneratedData and committedTrackerData with new inventory
updateLastGeneratedDataInventory();
// Save changes
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render inventory
renderInventory();
}
/**
* Updates lastGeneratedData.userStats AND committedTrackerData.userStats to include
* current inventory in text format.
* This ensures manual edits are immediately visible to AI in next generation.
* @private
*/
function updateLastGeneratedDataInventory() {
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
// Rebuild the userStats text format
const statsText =
`Health: ${stats.health}%\n` +
`Satiety: ${stats.satiety}%\n` +
`Energy: ${stats.energy}%\n` +
`Hygiene: ${stats.hygiene}%\n` +
`Arousal: ${stats.arousal}%\n` +
`${stats.mood}: ${stats.conditions}\n` +
`${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
}
+16 -6
View File
@@ -5,6 +5,7 @@
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';
// Type imports
@@ -51,14 +52,14 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
</div>
`).join('');
} else {
// List view: full-width rows
itemsHtml = items.map((item, index) => `
<div class="rpg-item-row" data-field="onPerson" data-index="${index}">
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
@@ -173,14 +174,14 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
</div>
`).join('');
} else {
// List view: full-width rows
itemsHtml = items.map((item, index) => `
<div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
@@ -269,14 +270,14 @@ export function renderAssetsView(assets, viewMode = 'list') {
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
<i class="fa-solid fa-times"></i>
</button>
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
</div>
`).join('');
} else {
// List view: full-width rows
itemsHtml = items.map((item, index) => `
<div class="rpg-item-row" data-field="assets" data-index="${index}">
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
<i class="fa-solid fa-times"></i>
</button>
@@ -455,6 +456,15 @@ export function renderInventory() {
// 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);
});
}
/**