PageSourceSearch

https://wwmmap.pages.dev/js/map_2026/filters.js

js wwmmap.pages.dev collected 2026-10-03 06:05:55 UTC 13,136 bytes, 347 lines download raw bytes

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.