1import { i18n } from './i18n.js'; 2 3// categories to disable by default 4const disableByDefault = [10000010004, 17310010084, 10000001084, 10000000006]; 5const disableGroupsByDefault = ["bác váºt"]; 6const STORAGE_KEY = 'wwmmap_filter_state_2026'; 7const UM_FILTER_KEY = 'wwmmap_user_marker_filter_2026'; 8 9export class FilterManager { 10 constructor(containerId, markerManager) { 11 this.container = document.getElementById(containerId); 12 this.markerManager = markerManager; 13 this.categories = []; 14 this.groups = []; 15 } 16 17 saveFilterState() { 18 const checked = Array.from(this.container.querySelectorAll('.category-item input:checked')).map(cb => cb.value); 19 try { 20 localStorage.setItem(STORAGE_KEY, JSON.stringify(checked)); 21 } catch (e) { /* quota exceeded or unavailable */ } 22 } 23 24 loadFilterState() { 25 try { 26 const raw = localStorage.getItem(STORAGE_KEY); 27 if (raw) return new Set(JSON.parse(raw)); 28 } catch (e) { /* corrupted or unavailable */ } 29 return null; 30 } 31 32 async init() { 33 // Load categories and groups 34 try { 35 const response = await fetch('data/categories_2026.json'); 36 const data = await response.json(); 37 this.groups = data.groups; 38 this.categories = data.categories; 39 this.render(); 40 41 // Listen for language changes to re-render 42 window.addEventListener('languageChanged', () => { 43 // Save checked state 44 const checked = new Set(Array.from(this.container.querySelectorAll('.category-item input:checked')).map(cb => cb.value)); 45 this.render(); 46 // Restore checked state 47 const checkboxes = this.container.querySelectorAll('.category-item input[type="checkbox"]'); 48 checkboxes.forEach(cb => { 49 cb.checked = checked.has(cb.value); 50 }); 51 52 // Update group toggles 53 this.container.querySelectorAll('.category-group').forEach(group => { 54 const groupToggle = group.querySelector('.group-toggle'); 55 const itemCheckboxes = Array.from(group.querySelectorAll('.category-item input')); 56 if (groupToggle) { 57 this.updateGroupToggleState(groupToggle, itemCheckboxes); 58 } 59 }); 60 }); 61 62 window.addEventListener('mapChanged', () => this.updateCounts()); 63 window.addEventListener('markerCompleted', () => this.updateCounts()); 64 } catch (e) { 65 console.error("Failed to load categories", e); 66 } 67 68 this.initControls(); 69 } 70 71 initControls() { 72 const btnShowAll = document.getElementById('btn-show-all'); 73 const btnHideAll = document.getElementById('btn-hide-all'); 74 const toggleCompleted = document.getElementById('toggle-completed'); 75 const labelHideCompleted = document.getElementById('label-hide-completed'); 76 const updateTexts = () => { 77 if (btnShowAll) btnShowAll.innerText = i18n.t('ui', 'show_all'); 78 if (btnHideAll) btnHideAll.innerText = i18n.t('ui', 'hide_all'); 79 if (labelHideCompleted) labelHideCompleted.innerText = i18n.t('ui', 'hide_completed'); 80 }; 81 82 if (btnShowAll) btnShowAll.addEventListener('click', () => this.toggleAll(true)); 83 if (btnHideAll) btnHideAll.addEventListener('click', () => this.toggleAll(false)); 84 85 if (toggleCompleted) { 86 toggleCompleted.addEventListener('change', (e) => { 87 this.markerManager.setHideCompleted(e.target.checked); 88 }); 89 } 90 91 updateTexts(); 92 window.addEventListener('languageChanged', () => { 93 updateTexts(); 94 this.onFilterChange(); // To update any text that depends on language 95 }); 96 } 97 98 toggleAll(show) { 99 const checkboxes = this.container.querySelectorAll('input[type="checkbox"]'); 100 checkboxes.forEach(cb => cb.checked = show); 101 this.onFilterChange(); 102 } 103 104 render() { 105 this.container.innerHTML = ''; 106 107 this.groups.forEach(group => { 108 const groupEl = document.createElement('div'); 109 groupEl.className = 'category-group'; 110 groupEl.dataset.groupId = group.id; 111 const title = document.createElement('h3'); 112 title.style.display = 'flex'; 113 title.style.justifyContent = 'space-between'; 114 title.style.alignItems = 'center'; 115 title.style.cursor = 'pointer'; 116 117 const getGroupName = (g) => { 118 return g.names[i18n.lang] || g.names.en || g.names.vi || g.names.zh; 119 }; 120 const isDisabledByDefault = disableGroupsByDefault.includes(group.names.vi.toLowerCase()); 121 122 const titleText = document.createElement('span'); 123 titleText.innerText = getGroupName(group); 124 title.appendChild(titleText); 125 126 const groupToggle = document.createElement('input'); 127 groupToggle.type = 'checkbox'; 128 groupToggle.className = 'group-toggle sword-check'; 129 groupToggle.checked = true; 130 groupToggle.title = 'Toggle Group'; 131 132 title.appendChild(groupToggle); 133 134 title.addEventListener('click', (e) => { 135 if (e.target !== groupToggle) { 136 groupToggle.checked = !groupToggle.checked; 137 groupToggle.dispatchEvent(new Event('change'));
138 } 139 }); 140 141 groupEl.appendChild(title); 142 143 const groupCategories = this.categories.filter(c => c.groupId === group.id); 144 const itemCheckboxes = []; 145 const saved = this.loadFilterState(); 146 147 groupCategories.forEach(cat => { 148 const item = document.createElement('label'); 149 item.className = 'category-item'; 150 151 const checkbox = document.createElement('input'); 152 checkbox.type = 'checkbox'; 153 checkbox.className = 'sword-check'; 154 checkbox.value = cat.id; 155 checkbox.checked = saved ? saved.has(String(cat.id)) : !disableByDefault.includes(cat.id); 156 157 itemCheckboxes.push(checkbox); 158 159 checkbox.addEventListener('change', () => { 160 this.onFilterChange(); 161 this.updateGroupToggleState(groupToggle, itemCheckboxes); 162 }); 163 164 const icon = document.createElement('img'); 165 icon.src = `images/icons/${cat.id}.png`; 166 icon.onerror = () => { icon.src = 'images/icons/default.svg'; }; // Fallback 167 168 const name = document.createElement('span'); 169 name.className = 'category-name'; 170 171 item.appendChild(checkbox); 172 item.appendChild(icon); 173 item.appendChild(name); 174 groupEl.appendChild(item); 175 }); 176 177 groupToggle.addEventListener('change', (e) => { 178 const checked = e.target.checked; 179 itemCheckboxes.forEach(cb => { 180 cb.checked = checked; 181 }); 182 this.onFilterChange(); 183 }); 184 if (!saved && isDisabledByDefault) { 185 groupToggle.checked = false; 186 groupToggle.dispatchEvent(new Event('change')); 187 } else if (saved) { 188 this.updateGroupToggleState(groupToggle, itemCheckboxes); 189 } 190 this.container.appendChild(groupEl); 191 }); 192 193 // Initial filter apply 194 this.onFilterChange(); 195 this.updateCounts(); 196 } 197 198 getCategoryCounts(categoryId) { 199 const currentMapId = this.markerManager.currentMapId; 200 const markers = this.markerManager.markerData.filter(m => 201 m.mapId == currentMapId && String(m.category) === String(categoryId) 202 ); 203 const total = markers.length; 204 const completed = markers.filter(m => m.completed).length; 205 return { total, completed }; 206 } 207 208 updateCounts() { 209 const items = this.container.querySelectorAll('.category-item'); 210 items.forEach(item => { 211 const checkbox = item.querySelector('input'); 212 const nameSpan = item.querySelector('.category-name'); 213 const catId = checkbox.value; 214 215 const cat = this.categories.find(c => String(c.id) === String(catId)); 216 if (!cat) return; 217 218 const { total, completed } = this.getCategoryCounts(catId); 219 220 if (total === 0) { 221 item.style.display = 'none'; 222 } else { 223 item.style.display = ''; 224 } 225 226 const countText = completed > 0 ? ` (${completed}/${total})` : ` (${total})`; 227 228 const getCatName = (obj) => { 229 return obj.names[i18n.lang] || obj.names.en || obj.names.vi || obj.names.zh; 230 }; 231 232 nameSpan.innerText = getCatName(cat) + countText; 233 }); 234 235 // Hide empty groups 236 this.container.querySelectorAll('.category-group').forEach(group => { 237 const visibleItems = Array.from(group.querySelectorAll('.category-item')).filter(item => item.style.display !== 'none'); 238 if (visibleItems.length === 0) { 239 group.style.display = 'none'; 240 } else { 241 group.style.display = ''; 242 } 243 }); 244 } 245 246 updateGroupToggleState(groupToggle, itemCheckboxes) { 247 const allChecked = itemCheckboxes.every(cb => cb.checked); 248 const someChecked = itemCheckboxes.some(cb => cb.checked); 249 groupToggle.checked = allChecked; 250 groupToggle.indeterminate = someChecked && !allChecked; 251 } 252 253 onFilterChange() { 254 const checked = Array.from(this.container.querySelectorAll('.category-item input:checked')).map(cb => cb.value); 255 this.saveFilterState(); 256 this.markerManager.renderMarkers(checked); 257 } 258 259 /** 260 * Add a "User Marker" category item to the Locations group in the filter list.
261 * @param {import('./user_markers').UserMarkerManager} userMarkerManager 262 */ 263 addUserMarkerCategory(userMarkerManager) { 264 this._userMarkerManager = userMarkerManager; 265 266 const _addItem = () => { 267 // Find the Locations group (id 1731001004) 268 const locGroup = this.container.querySelector('.category-group[data-group-id="1731001004"]'); 269 if (!locGroup) return; 270 271 // Remove previous user marker item if exists (from re-render) 272 const prev = locGroup.querySelector('.user-marker-item'); 273 if (prev) prev.remove(); 274 275 const item = document.createElement('label'); 276 item.className = 'category-item user-marker-item'; 277 278 const checkbox = document.createElement('input'); 279 checkbox.type = 'checkbox'; 280 checkbox.className = 'sword-check'; 281 checkbox.value = 'user_marker'; 282 283 const umSaved = localStorage.getItem(UM_FILTER_KEY); 284 checkbox.checked = umSaved !== null ? umSaved === '1' : false; 285 286 const icon = document.createElement('img'); 287 icon.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent('<svg width="48" height="64" viewBox="0 0 48 64" xmlns="http://www.w3.org/2000/svg"><path d="M24 0C13 0 4 9 4 20c0 15 20 40 20 40s20-25 20-40C44 9 35 0 24 0z" fill="#333333" stroke="#ffffff" stroke-width="2"/><circle cx="24" cy="20" r="8" fill="#ffffff"/></svg>'); 288 icon.style.width = '20px'; 289 icon.style.height = '20px'; 290 291 const name = document.createElement('span'); 292 name.className = 'category-name'; 293 294 const updateName = () => { 295 const counts = userMarkerManager.getCountForCurrentMap(); 296 const label = i18n.t('ui', 'user_marker'); 297 name.innerText = counts.total > 0 ? `${label} (${counts.total})` : label; 298 }; 299 updateName(); 300 301 checkbox.addEventListener('change', () => { 302 userMarkerManager.setEnabled(checkbox.checked); 303 localStorage.setItem(UM_FILTER_KEY, checkbox.checked ? '1' : '0'); 304 }); 305 306 item.appendChild(checkbox); 307 item.appendChild(icon); 308 item.appendChild(name); 309 locGroup.appendChild(item); 310 311 // Apply initial state 312 userMarkerManager.setEnabled(checkbox.checked); 313 314 // Wire into the group toggle 315 const groupToggle = locGroup.querySelector('.group-toggle'); 316 if (groupToggle) { 317 groupToggle.addEventListener('change', () => { 318 checkbox.checked = groupToggle.checked; 319 userMarkerManager.setEnabled(checkbox.checked); 320 localStorage.setItem(UM_FILTER_KEY, checkbox.checked ? '1' : '0'); 321 }); 322 } 323 324 return updateName; 325 }; 326 327 let currentUpdateName = _addItem(); 328 329 // Re-add after language change re-renders 330 window.addEventListener('languageChanged', () => { 331 setTimeout(() => { 332 const fn = _addItem(); 333 if (fn) currentUpdateName = fn; 334 }, 0); 335 }); 336 337 // Update counts on map/marker changes 338 window.addEventListener('mapChanged', () => currentUpdateName && currentUpdateName()); 339 340 // Listen for user marker changes to update count 341 const origInject = userMarkerManager._injectIntoMarkerManager.bind(userMarkerManager); 342 userMarkerManager._injectIntoMarkerManager = async function() { 343 await origInject.call(this); 344 if (currentUpdateName) currentUpdateName(); 345 }; 346 } 347}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.