// Magic Item Market filtering state and helpers
let marketFilters = {
name: '',
faction: [],
rarity: [],
stock: [],
costMin: '',
costMax: ''
};
let marketFilterFocus = null;
let marketFilterOpenKey = null;
let marketFilterSearch = {
faction: '',
rarity: '',
stock: '',
};
let marketTableScrollLeft = null;
let marketFilterAllValues = {
faction: [],
rarity: [],
stock: [],
};
let marketFiltersInitialized = false;
let marketFilterDebounce = null;
function renderMarketFilters(values) {
// Build the filter row markup for the market table.
const { factions, rarities, stocks, } = values;
return `
|
|
${renderFilterDropdown('faction', 'Faction', factions, marketFilters.faction)}
|
${renderFilterDropdown('rarity', 'Rarity', rarities, marketFilters.rarity)}
|
${renderFilterDropdown('stock', 'Stock', stocks, marketFilters.stock, toTitleCase)}
|
|
`;
}
function getMarketFilterValues(items) {
// Derive filter options once per render, seeding defaults on first load.
const factions = getUniqueValues(items, item => item.faction);
const rarities = getRarityValues();
const stocks = getUniqueValues(items, item => item.stock, stockSort);
marketFilterAllValues = {
faction: factions,
rarity: rarities,
stock: stocks,
};
if (!marketFiltersInitialized) {
marketFilters.faction = [...factions];
marketFilters.rarity = [...rarities];
marketFilters.stock = [...stocks];
marketFiltersInitialized = true;
}
return { factions, rarities, stocks };
}
function getUniqueValues(items, getter, sorter) {
// Collect unique values and apply optional custom sort.
const values = new Set();
items.forEach(item => {
const value = getter(item);
if (value) values.add(value);
});
const list = Array.from(values);
if (sorter) {
return list.sort(sorter);
}
return list.sort((a, b) => a.localeCompare(b));
}
function renderFilterDropdown(key, label, values, selected, labelFormatter) {
// Compact filter UI with search and multi-select checkboxes.
const formatted = labelFormatter || (value => value);
const search = marketFilterSearch[key] || '';
const filteredValues = values.filter(value =>
value.toLowerCase().includes(search.toLowerCase())
);
// "All" is shown only when every value is selected; "NONE" means zero selected.
const isAllSelected = selected.length === values.length && values.length > 0;
const isNoneSelected = selected.length === 0;
const summary = isAllSelected
? 'All'
: isNoneSelected
? 'NONE'
: selected.length === 1
? formatted(selected[0])
: `${selected.length} selected`;
const options = filteredValues.map(value => {
const isChecked = selected.includes(value) ? ' checked' : '';
const display = escapeAttribute(formatted(value));
return `
`;
}).join('');
const openClass = marketFilterOpenKey === key ? ' show' : '';
return `
${options || '
No matches
'}
`;
}
function toTitleCase(value) {
// Title-case display helper for stock labels.
return value.replace(/\b\w/g, (char) => char.toUpperCase());
}
function stockSort(a, b) {
// Enforce stock order from most scarce to most available.
const order = ["Very Limited", "Limited", "Modest", "Abundant", "Very Abundant"];
return order.indexOf(toTitleCase(a)) - order.indexOf(toTitleCase(b));
}
function raritySort(a, b) {
// Enforce rarity order from rarest to most common.
const order = ["Unique", "Artifact", "Legendary", "Very Rare", "Rare", "Uncommon", "Common"];
const indexA = order.indexOf(a);
const indexB = order.indexOf(b);
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
}
function getRarityValues() {
// Use full dataset so rarities aren't limited by current visibility.
if (typeof MAGIC_ITEMS_DATA === 'undefined') return [];
const values = new Set();
MAGIC_ITEMS_DATA.forEach(item => {
if (item.rarity) values.add(item.rarity);
});
const list = Array.from(values);
return list.sort(raritySort);
}
function applyMarketFilters(items) {
// Empty selections mean "show none" for that filter (intentional for clear state).
const name = marketFilters.name.trim().toLowerCase();
const faction = marketFilters.faction;
const rarity = marketFilters.rarity;
const stock = marketFilters.stock;
const costMin = marketFilters.costMin === '' ? null : Number(marketFilters.costMin);
const costMax = marketFilters.costMax === '' ? null : Number(marketFilters.costMax);
return items.filter(item => {
if (name && !item.name.toLowerCase().includes(name)) return false;
if (faction.length === 0 || !faction.includes(item.faction)) return false;
if (rarity.length === 0 || !rarity.includes(item.rarity)) return false;
if (stock.length === 0 || !stock.includes(item.stock)) return false;
if (costMin !== null && item.adjustedPrice < costMin) return false;
if (costMax !== null && item.adjustedPrice > costMax) return false;
return true;
});
}
function updateMarketFilter(key, value) {
// Preserve focus while re-rendering.
captureMarketFilterFocus();
marketFilters[key] = value;
scheduleMarketFilterRender();
}
function toggleMarketFilterValue(key, value, isChecked) {
// Toggle a checkbox value within the active selection array.
const values = marketFilters[key];
if (!Array.isArray(values)) return;
const index = values.indexOf(value);
if (isChecked && index === -1) {
values.push(value);
} else if (!isChecked && index >= 0) {
values.splice(index, 1);
}
scheduleMarketFilterRender();
}
function clearMarketFilter(key) {
// Clear selection or input for the given filter key.
captureMarketFilterFocus();
if (Array.isArray(marketFilters[key])) {
marketFilters[key] = [];
} else {
marketFilters[key] = '';
}
if (key === 'costMin' || key === 'costMax' || key === 'name') {
marketFilterFocus = {
key,
selectionStart: 0,
selectionEnd: 0
};
}
scheduleMarketFilterRender();
}
function selectAllMarketFilter(key) {
// Select all values for the given filter.
const values = marketFilters[key];
if (!Array.isArray(values)) return;
marketFilters[key] = [...(marketFilterAllValues[key] || [])];
scheduleMarketFilterRender();
}
function toggleMarketFilterDropdown(key) {
// Open/close the filter popover for the given column.
marketFilterOpenKey = marketFilterOpenKey === key ? null : key;
scheduleMarketFilterRender();
if (marketFilterOpenKey) {
requestAnimationFrame(() => positionMarketFilterPopover(marketFilterOpenKey));
}
}
function updateMarketFilterSearch(key, value) {
// Update popover search text and refresh the list.
captureMarketFilterFocus();
marketFilterSearch[key] = value;
scheduleMarketFilterRender();
}
function closeMarketFilterDropdown() {
// Close any open popover when clicking outside.
if (marketFilterOpenKey !== null) {
captureMarketFilterFocus();
marketFilterOpenKey = null;
scheduleMarketFilterRender();
}
}
function positionMarketFilterPopover(key) {
// Keep the popover within the viewport on narrow screens.
const dropdown = document.querySelector(`.market-filter-dropdown[data-filter-key="${key}"]`);
if (!dropdown) return;
const trigger = dropdown.querySelector('.market-filter-trigger');
const popover = dropdown.querySelector('.market-filter-popover');
if (!popover || !trigger) return;
const margin = 12;
const viewportWidth = window.innerWidth;
const triggerRect = trigger.getBoundingClientRect();
popover.style.position = 'absolute';
popover.style.left = '0';
popover.style.right = 'auto';
popover.style.transform = 'none';
const popoverRect = popover.getBoundingClientRect();
const popoverWidth = popoverRect.width;
const maxLeft = Math.max(margin, viewportWidth - popoverWidth - margin);
const left = Math.min(Math.max(triggerRect.left, margin), maxLeft);
const shiftLeft = triggerRect.left - left;
if (shiftLeft !== 0) {
popover.style.transform = `translateX(-${shiftLeft}px)`;
}
}
function captureMarketFilterFocus() {
// Track cursor placement to avoid input blur on re-render.
const el = document.activeElement;
if (!el || !el.dataset) return;
if (el.dataset.filterKey) {
marketFilterFocus = {
type: 'filter',
key: el.dataset.filterKey,
selectionStart: el.selectionStart,
selectionEnd: el.selectionEnd
};
} else if (el.dataset.filterSearchKey) {
marketFilterFocus = {
type: 'search',
key: el.dataset.filterSearchKey,
selectionStart: el.selectionStart,
selectionEnd: el.selectionEnd
};
}
}
function restoreMarketFilterFocus() {
// Restore focus and caret to the active filter input.
if (!marketFilterFocus) return;
const selector = marketFilterFocus.type === 'search'
? `[data-filter-search-key="${marketFilterFocus.key}"]`
: `[data-filter-key="${marketFilterFocus.key}"]`;
const el = document.querySelector(selector);
if (!el) return;
el.focus();
if (typeof el.selectionStart === 'number') {
el.setSelectionRange(marketFilterFocus.selectionStart, marketFilterFocus.selectionEnd);
}
marketFilterFocus = null;
}
function captureMarketTableScroll() {
// Preserve horizontal scroll when re-rendering on mobile.
const wrapper = document.querySelector('.market-table-wrapper');
if (!wrapper) return;
marketTableScrollLeft = wrapper.scrollLeft;
}
function restoreMarketTableScroll() {
// Restore horizontal scroll position after re-render.
if (marketTableScrollLeft === null) return;
const wrapper = document.querySelector('.market-table-wrapper');
if (!wrapper) return;
wrapper.scrollLeft = marketTableScrollLeft;
marketTableScrollLeft = null;
}
function scheduleMarketFilterRender() {
clearTimeout(marketFilterDebounce);
marketFilterDebounce = setTimeout(() => {
if (marketData && campaignData) {
renderMarket(marketData, campaignData.reputation);
}
}, 250);
}