feat(inventory): add list/grid view modes with individual item management
Implemented comprehensive individual item management system with toggleable view modes: - Added item parsing utilities (parseItems/serializeItems) for comma-separated strings - Implemented list view (full-width rows) and grid view (responsive cards) - Added view mode toggle buttons per inventory section (onPerson, stored, assets) - View preferences persist per-section in settings - Replaced text-based editing with add/remove item controls - Added inline forms for adding new items (matching existing UX patterns) - Applied theme accent color (--rpg-highlight) to all outlines and active states - Updated all tabs (desktop/mobile/inventory subtabs) with theme-consistent styling Technical improvements: - Created itemParser.js utility module for item string manipulation - Enhanced inventory rendering with conditional list/grid HTML generation - Added switchViewMode handler with settings persistence - Fixed [object Object] display bug with comprehensive type checking - All buttons and items now use transparent backgrounds with theme accent borders
This commit is contained in:
+6
-1
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
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,19 +354,50 @@ 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;
|
||||
|
||||
if (field === 'onPerson') {
|
||||
handleOnPersonBlur(element);
|
||||
} else if (field === 'stored') {
|
||||
const location = $(this).data('location');
|
||||
handleStoredLocationBlur(element, location);
|
||||
} else if (field === 'assets') {
|
||||
handleAssetsBlur(element);
|
||||
showAddItemForm(field, location);
|
||||
});
|
||||
|
||||
// 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
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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,41 +32,109 @@ 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 = '<div class="rpg-inventory-empty">No items carried</div>';
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" data-field="onPerson" data-index="${index}">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view';
|
||||
|
||||
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">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" 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="onPerson" data-view="grid" title="Grid view">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="onPerson" title="Add new item">
|
||||
<i class="fa-solid fa-plus"></i> Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inventory-content">
|
||||
<div class="rpg-inventory-text rpg-editable" contenteditable="true" data-field="onPerson" title="Click to edit">${escapeHtml(displayText)}</div>
|
||||
<div class="rpg-inline-form" id="rpg-add-item-form-onPerson" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-onPerson" 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 ${listViewClass}">
|
||||
${itemsHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the "Stored" inventory view with collapsible locations
|
||||
* Renders the "Stored" inventory view with collapsible locations and list/grid views
|
||||
* @param {Object.<string, string>} 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 = `
|
||||
<div class="rpg-inventory-section" data-section="stored">
|
||||
<div class="rpg-inventory-header">
|
||||
<h4>Storage Locations</h4>
|
||||
<div class="rpg-inventory-header-actions">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="stored" 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="stored" data-view="grid" title="Grid view">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-location" title="Add new storage location">
|
||||
<i class="fa-solid fa-plus"></i> Add Location
|
||||
</button>
|
||||
</div>
|
||||
</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..." />
|
||||
@@ -88,8 +157,40 @@ export function renderStoredView(stored, collapsedLocations = []) {
|
||||
`;
|
||||
} else {
|
||||
for (const location of locations) {
|
||||
const items = stored[location];
|
||||
const itemString = stored[location];
|
||||
const items = parseItems(itemString);
|
||||
const isCollapsed = collapsedLocations.includes(location);
|
||||
const locationId = escapeHtml(location).replace(/\s+/g, '-');
|
||||
|
||||
let itemsHtml = '';
|
||||
if (items.length === 0) {
|
||||
itemsHtml = '<div class="rpg-inventory-empty">No items stored here</div>';
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view';
|
||||
|
||||
html += `
|
||||
<div class="rpg-storage-location ${isCollapsed ? 'collapsed' : ''}" data-location="${escapeHtml(location)}">
|
||||
<div class="rpg-storage-header">
|
||||
@@ -98,15 +199,31 @@ export function renderStoredView(stored, collapsedLocations = []) {
|
||||
</button>
|
||||
<h5 class="rpg-storage-name">${escapeHtml(location)}</h5>
|
||||
<div class="rpg-storage-actions">
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="stored" data-location="${escapeHtml(location)}" title="Add item to this location">
|
||||
<i class="fa-solid fa-plus"></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 rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" title="Click to edit">${escapeHtml(items || 'None')}</div>
|
||||
<div class="rpg-inline-form" id="rpg-add-item-form-stored-${locationId}" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input rpg-location-item-input" data-location="${escapeHtml(location)}" 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 class="rpg-inline-confirmation" id="rpg-remove-confirm-${escapeHtml(location).replace(/\s+/g, '-')}" style="display: none;">
|
||||
</div>
|
||||
<div class="rpg-item-list ${listViewClass}">
|
||||
${itemsHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inline-confirmation" id="rpg-remove-confirm-${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)}">
|
||||
@@ -131,19 +248,76 @@ export function renderStoredView(stored, collapsedLocations = []) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the "Assets" inventory view
|
||||
* Renders the "Assets" inventory view with list or grid display
|
||||
* @param {string} assets - Current assets (vehicles, property, equipment)
|
||||
* @returns {string} HTML for assets view with edit controls
|
||||
* @param {string} viewMode - View mode ('list' or 'grid')
|
||||
* @returns {string} HTML for assets view with items and add button
|
||||
*/
|
||||
export function renderAssetsView(assets) {
|
||||
const displayText = assets || 'None';
|
||||
export function renderAssetsView(assets, viewMode = 'list') {
|
||||
const items = parseItems(assets);
|
||||
|
||||
let itemsHtml = '';
|
||||
if (items.length === 0) {
|
||||
itemsHtml = '<div class="rpg-inventory-empty">No assets owned</div>';
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
<div class="rpg-item-card" data-field="assets" data-index="${index}">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
const listViewClass = viewMode === 'list' ? 'rpg-item-list-view' : 'rpg-item-grid-view';
|
||||
|
||||
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">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="assets" 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="assets" data-view="grid" title="Grid view">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="assets" title="Add new asset">
|
||||
<i class="fa-solid fa-plus"></i> Add Asset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpg-inventory-content">
|
||||
<div class="rpg-inventory-text rpg-editable" contenteditable="true" data-field="assets" title="Click to edit">${escapeHtml(displayText)}</div>
|
||||
<div class="rpg-inline-form" id="rpg-add-item-form-assets" style="display: none;">
|
||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-assets" 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 ${listViewClass}">
|
||||
${itemsHtml}
|
||||
</div>
|
||||
<div class="rpg-inventory-hint">
|
||||
<i class="fa-solid fa-info-circle"></i>
|
||||
Assets include vehicles (cars, motorcycles), property (homes, apartments),
|
||||
@@ -189,25 +363,43 @@ function generateInventoryHTML(inventory, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
// 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 = `
|
||||
<div class="rpg-inventory-container">
|
||||
${renderInventorySubTabs(activeSubTab)}
|
||||
<div class="rpg-inventory-views">
|
||||
`;
|
||||
|
||||
// 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);
|
||||
html += renderOnPersonView(v2Inventory.onPerson, viewModes.onPerson);
|
||||
break;
|
||||
case 'stored':
|
||||
html += renderStoredView(v2Inventory.stored, collapsedLocations);
|
||||
html += renderStoredView(v2Inventory.stored, collapsedLocations, viewModes.stored);
|
||||
break;
|
||||
case 'assets':
|
||||
html += renderAssetsView(v2Inventory.assets);
|
||||
html += renderAssetsView(v2Inventory.assets, viewModes.assets);
|
||||
break;
|
||||
default:
|
||||
html += renderOnPersonView(v2Inventory.onPerson);
|
||||
html += renderOnPersonView(v2Inventory.onPerson, viewModes.onPerson);
|
||||
}
|
||||
|
||||
html += `
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Item Parser Module
|
||||
* Utilities for parsing item strings into arrays and vice versa
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parses a comma-separated item string into an array of trimmed item names.
|
||||
* Filters out empty strings and handles "None" gracefully.
|
||||
*
|
||||
* @param {string} itemString - Comma-separated items (e.g., "Sword, Shield, 3x Potions")
|
||||
* @returns {string[]} Array of item names, or empty array if none
|
||||
*
|
||||
* @example
|
||||
* parseItems("Sword, Shield, 3x Potions") // ["Sword", "Shield", "3x Potions"]
|
||||
* parseItems("None") // []
|
||||
* parseItems("") // []
|
||||
* parseItems(null) // []
|
||||
*/
|
||||
export function parseItems(itemString) {
|
||||
// Handle null/undefined/non-string
|
||||
if (!itemString || typeof itemString !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Trim and check for "None" (case-insensitive)
|
||||
const trimmed = itemString.trim();
|
||||
if (trimmed === '' || trimmed.toLowerCase() === 'none') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Split by comma, trim each item, filter empties
|
||||
return itemString
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(item => item !== '' && item.toLowerCase() !== 'none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an array of items back into a comma-separated string.
|
||||
* Returns "None" for empty arrays.
|
||||
*
|
||||
* @param {string[]} itemArray - Array of item names
|
||||
* @returns {string} Comma-separated string, or "None" if empty
|
||||
*
|
||||
* @example
|
||||
* serializeItems(["Sword", "Shield", "3x Potions"]) // "Sword, Shield, 3x Potions"
|
||||
* serializeItems([]) // "None"
|
||||
* serializeItems(["Sword"]) // "Sword"
|
||||
*/
|
||||
export function serializeItems(itemArray) {
|
||||
// Handle null/undefined/non-array
|
||||
if (!itemArray || !Array.isArray(itemArray)) {
|
||||
return 'None';
|
||||
}
|
||||
|
||||
// Filter out empty strings and trim
|
||||
const cleaned = itemArray
|
||||
.filter(item => item && typeof item === 'string' && item.trim() !== '')
|
||||
.map(item => item.trim());
|
||||
|
||||
// Return "None" if array is empty after cleaning
|
||||
if (cleaned.length === 0) {
|
||||
return 'None';
|
||||
}
|
||||
|
||||
// Join with comma and space
|
||||
return cleaned.join(', ');
|
||||
}
|
||||
@@ -3421,9 +3421,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
}
|
||||
|
||||
.rpg-mobile-tab.active {
|
||||
color: var(--SmartThemeQuoteColor);
|
||||
border-bottom-color: var(--SmartThemeQuoteColor);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--rpg-highlight);
|
||||
border-bottom-color: var(--rpg-highlight);
|
||||
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
|
||||
}
|
||||
|
||||
.rpg-mobile-tab i {
|
||||
@@ -3939,8 +3939,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
.rpg-inventory-subtab {
|
||||
flex: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--SmartThemeBlurTintColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
background: transparent;
|
||||
border: 2px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
cursor: pointer;
|
||||
@@ -3949,12 +3949,15 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
}
|
||||
|
||||
.rpg-inventory-subtab:hover {
|
||||
background: var(--SmartThemeQuoteColor);
|
||||
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
|
||||
border-color: var(--rpg-highlight);
|
||||
color: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
.rpg-inventory-subtab.active {
|
||||
background: var(--SmartThemeEmColor);
|
||||
border-color: var(--ac-style-color-matchedText);
|
||||
background: transparent;
|
||||
border-color: var(--rpg-highlight);
|
||||
color: var(--rpg-highlight);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -4005,8 +4008,8 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
|
||||
.rpg-inventory-hint {
|
||||
padding: 0.5rem;
|
||||
background: var(--SmartThemeQuoteColor);
|
||||
border-left: 3px solid var(--ac-style-color-matchedText);
|
||||
background: transparent;
|
||||
border: 2px solid var(--rpg-highlight);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--SmartThemeFastUISliderColColor);
|
||||
@@ -4097,13 +4100,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
}
|
||||
|
||||
.rpg-inventory-add-btn {
|
||||
background: var(--ac-style-color-matchedText);
|
||||
border-color: var(--ac-style-color-matchedText);
|
||||
color: white;
|
||||
background: transparent;
|
||||
border-color: var(--rpg-highlight);
|
||||
color: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
.rpg-inventory-add-btn:hover {
|
||||
opacity: 0.85;
|
||||
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
|
||||
border-color: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
.rpg-inventory-remove-btn {
|
||||
@@ -4232,6 +4236,178 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ITEM LIST AND GRID VIEW STYLES
|
||||
============================================ */
|
||||
|
||||
/* Item list container base styles */
|
||||
.rpg-item-list {
|
||||
min-height: 2rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* LIST VIEW - Full-width rows */
|
||||
.rpg-item-list-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.rpg-item-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: transparent;
|
||||
border: 2px solid var(--rpg-highlight);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.rpg-item-row:hover {
|
||||
border-color: var(--rpg-highlight);
|
||||
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
|
||||
}
|
||||
|
||||
.rpg-item-row .rpg-item-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rpg-item-row .rpg-item-remove {
|
||||
flex-shrink: 0;
|
||||
padding: 0.3rem 0.6rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--SmartThemeFastUISliderColColor);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.rpg-item-row .rpg-item-remove:hover {
|
||||
background: #dc3545;
|
||||
border-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* GRID VIEW - Responsive card grid */
|
||||
.rpg-item-grid-view {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.rpg-item-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 0.75rem;
|
||||
background: transparent;
|
||||
border: 2px solid var(--rpg-highlight);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--SmartThemeBodyColor);
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.rpg-item-card:hover {
|
||||
border-color: var(--rpg-highlight);
|
||||
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
|
||||
}
|
||||
|
||||
.rpg-item-card .rpg-item-name {
|
||||
text-align: center;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.rpg-item-card .rpg-item-remove {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
padding: 0;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
color: var(--SmartThemeFastUISliderColColor);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rpg-item-card .rpg-item-remove:hover {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Empty state message */
|
||||
.rpg-inventory-empty {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: var(--SmartThemeFastUISliderColColor);
|
||||
font-style: italic;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* View Toggle Buttons */
|
||||
.rpg-inventory-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.rpg-view-toggle {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: var(--SmartThemeQuoteColor);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.15rem;
|
||||
}
|
||||
|
||||
.rpg-view-btn {
|
||||
padding: 0.35rem 0.6rem;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.15rem;
|
||||
color: var(--SmartThemeFastUISliderColColor);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rpg-view-btn:hover {
|
||||
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
|
||||
border-color: var(--rpg-highlight);
|
||||
color: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
.rpg-view-btn.active {
|
||||
background: transparent;
|
||||
border-color: var(--rpg-highlight);
|
||||
color: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
DESKTOP TABS SYSTEM
|
||||
============================================ */
|
||||
@@ -4273,14 +4449,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
}
|
||||
|
||||
.rpg-tab-btn:hover {
|
||||
background: var(--SmartThemeQuoteColor);
|
||||
color: var(--ac-style-color-matchedText);
|
||||
background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1);
|
||||
color: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
.rpg-tab-btn.active {
|
||||
background: var(--SmartThemeQuoteColor);
|
||||
border-bottom-color: var(--ac-style-color-matchedText);
|
||||
color: var(--ac-style-color-matchedText);
|
||||
background: transparent;
|
||||
border-bottom-color: var(--rpg-highlight);
|
||||
color: var(--rpg-highlight);
|
||||
}
|
||||
|
||||
.rpg-tab-btn i {
|
||||
|
||||
Reference in New Issue
Block a user