feat(inventory): replace prompt dialogs with inline editing

Replaced all prompt() and confirm() dialogs with contenteditable fields
and inline UI components for a better user experience.

Changes:
- Made inventory fields (On Person, Stored items, Assets) contenteditable
  with blur-to-save functionality
- Replaced "Add Location" prompt with inline form (hidden by default)
- Replaced "Remove Location" confirm with inline confirmation UI
- Added CSS styling for inline editing states (hover, focus, empty)
- Added CSS for inline forms, buttons, and confirmation UI
- Fixed bug where inventory sub-tabs were unclickable due to
  incorrect container ID in toggleLocationCollapse() and
  switchInventoryTab() functions

All inline edits now save automatically on blur, matching the UX
pattern used elsewhere in the extension (mood/conditions fields).
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-17 16:27:59 +11:00
parent f560bb543b
commit 97dc87062f
3 changed files with 294 additions and 122 deletions
+154 -110
View File
@@ -6,7 +6,7 @@
import { extensionSettings, lastGeneratedData } from '../../core/state.js';
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
import { renderInventory, updateInventoryDisplay } from '../rendering/inventory.js';
import { renderInventory } from '../rendering/inventory.js';
// Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
@@ -43,124 +43,151 @@ function updateLastGeneratedDataInventory() {
}
/**
* Edits items currently on the character's person.
* @returns {Promise<void>}
* Handles blur event for contenteditable "On Person" field.
* Saves changes when user finishes editing.
* @param {HTMLElement} element - The contenteditable element
*/
export async function editOnPerson() {
export function handleOnPersonBlur(element) {
const inventory = extensionSettings.userStats.inventory;
const current = inventory.onPerson || 'None';
const newValue = element.textContent.trim() || 'None';
const newValue = prompt('Edit items on person (carried/worn):', current);
if (newValue === null) return; // User cancelled
// Only save if value actually changed
if (newValue !== inventory.onPerson) {
inventory.onPerson = newValue;
inventory.onPerson = newValue.trim() || 'None';
updateLastGeneratedDataInventory();
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render inventory UI
updateInventoryDisplay('rpg-inventory-content', {
activeSubTab: currentActiveSubTab,
collapsedLocations
});
updateLastGeneratedDataInventory();
saveSettings();
saveChatData();
updateMessageSwipeData();
}
}
/**
* Edits items stored at a specific location.
* 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
* @returns {Promise<void>}
*/
export async function editStoredLocation(locationName) {
export function handleStoredLocationBlur(element, locationName) {
const inventory = extensionSettings.userStats.inventory;
const current = inventory.stored[locationName] || 'None';
const newValue = element.textContent.trim() || 'None';
const newValue = prompt(`Edit items stored at "${locationName}":`, current);
if (newValue === null) return; // User cancelled
// Only save if value actually changed
if (newValue !== inventory.stored[locationName]) {
inventory.stored[locationName] = newValue;
inventory.stored[locationName] = newValue.trim() || 'None';
updateLastGeneratedDataInventory();
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render inventory UI
updateInventoryDisplay('rpg-inventory-content', {
activeSubTab: currentActiveSubTab,
collapsedLocations
});
updateLastGeneratedDataInventory();
saveSettings();
saveChatData();
updateMessageSwipeData();
}
}
/**
* Edits character's assets (vehicles, property, major possessions).
* @returns {Promise<void>}
* Handles blur event for contenteditable "Assets" field.
* Saves changes when user finishes editing.
* @param {HTMLElement} element - The contenteditable element
*/
export async function editAssets() {
export function handleAssetsBlur(element) {
const inventory = extensionSettings.userStats.inventory;
const current = inventory.assets || 'None';
const newValue = element.textContent.trim() || 'None';
const newValue = prompt('Edit assets (vehicles, property, equipment):', current);
if (newValue === null) return; // User cancelled
// Only save if value actually changed
if (newValue !== inventory.assets) {
inventory.assets = newValue;
inventory.assets = newValue.trim() || 'None';
updateLastGeneratedDataInventory();
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render inventory UI
updateInventoryDisplay('rpg-inventory-content', {
activeSubTab: currentActiveSubTab,
collapsedLocations
});
updateLastGeneratedDataInventory();
saveSettings();
saveChatData();
updateMessageSwipeData();
}
}
/**
* Adds a new storage location to the inventory.
* @returns {Promise<void>}
* Shows the inline form for adding a new storage location.
*/
export async function addStorageLocation() {
export function showAddLocationForm() {
const form = $('#rpg-add-location-form');
const input = $('#rpg-new-location-name');
form.show();
input.val('').focus();
}
/**
* Hides the inline form for adding a new storage location.
*/
export function hideAddLocationForm() {
const form = $('#rpg-add-location-form');
const input = $('#rpg-new-location-name');
form.hide();
input.val('');
}
/**
* Saves a new storage location from the inline form.
*/
export function saveAddLocation() {
const inventory = extensionSettings.userStats.inventory;
const input = $('#rpg-new-location-name');
const locationName = input.val().trim();
const locationName = prompt('Enter name for new storage location:');
if (!locationName) return; // User cancelled or entered empty string
const trimmedName = locationName.trim();
if (!locationName) {
hideAddLocationForm();
return;
}
// Check for duplicate
if (inventory.stored[trimmedName]) {
alert(`Storage location "${trimmedName}" already exists.`);
if (inventory.stored[locationName]) {
alert(`Storage location "${locationName}" already exists.`);
return;
}
// Create new location with default "None"
inventory.stored[trimmedName] = 'None';
inventory.stored[locationName] = 'None';
updateLastGeneratedDataInventory();
saveSettings();
saveChatData();
updateMessageSwipeData();
// Switch to stored tab and re-render
currentActiveSubTab = 'stored';
updateInventoryDisplay('rpg-inventory-content', {
activeSubTab: currentActiveSubTab,
collapsedLocations
});
// Hide form and re-render
hideAddLocationForm();
renderInventory();
}
/**
* Removes a storage location from the inventory.
* Shows the inline confirmation UI for removing a storage location.
* @param {string} locationName - Name of location to remove
* @returns {Promise<void>}
*/
export async function removeStorageLocation(locationName) {
const confirmed = confirm(`Remove storage location "${locationName}"?\n\nThis will delete all items stored there.`);
if (!confirmed) return;
export function showRemoveConfirmation(locationName) {
const confirmId = `rpg-remove-confirm-${locationName.replace(/\s+/g, '-')}`;
const confirmUI = $(`#${confirmId}`);
if (confirmUI.length > 0) {
confirmUI.show();
}
}
/**
* Hides the inline confirmation UI for removing a storage location.
* @param {string} locationName - Name of location
*/
export function hideRemoveConfirmation(locationName) {
const confirmId = `rpg-remove-confirm-${locationName.replace(/\s+/g, '-')}`;
const confirmUI = $(`#${confirmId}`);
if (confirmUI.length > 0) {
confirmUI.hide();
}
}
/**
* Confirms and removes a storage location from the inventory.
* @param {string} locationName - Name of location to remove
*/
export function confirmRemoveLocation(locationName) {
const inventory = extensionSettings.userStats.inventory;
delete inventory.stored[locationName];
@@ -176,10 +203,7 @@ export async function removeStorageLocation(locationName) {
updateMessageSwipeData();
// Re-render inventory UI
updateInventoryDisplay('rpg-inventory-content', {
activeSubTab: currentActiveSubTab,
collapsedLocations
});
renderInventory();
}
/**
@@ -202,10 +226,7 @@ export function toggleLocationCollapse(locationName) {
saveSettings();
// Re-render inventory UI
updateInventoryDisplay('rpg-inventory-content', {
activeSubTab: currentActiveSubTab,
collapsedLocations
});
renderInventory();
}
/**
@@ -216,10 +237,7 @@ export function switchInventoryTab(tabName) {
currentActiveSubTab = tabName;
// Re-render inventory UI
updateInventoryDisplay('rpg-inventory-content', {
activeSubTab: currentActiveSubTab,
collapsedLocations
});
renderInventory();
}
/**
@@ -232,40 +250,66 @@ export function initInventoryEventListeners() {
collapsedLocations = extensionSettings.collapsedInventoryLocations;
}
// Event delegation for all inventory buttons
$(document).on('click', '.rpg-inventory-edit-btn', function(e) {
e.preventDefault();
const action = $(this).data('action');
// Contenteditable blur handlers (inline editing)
$(document).on('blur', '.rpg-inventory-text[contenteditable="true"]', function() {
const field = $(this).data('field');
const element = this;
if (action === 'edit-onperson') {
editOnPerson();
} else if (action === 'edit-location') {
if (field === 'onPerson') {
handleOnPersonBlur(element);
} else if (field === 'stored') {
const location = $(this).data('location');
editStoredLocation(location);
} else if (action === 'edit-assets') {
editAssets();
handleStoredLocationBlur(element, location);
} else if (field === 'assets') {
handleAssetsBlur(element);
}
});
// Add location button
$(document).on('click', '.rpg-inventory-add-btn', function(e) {
// Add location button - shows inline form
$(document).on('click', '.rpg-inventory-add-btn[data-action="add-location"]', function(e) {
e.preventDefault();
const action = $(this).data('action');
showAddLocationForm();
});
if (action === 'add-location') {
addStorageLocation();
// Add location inline form - save button
$(document).on('click', '.rpg-inline-btn[data-action="save-add-location"]', function(e) {
e.preventDefault();
saveAddLocation();
});
// Add location inline form - cancel button
$(document).on('click', '.rpg-inline-btn[data-action="cancel-add-location"]', function(e) {
e.preventDefault();
hideAddLocationForm();
});
// Add location inline form - enter key to save
$(document).on('keypress', '#rpg-new-location-name', function(e) {
if (e.which === 13) { // Enter key
e.preventDefault();
saveAddLocation();
}
});
// Remove location buttons
$(document).on('click', '.rpg-inventory-remove-btn', function(e) {
// Remove location button - shows inline confirmation
$(document).on('click', '.rpg-inventory-remove-btn[data-action="remove-location"]', function(e) {
e.preventDefault();
const action = $(this).data('action');
const location = $(this).data('location');
showRemoveConfirmation(location);
});
if (action === 'remove-location') {
const location = $(this).data('location');
removeStorageLocation(location);
}
// Remove location inline confirmation - confirm button
$(document).on('click', '.rpg-inline-btn[data-action="confirm-remove-location"]', function(e) {
e.preventDefault();
const location = $(this).data('location');
confirmRemoveLocation(location);
});
// Remove location inline confirmation - cancel button
$(document).on('click', '.rpg-inline-btn[data-action="cancel-remove-location"]', function(e) {
e.preventDefault();
const location = $(this).data('location');
hideRemoveConfirmation(location);
});
// Collapse toggle buttons
+25 -12
View File
@@ -41,12 +41,9 @@ export function renderOnPersonView(onPersonItems) {
<div class="rpg-inventory-section" data-section="onPerson">
<div class="rpg-inventory-header">
<h4>Items Currently Carried</h4>
<button class="rpg-inventory-edit-btn" data-action="edit-onperson" title="Edit on-person inventory">
<i class="fa-solid fa-pen"></i> Edit
</button>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inventory-text">${escapeHtml(displayText)}</div>
<div class="rpg-inventory-text rpg-editable" contenteditable="true" data-field="onPerson" title="Click to edit">${escapeHtml(displayText)}</div>
</div>
</div>
`;
@@ -70,6 +67,17 @@ export function renderStoredView(stored, collapsedLocations = []) {
</button>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inline-form" id="rpg-add-location-form" style="display: none;">
<input type="text" class="rpg-inline-input" id="rpg-new-location-name" placeholder="Enter location name..." />
<div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-location">
<i class="fa-solid fa-times"></i> Cancel
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-location">
<i class="fa-solid fa-check"></i> Save
</button>
</div>
</div>
`;
if (locations.length === 0) {
@@ -90,16 +98,24 @@ export function renderStoredView(stored, collapsedLocations = []) {
</button>
<h5 class="rpg-storage-name">${escapeHtml(location)}</h5>
<div class="rpg-storage-actions">
<button class="rpg-inventory-edit-btn" data-action="edit-location" data-location="${escapeHtml(location)}" title="Edit items at this location">
<i class="fa-solid fa-pen"></i>
</button>
<button class="rpg-inventory-remove-btn" data-action="remove-location" data-location="${escapeHtml(location)}" title="Remove this storage location">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
<div class="rpg-storage-content" ${isCollapsed ? 'style="display:none;"' : ''}>
<div class="rpg-inventory-text">${escapeHtml(items || 'None')}</div>
<div class="rpg-inventory-text rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" title="Click to edit">${escapeHtml(items || 'None')}</div>
</div>
<div class="rpg-inline-confirmation" id="rpg-remove-confirm-${escapeHtml(location).replace(/\s+/g, '-')}" style="display: none;">
<p>Remove "${escapeHtml(location)}"? This will delete all items stored there.</p>
<div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-remove-location" data-location="${escapeHtml(location)}">
<i class="fa-solid fa-times"></i> Cancel
</button>
<button class="rpg-inline-btn rpg-inline-confirm" data-action="confirm-remove-location" data-location="${escapeHtml(location)}">
<i class="fa-solid fa-check"></i> Confirm
</button>
</div>
</div>
</div>
`;
@@ -125,12 +141,9 @@ export function renderAssetsView(assets) {
<div class="rpg-inventory-section" data-section="assets">
<div class="rpg-inventory-header">
<h4>Vehicles, Property & Major Possessions</h4>
<button class="rpg-inventory-edit-btn" data-action="edit-assets" title="Edit assets">
<i class="fa-solid fa-pen"></i> Edit
</button>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inventory-text">${escapeHtml(displayText)}</div>
<div class="rpg-inventory-text rpg-editable" contenteditable="true" data-field="assets" title="Click to edit">${escapeHtml(displayText)}</div>
<div class="rpg-inventory-hint">
<i class="fa-solid fa-info-circle"></i>
Assets include vehicles (cars, motorcycles), property (homes, apartments),
+115
View File
@@ -4109,6 +4109,121 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
color: white;
}
/* Inline Editing Styles */
.rpg-inventory-text.rpg-editable {
cursor: text;
transition: all 0.2s ease;
min-height: 2rem;
}
.rpg-inventory-text.rpg-editable:hover {
background: var(--SmartThemeQuoteColor);
border-color: var(--ac-style-color-matchedText);
}
.rpg-inventory-text.rpg-editable:focus {
outline: none;
border-color: var(--ac-style-color-matchedText);
background: var(--SmartThemeEmColor);
box-shadow: 0 0 0 2px rgba(var(--ac-style-color-matchedText-rgb, 66, 135, 245), 0.2);
}
.rpg-inventory-text.rpg-editable:empty::before {
content: 'Click to edit...';
color: var(--SmartThemeFastUISliderColColor);
font-style: italic;
}
/* Inline Forms */
.rpg-inline-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem;
background: var(--SmartThemeQuoteColor);
border: 1px solid var(--ac-style-color-matchedText);
border-radius: 0.25rem;
margin-bottom: 0.75rem;
}
.rpg-inline-input {
padding: 0.5rem 0.75rem;
background: var(--SmartThemeBlurTintColor);
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
color: var(--SmartThemeBodyColor);
font-size: 0.9rem;
font-family: inherit;
}
.rpg-inline-input:focus {
outline: none;
border-color: var(--ac-style-color-matchedText);
box-shadow: 0 0 0 2px rgba(var(--ac-style-color-matchedText-rgb, 66, 135, 245), 0.2);
}
.rpg-inline-buttons {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.rpg-inline-btn {
padding: 0.4rem 0.75rem;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 0.25rem;
background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.85rem;
display: flex;
align-items: center;
gap: 0.35rem;
}
.rpg-inline-btn:hover {
opacity: 0.85;
}
.rpg-inline-cancel {
background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeFastUISliderColColor);
}
.rpg-inline-cancel:hover {
background: #6c757d;
border-color: #6c757d;
color: white;
}
.rpg-inline-save,
.rpg-inline-confirm {
background: var(--ac-style-color-matchedText);
border-color: var(--ac-style-color-matchedText);
color: white;
}
.rpg-inline-save:hover,
.rpg-inline-confirm:hover {
opacity: 0.85;
}
/* Inline Confirmation */
.rpg-inline-confirmation {
padding: 0.75rem;
background: var(--SmartThemeQuoteColor);
border: 1px solid #dc3545;
border-radius: 0.25rem;
margin-top: 0.5rem;
}
.rpg-inline-confirmation p {
margin: 0 0 0.75rem 0;
color: var(--SmartThemeBodyColor);
font-size: 0.9rem;
}
/* ============================================
DESKTOP TABS SYSTEM
============================================ */