// Magic Item Market filtering state and helpers let marketFilters = { name: '', faction: [], rarity: [], stock: [], attunement: [], costMin: '', costMax: '' }; let marketFilterFocus = null; let marketFilterOpenKey = null; let marketFilterSearch = { faction: '', rarity: '', stock: '', attunement: '' }; let marketTableScrollLeft = null; let marketFilterAllValues = { faction: [], rarity: [], stock: [], attunement: [] }; let marketFiltersInitialized = false; function renderMarketFilters(values) { // Build the filter row markup for the market table. const { factions, rarities, stocks, attunements } = values; return ` ${renderFilterDropdown('faction', 'Faction', factions, marketFilters.faction)} ${renderFilterDropdown('rarity', 'Rarity', rarities, marketFilters.rarity)} ${renderFilterDropdown('stock', 'Stock', stocks, marketFilters.stock, toTitleCase)}
${renderFilterDropdown('attunement', 'Attunement', attunements, marketFilters.attunement)} `; } 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); const attunements = getUniqueValues(items, item => item.attunementRequired); marketFilterAllValues = { faction: factions, rarity: rarities, stock: stocks, attunement: attunements }; if (!marketFiltersInitialized) { marketFilters.faction = [...factions]; marketFilters.rarity = [...rarities]; marketFilters.stock = [...stocks]; marketFilters.attunement = [...attunements]; marketFiltersInitialized = true; } return { factions, rarities, stocks, attunements }; } 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 attunement = marketFilters.attunement; 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 (attunement.length === 0 || !attunement.includes(item.attunementRequired)) 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; if (marketData && campaignData) { renderMarket(marketData, campaignData.reputation); } } 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); } if (marketData && campaignData) { renderMarket(marketData, campaignData.reputation); } } 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 }; } if (marketData && campaignData) { renderMarket(marketData, campaignData.reputation); } } function selectAllMarketFilter(key) { // Select all values for the given filter. const values = marketFilters[key]; if (!Array.isArray(values)) return; marketFilters[key] = [...(marketFilterAllValues[key] || [])]; if (marketData && campaignData) { renderMarket(marketData, campaignData.reputation); } } function toggleMarketFilterDropdown(key) { // Open/close the filter popover for the given column. marketFilterOpenKey = marketFilterOpenKey === key ? null : key; if (marketData && campaignData) { renderMarket(marketData, campaignData.reputation); } if (marketFilterOpenKey) { requestAnimationFrame(() => positionMarketFilterPopover(marketFilterOpenKey)); } } function updateMarketFilterSearch(key, value) { // Update popover search text and refresh the list. captureMarketFilterFocus(); marketFilterSearch[key] = value; if (marketData && campaignData) { renderMarket(marketData, campaignData.reputation); } } function closeMarketFilterDropdown() { // Close any open popover when clicking outside. if (marketFilterOpenKey !== null) { marketFilterOpenKey = null; if (marketData && campaignData) { renderMarket(marketData, campaignData.reputation); } } } 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; }