Files
rpg-companion-sillytavern/src/systems/dashboard/widgets/inventoryWidget.js
T
Lucas 'Paperboy' Rose-Winters 13b1acb151 fix: center icons in compact buttons with fixed dimensions and flexbox
Properly centered icons by wrapping text in spans and forcing exact button
dimensions to override base min-width: fit-content style.

Root Cause:
- Base .rpg-inventory-add-btn has min-width: fit-content
- Even with span hidden, button width wasn't constrained
- Icon appeared off-center with extra space to the right

Solution:

1. HTML Structure (inventoryWidget.js, questsWidget.js):
   - Wrapped text in <span class="rpg-btn-label">
   - Pattern: <i class="fa-solid fa-plus"></i><span class="rpg-btn-label"> Add Item</span>
   - Applied to: Add Item, Add Location, Add Asset, Add Quest buttons

2. CSS (style.css):
   - Hide labels: .rpg-inventory-compact .rpg-btn-label { display: none; }
   - Force square dimensions: width: 32px !important (overrides fit-content)
   - Center icon: display: inline-flex; justify-content: center; align-items: center
   - Remove padding: padding: 0 (icon uses full 32px space)

Result: Perfect 32×32px square buttons with centered icons in compact mode,
matching the pattern used for sub-tab buttons throughout the codebase.

Fixes: Icon skewed left in Add Item/Quest buttons at narrow widths
2025-11-05 10:16:21 +11:00

959 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Inventory Widget
*
* Comprehensive inventory management with three sub-tabs:
* - On Person: Items currently carried
* - Stored: Items in storage locations
* - Assets: Vehicles, property, major possessions
*
* Features:
* - List/Grid view modes per sub-tab
* - Add/remove items and storage locations
* - Collapsible storage locations
* - Editable item names
* - Inline forms for adding items
*/
import { parseItems, serializeItems } from '../../../utils/itemParser.js';
import { sanitizeItemName, sanitizeLocationName } from '../../../utils/security.js';
import { showAlertDialog } from '../confirmDialog.js';
/**
* Convert location name to safe HTML ID
*/
function getLocationId(locationName) {
return locationName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '-');
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Register Inventory Widget
*/
export function registerInventoryWidget(registry, dependencies) {
const { getExtensionSettings, onDataChange } = dependencies;
// Widget state (per-instance)
const widgetStates = new Map();
function getWidgetState(widgetId) {
if (!widgetStates.has(widgetId)) {
widgetStates.set(widgetId, {
activeSubTab: 'onPerson',
collapsedLocations: [],
viewModes: {
onPerson: 'list',
stored: 'list',
assets: 'list'
}
});
}
return widgetStates.get(widgetId);
}
registry.register('inventory', {
name: 'Inventory',
icon: '🎒',
description: 'Full inventory system with On Person, Stored, and Assets',
category: 'inventory',
minSize: { w: 2, h: 4 },
// Column-aware sizing: compact on mobile, spacious on desktop
defaultSize: (columns) => {
if (columns <= 2) {
return { w: 2, h: 5 }; // Mobile: 2×5 (full width, compact)
}
return { w: 2, h: 6 }; // Desktop: 2×6 (default)
},
maxAutoSize: (columns) => {
if (columns <= 2) {
return { w: 2, h: 8 }; // Mobile: 2×8 max (increased for expansion headroom)
}
return { w: 3, h: 8 }; // Desktop: 3×8 max (can expand)
},
requiresSchema: false,
render(container, config = {}) {
const settings = getExtensionSettings();
const inventory = settings.userStats.inventory || {
version: 2,
onPerson: 'None',
stored: {},
assets: 'None'
};
// Get or create widget state
const widgetId = container.closest('.dashboard-widget')?.dataset?.widgetId || 'default';
const state = getWidgetState(widgetId);
// Build HTML
const html = `
<div class="rpg-inventory-widget" data-widget-id="${widgetId}">
${renderSubTabs(state.activeSubTab)}
<div class="rpg-inventory-views">
${renderActiveView(inventory, state)}
</div>
</div>
`;
container.innerHTML = html;
// Attach event handlers
attachInventoryHandlers(container, widgetId, inventory, state, dependencies);
},
getConfig() {
return {
compactMode: {
type: 'boolean',
label: 'Compact Mode',
default: false
}
};
},
onConfigChange(container, newConfig) {
this.render(container, newConfig);
},
onResize(container, newW, newH) {
// Re-render widget to update internal layout for new dimensions
// This ensures sub-tabs, item lists, and storage locations adapt correctly
this.render(container, this.config || {});
// Apply compact mode styling if needed
const widget = container.querySelector('.rpg-inventory-widget');
if (widget) {
if (newW < 6) {
widget.classList.add('rpg-inventory-compact');
} else {
widget.classList.remove('rpg-inventory-compact');
}
}
},
onRemove(widgetId) {
// Clean up widget state
widgetStates.delete(widgetId);
}
});
/**
* Render sub-tab navigation
*/
function renderSubTabs(activeTab) {
return `
<div class="rpg-inventory-subtabs">
<button class="rpg-inventory-subtab ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson" title="On Person">
<i class="fa-solid fa-user"></i>
<span class="rpg-subtab-label">On Person</span>
</button>
<button class="rpg-inventory-subtab ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored" title="Stored">
<i class="fa-solid fa-box"></i>
<span class="rpg-subtab-label">Stored</span>
</button>
<button class="rpg-inventory-subtab ${activeTab === 'assets' ? 'active' : ''}" data-tab="assets" title="Assets">
<i class="fa-solid fa-building"></i>
<span class="rpg-subtab-label">Assets</span>
</button>
</div>
`;
}
/**
* Render active view based on state
*/
function renderActiveView(inventory, state) {
switch (state.activeSubTab) {
case 'onPerson':
return renderOnPersonView(inventory.onPerson, state.viewModes.onPerson);
case 'stored':
return renderStoredView(inventory.stored, state.collapsedLocations, state.viewModes.stored);
case 'assets':
return renderAssetsView(inventory.assets, state.viewModes.assets);
default:
return renderOnPersonView(inventory.onPerson, state.viewModes.onPerson);
}
}
/**
* Render On Person view
*/
function renderOnPersonView(onPersonItems, viewMode) {
const items = parseItems(onPersonItems);
const itemsHtml = items.length === 0
? '<div class="rpg-inventory-empty">No items carried</div>'
: renderItemList(items, 'onPerson', null, viewMode);
return `
<div class="rpg-inventory-section" data-section="onPerson">
<div class="rpg-inventory-header">
<h4>Items Currently Carried</h4>
<div class="rpg-inventory-header-actions">
${renderViewToggle('onPerson', viewMode)}
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="onPerson">
<i class="fa-solid fa-plus"></i><span class="rpg-btn-label"> Add Item</span>
</button>
</div>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inline-form" data-form="add-item-onPerson" style="display: none;">
<input type="text" class="rpg-inline-input" placeholder="Enter item name..." />
<div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="onPerson">
<i class="fa-solid fa-times"></i> Cancel
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="onPerson">
<i class="fa-solid fa-check"></i> Add
</button>
</div>
</div>
<div class="rpg-item-list ${viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view'}">
${itemsHtml}
</div>
</div>
</div>
`;
}
/**
* Render Stored view
*/
function renderStoredView(stored, collapsedLocations, viewMode) {
const locations = Object.keys(stored || {});
let locationsHtml = '';
if (locations.length === 0) {
locationsHtml = `
<div class="rpg-inventory-empty">
No storage locations yet. Click "Add Location" to create one.
</div>
`;
} else {
locationsHtml = locations.map(location => {
const items = parseItems(stored[location]);
const isCollapsed = collapsedLocations.includes(location);
const locationId = getLocationId(location);
const itemsHtml = items.length === 0
? '<div class="rpg-inventory-empty">No items stored here</div>'
: renderItemList(items, 'stored', location, viewMode);
return `
<div class="rpg-storage-location ${isCollapsed ? 'collapsed' : ''}" data-location="${escapeHtml(location)}">
<div class="rpg-storage-header">
<button class="rpg-storage-toggle" data-action="toggle-location" data-location="${escapeHtml(location)}">
<i class="fa-solid fa-chevron-${isCollapsed ? 'right' : 'down'}"></i>
</button>
<h5 class="rpg-storage-name">${escapeHtml(location)}</h5>
<div class="rpg-storage-actions">
<button class="rpg-inventory-remove-btn" data-action="remove-location" data-location="${escapeHtml(location)}">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
<div class="rpg-storage-content" ${isCollapsed ? 'style="display:none;"' : ''}>
<div class="rpg-inline-form" data-form="add-item-stored-${locationId}" style="display: none;">
<input type="text" class="rpg-inline-input" placeholder="Enter item name..." />
<div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="stored" data-location="${escapeHtml(location)}">
<i class="fa-solid fa-times"></i> Cancel
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="stored" data-location="${escapeHtml(location)}">
<i class="fa-solid fa-check"></i> Add
</button>
</div>
</div>
<div class="rpg-item-list ${viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view'}">
${itemsHtml}
</div>
<div class="rpg-storage-add-item-container">
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="stored" data-location="${escapeHtml(location)}">
<i class="fa-solid fa-plus"></i> Add Item
</button>
</div>
</div>
<div class="rpg-inline-confirmation" data-confirm="remove-location-${locationId}" 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>
`;
}).join('');
}
return `
<div class="rpg-inventory-section" data-section="stored">
<div class="rpg-inventory-header">
<h4>Storage Locations</h4>
<div class="rpg-inventory-header-actions">
${renderViewToggle('stored', viewMode)}
<button class="rpg-inventory-add-btn" data-action="add-location">
<i class="fa-solid fa-plus"></i><span class="rpg-btn-label"> Add Location</span>
</button>
</div>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inline-form" data-form="add-location" style="display: none;">
<input type="text" class="rpg-inline-input" 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>
${locationsHtml}
</div>
</div>
`;
}
/**
* Render Assets view
*/
function renderAssetsView(assets, viewMode) {
const items = parseItems(assets);
const itemsHtml = items.length === 0
? '<div class="rpg-inventory-empty">No assets owned</div>'
: renderItemList(items, 'assets', null, viewMode);
return `
<div class="rpg-inventory-section" data-section="assets">
<div class="rpg-inventory-header">
<h4>Vehicles, Property & Major Possessions</h4>
<div class="rpg-inventory-header-actions">
${renderViewToggle('assets', viewMode)}
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="assets">
<i class="fa-solid fa-plus"></i><span class="rpg-btn-label"> Add Asset</span>
</button>
</div>
</div>
<div class="rpg-inventory-content">
<div class="rpg-inline-form" data-form="add-item-assets" style="display: none;">
<input type="text" class="rpg-inline-input" placeholder="Enter asset name..." />
<div class="rpg-inline-buttons">
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="assets">
<i class="fa-solid fa-times"></i> Cancel
</button>
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="assets">
<i class="fa-solid fa-check"></i> Add
</button>
</div>
</div>
<div class="rpg-item-list ${viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view'}">
${itemsHtml}
</div>
<div class="rpg-inventory-hint">
<i class="fa-solid fa-info-circle"></i>
Assets include vehicles (cars, motorcycles), property (homes, apartments),
and major equipment (workshop tools, special items).
</div>
</div>
</div>
`;
}
/**
* Render view toggle buttons
*/
function renderViewToggle(field, viewMode) {
return `
<div class="rpg-view-toggle">
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="${field}" data-view="list" title="List view">
<i class="fa-solid fa-list"></i>
</button>
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="${field}" data-view="grid" title="Grid view">
<i class="fa-solid fa-th"></i>
</button>
</div>
`;
}
/**
* Render item list (list or grid view)
*/
function renderItemList(items, field, location, viewMode) {
const locationAttr = location ? `data-location="${escapeHtml(location)}"` : '';
if (viewMode === 'grid') {
return items.map((item, index) => `
<div class="rpg-item-card" data-field="${field}" ${locationAttr} data-index="${index}">
<button class="rpg-item-remove" data-action="remove-item" data-field="${field}" ${locationAttr} data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="${field}" ${locationAttr} data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
</div>
`).join('');
} else {
return items.map((item, index) => `
<div class="rpg-item-row" data-field="${field}" ${locationAttr} data-index="${index}">
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="${field}" ${locationAttr} data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
<button class="rpg-item-remove" data-action="remove-item" data-field="${field}" ${locationAttr} data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
</div>
`).join('');
}
}
/**
* Attach all event handlers
*/
function attachInventoryHandlers(container, widgetId, inventory, state, dependencies) {
const widget = container.querySelector('.rpg-inventory-widget');
if (!widget) return;
// Sub-tab switching
widget.querySelectorAll('.rpg-inventory-subtab').forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.dataset.tab;
state.activeSubTab = tab;
// Re-render
const settings = getExtensionSettings();
const inv = settings.userStats.inventory;
widget.querySelector('.rpg-inventory-views').innerHTML = renderActiveView(inv, state);
// Update active tab styling
widget.querySelectorAll('.rpg-inventory-subtab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Re-attach handlers for new view
attachInventoryHandlers(container, widgetId, inv, state, dependencies);
});
});
// View mode toggle
widget.querySelectorAll('[data-action="switch-view"]').forEach(btn => {
btn.addEventListener('click', () => {
const field = btn.dataset.field;
const view = btn.dataset.view;
state.viewModes[field] = view;
// Re-render active view
const settings = getExtensionSettings();
const inv = settings.userStats.inventory;
widget.querySelector('.rpg-inventory-views').innerHTML = renderActiveView(inv, state);
attachInventoryHandlers(container, widgetId, inv, state, dependencies);
});
});
// Add item button
widget.querySelectorAll('[data-action="add-item"]').forEach(btn => {
btn.addEventListener('click', () => {
const field = btn.dataset.field;
const location = btn.dataset.location;
showAddItemForm(widget, field, location);
});
});
// Cancel add item
widget.querySelectorAll('[data-action="cancel-add-item"]').forEach(btn => {
btn.addEventListener('click', () => {
const field = btn.dataset.field;
const location = btn.dataset.location;
hideAddItemForm(widget, field, location);
});
});
// Save add item
widget.querySelectorAll('[data-action="save-add-item"]').forEach(btn => {
btn.addEventListener('click', () => {
const field = btn.dataset.field;
const location = btn.dataset.location;
saveAddItem(container, widgetId, field, location, state, dependencies);
});
});
// Enter key in add item form
widget.querySelectorAll('.rpg-inline-form input').forEach(input => {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const form = input.closest('.rpg-inline-form');
const saveBtn = form.querySelector('[data-action="save-add-item"], [data-action="save-add-location"]');
if (saveBtn) saveBtn.click();
}
if (e.key === 'Escape') {
e.preventDefault();
const form = input.closest('.rpg-inline-form');
const cancelBtn = form.querySelector('[data-action="cancel-add-item"], [data-action="cancel-add-location"]');
if (cancelBtn) cancelBtn.click();
}
});
});
// Remove item
widget.querySelectorAll('[data-action="remove-item"]').forEach(btn => {
btn.addEventListener('click', () => {
const field = btn.dataset.field;
const index = parseInt(btn.dataset.index);
const location = btn.dataset.location;
removeItem(container, widgetId, field, index, location, state, dependencies);
});
});
// Edit item name
widget.querySelectorAll('.rpg-item-name.rpg-editable').forEach(field => {
let originalValue = field.textContent.trim();
field.addEventListener('focus', () => {
originalValue = field.textContent.trim();
const range = document.createRange();
range.selectNodeContents(field);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
field.addEventListener('blur', () => {
const newValue = field.textContent.trim();
if (newValue && newValue !== originalValue) {
const fieldName = field.dataset.field;
const index = parseInt(field.dataset.index);
const location = field.dataset.location;
updateItemName(container, widgetId, fieldName, index, newValue, location, state, dependencies);
} else if (!newValue) {
field.textContent = originalValue;
}
});
field.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
field.blur();
}
if (e.key === 'Escape') {
e.preventDefault();
field.textContent = originalValue;
field.blur();
}
});
field.addEventListener('paste', (e) => {
e.preventDefault();
const text = (e.clipboardData || window.clipboardData).getData('text/plain');
document.execCommand('insertText', false, text);
});
});
// Add location
const addLocationBtn = widget.querySelector('[data-action="add-location"]');
if (addLocationBtn) {
addLocationBtn.addEventListener('click', () => {
showAddLocationForm(widget);
});
}
// Cancel add location
const cancelAddLocationBtn = widget.querySelector('[data-action="cancel-add-location"]');
if (cancelAddLocationBtn) {
cancelAddLocationBtn.addEventListener('click', () => {
hideAddLocationForm(widget);
});
}
// Save add location
const saveAddLocationBtn = widget.querySelector('[data-action="save-add-location"]');
if (saveAddLocationBtn) {
saveAddLocationBtn.addEventListener('click', () => {
saveAddLocation(container, widgetId, state, dependencies);
});
}
// Toggle location collapse
widget.querySelectorAll('[data-action="toggle-location"]').forEach(btn => {
btn.addEventListener('click', () => {
const location = btn.dataset.location;
toggleLocationCollapse(widget, location, state);
});
});
// Remove location
widget.querySelectorAll('[data-action="remove-location"]').forEach(btn => {
btn.addEventListener('click', () => {
const location = btn.dataset.location;
showRemoveLocationConfirm(widget, location);
});
});
// Cancel remove location
widget.querySelectorAll('[data-action="cancel-remove-location"]').forEach(btn => {
btn.addEventListener('click', () => {
const location = btn.dataset.location;
hideRemoveLocationConfirm(widget, location);
});
});
// Confirm remove location
widget.querySelectorAll('[data-action="confirm-remove-location"]').forEach(btn => {
btn.addEventListener('click', () => {
const location = btn.dataset.location;
removeLocation(container, widgetId, location, state, dependencies);
});
});
}
/**
* Show add item form
*/
function showAddItemForm(widget, field, location) {
let formSelector;
if (field === 'stored') {
const locationId = getLocationId(location);
formSelector = `[data-form="add-item-stored-${locationId}"]`;
} else {
formSelector = `[data-form="add-item-${field}"]`;
}
const form = widget.querySelector(formSelector);
if (form) {
form.style.display = 'block';
const input = form.querySelector('input');
if (input) {
input.value = '';
input.focus();
}
}
}
/**
* Hide add item form
*/
function hideAddItemForm(widget, field, location) {
let formSelector;
if (field === 'stored') {
const locationId = getLocationId(location);
formSelector = `[data-form="add-item-stored-${locationId}"]`;
} else {
formSelector = `[data-form="add-item-${field}"]`;
}
const form = widget.querySelector(formSelector);
if (form) {
form.style.display = 'none';
const input = form.querySelector('input');
if (input) input.value = '';
}
}
/**
* Save new item
*/
function saveAddItem(container, widgetId, field, location, state, dependencies) {
const widget = container.querySelector('.rpg-inventory-widget');
let formSelector;
if (field === 'stored') {
const locationId = getLocationId(location);
formSelector = `[data-form="add-item-stored-${locationId}"]`;
} else {
formSelector = `[data-form="add-item-${field}"]`;
}
const form = widget.querySelector(formSelector);
if (!form) return;
const input = form.querySelector('input');
const rawItemName = input.value.trim();
if (!rawItemName) {
hideAddItemForm(widget, field, location);
return;
}
const itemName = sanitizeItemName(rawItemName);
if (!itemName) {
showAlertDialog({
title: 'Invalid Item',
message: 'Please enter a valid item name.',
variant: 'warning'
});
hideAddItemForm(widget, field, location);
return;
}
const settings = getExtensionSettings();
const inventory = settings.userStats.inventory;
// Get current items
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
if (field === 'stored') {
inventory.stored[location] = newString;
} else {
inventory[field] = newString;
}
// Trigger change callback
if (onDataChange) {
onDataChange('inventory', field, newString, location);
}
hideAddItemForm(widget, field, location);
// Re-render view
widget.querySelector('.rpg-inventory-views').innerHTML = renderActiveView(inventory, state);
attachInventoryHandlers(container, widgetId, inventory, state, dependencies);
}
/**
* Remove item
*/
function removeItem(container, widgetId, field, index, location, state, dependencies) {
const settings = getExtensionSettings();
const inventory = settings.userStats.inventory;
// Get current items
let currentString;
if (field === 'stored') {
currentString = inventory.stored[location] || 'None';
} else {
currentString = inventory[field] || 'None';
}
const items = parseItems(currentString);
items.splice(index, 1);
const newString = serializeItems(items);
// Save back
if (field === 'stored') {
inventory.stored[location] = newString;
} else {
inventory[field] = newString;
}
// Trigger change callback
if (onDataChange) {
onDataChange('inventory', field, newString, location);
}
// Re-render view
const widget = container.querySelector('.rpg-inventory-widget');
widget.querySelector('.rpg-inventory-views').innerHTML = renderActiveView(inventory, state);
attachInventoryHandlers(container, widgetId, inventory, state, dependencies);
}
/**
* Update item name
*/
function updateItemName(container, widgetId, field, index, newName, location, state, dependencies) {
const sanitized = sanitizeItemName(newName);
if (!sanitized) return;
const settings = getExtensionSettings();
const inventory = settings.userStats.inventory;
// Get current items
let currentString;
if (field === 'stored') {
currentString = inventory.stored[location] || 'None';
} else {
currentString = inventory[field] || 'None';
}
const items = parseItems(currentString);
items[index] = sanitized;
const newString = serializeItems(items);
// Save back
if (field === 'stored') {
inventory.stored[location] = newString;
} else {
inventory[field] = newString;
}
// Trigger change callback
if (onDataChange) {
onDataChange('inventory', field, newString, location);
}
}
/**
* Show add location form
*/
function showAddLocationForm(widget) {
const form = widget.querySelector('[data-form="add-location"]');
if (form) {
form.style.display = 'block';
const input = form.querySelector('input');
if (input) {
input.value = '';
input.focus();
}
}
}
/**
* Hide add location form
*/
function hideAddLocationForm(widget) {
const form = widget.querySelector('[data-form="add-location"]');
if (form) {
form.style.display = 'none';
const input = form.querySelector('input');
if (input) input.value = '';
}
}
/**
* Save new location
*/
function saveAddLocation(container, widgetId, state, dependencies) {
const widget = container.querySelector('.rpg-inventory-widget');
const form = widget.querySelector('[data-form="add-location"]');
if (!form) return;
const input = form.querySelector('input');
const rawLocationName = input.value.trim();
if (!rawLocationName) {
hideAddLocationForm(widget);
return;
}
const locationName = sanitizeLocationName(rawLocationName);
if (!locationName) {
showAlertDialog({
title: 'Invalid Location',
message: 'Please enter a valid location name.',
variant: 'warning'
});
hideAddLocationForm(widget);
return;
}
const settings = getExtensionSettings();
const inventory = settings.userStats.inventory;
// Check if location already exists
if (inventory.stored[locationName]) {
showAlertDialog({
title: 'Duplicate Location',
message: 'A location with this name already exists.',
variant: 'warning'
});
hideAddLocationForm(widget);
return;
}
// Add new location
inventory.stored[locationName] = 'None';
// Trigger change callback
if (onDataChange) {
onDataChange('inventory', 'stored', inventory.stored);
}
hideAddLocationForm(widget);
// Re-render view
widget.querySelector('.rpg-inventory-views').innerHTML = renderActiveView(inventory, state);
attachInventoryHandlers(container, widgetId, inventory, state, dependencies);
}
/**
* Toggle location collapse
*/
function toggleLocationCollapse(widget, location, state) {
const index = state.collapsedLocations.indexOf(location);
if (index === -1) {
state.collapsedLocations.push(location);
} else {
state.collapsedLocations.splice(index, 1);
}
// Update DOM
const locationDiv = widget.querySelector(`.rpg-storage-location[data-location="${location}"]`);
if (locationDiv) {
const content = locationDiv.querySelector('.rpg-storage-content');
const icon = locationDiv.querySelector('.rpg-storage-toggle i');
if (index === -1) {
// Now collapsed
locationDiv.classList.add('collapsed');
content.style.display = 'none';
icon.className = 'fa-solid fa-chevron-right';
} else {
// Now expanded
locationDiv.classList.remove('collapsed');
content.style.display = 'block';
icon.className = 'fa-solid fa-chevron-down';
}
}
}
/**
* Show remove location confirmation
*/
function showRemoveLocationConfirm(widget, location) {
const locationId = getLocationId(location);
const confirm = widget.querySelector(`[data-confirm="remove-location-${locationId}"]`);
if (confirm) {
confirm.style.display = 'block';
}
}
/**
* Hide remove location confirmation
*/
function hideRemoveLocationConfirm(widget, location) {
const locationId = getLocationId(location);
const confirm = widget.querySelector(`[data-confirm="remove-location-${locationId}"]`);
if (confirm) {
confirm.style.display = 'none';
}
}
/**
* Remove location
*/
function removeLocation(container, widgetId, location, state, dependencies) {
const settings = getExtensionSettings();
const inventory = settings.userStats.inventory;
delete inventory.stored[location];
// Remove from collapsed locations
const index = state.collapsedLocations.indexOf(location);
if (index !== -1) {
state.collapsedLocations.splice(index, 1);
}
// Trigger change callback
if (onDataChange) {
onDataChange('inventory', 'stored', inventory.stored);
}
// Re-render view
const widget = container.querySelector('.rpg-inventory-widget');
widget.querySelector('.rpg-inventory-views').innerHTML = renderActiveView(inventory, state);
attachInventoryHandlers(container, widgetId, inventory, state, dependencies);
}
}