1616 } ,
1717
1818 allSubs : [ ] ,
19+ isUpdatingMultiselect : false ,
20+ isInitialized : false ,
1921
2022 // Initialize the filter manager
2123 init : function ( ) {
24+ // Prevent multiple initializations
25+ if ( this . isInitialized ) {
26+ return ;
27+ }
28+ this . isInitialized = true ;
29+
2230 this . loadState ( ) ;
2331 this . bindEvents ( ) ;
2432 this . applyInitialFilters ( ) ;
2533 } ,
2634
2735 // Load filter state from localStorage and URL
2836 loadState : function ( ) {
29- // Get all available subcategories
37+ // Get all available subcategories from multiselect or fallback to known categories
3038 this . allSubs = [ ] ;
3139 $ ( '#subject-select option' ) . each ( ( i , opt ) => {
3240 if ( opt . value ) this . allSubs . push ( opt . value ) ;
3341 } ) ;
3442
43+ // If multiselect doesn't exist or has no options, use default categories
44+ if ( this . allSubs . length === 0 ) {
45+ this . allSubs = [ 'PY' , 'SCIPY' , 'DATA' , 'WEB' , 'BIZ' , 'GEO' , 'CAMP' , 'DAY' ] ;
46+ }
47+
3548 // Check URL parameters first (highest priority)
3649 const urlParams = new URLSearchParams ( window . location . search ) ;
3750 const urlSubs = urlParams . get ( 'sub' ) ;
4255 // Fall back to localStorage
4356 const stored = store . get ( this . STORAGE_DOMAIN + '-subs' ) ;
4457 if ( stored && ! this . isDataExpired ( stored ) ) {
45- this . currentFilters . subs = stored . subs || [ ] ;
58+ // If stored filters are all categories, treat as empty (show all)
59+ if ( stored . subs && stored . subs . length === this . allSubs . length ) {
60+ this . currentFilters . subs = [ ] ;
61+ } else {
62+ this . currentFilters . subs = stored . subs || [ ] ;
63+ }
4664 } else {
47- // Default to all categories
48- this . currentFilters . subs = [ ... this . allSubs ] ;
65+ // Default to empty (show all) instead of listing all categories
66+ this . currentFilters . subs = [ ] ;
4967 }
5068 }
5169 } ,
7795
7896 // Apply filters to conference cards
7997 applyFilters : function ( ) {
80- // Hide all conferences first
81- $ ( '.ConfItem' ) . hide ( ) ;
82-
83- // Show filtered conferences
84- if ( this . currentFilters . subs . length === 0 ||
85- this . currentFilters . subs . length === this . allSubs . length ) {
98+ // If no filters or all filters selected, show all conferences
99+ if ( ! this . currentFilters . subs ||
100+ this . currentFilters . subs . length === 0 ||
101+ ( this . allSubs . length > 0 && this . currentFilters . subs . length === this . allSubs . length ) ) {
86102 // Show all
87103 $ ( '.ConfItem' ) . show ( ) ;
88104 } else {
105+ // Hide all conferences first
106+ $ ( '.ConfItem' ) . hide ( ) ;
107+
89108 // Show only selected subcategories
90109 this . currentFilters . subs . forEach ( sub => {
91110 $ ( '.' + sub + '-conf' ) . show ( ) ;
128147
129148 // Update filters from multiselect dropdown
130149 updateFromMultiselect : function ( selectedValues ) {
131- this . currentFilters . subs = selectedValues || [ ] ;
150+ // Skip if we're programmatically updating the multiselect
151+ if ( this . isUpdatingMultiselect ) {
152+ return ;
153+ }
154+
155+ // Ensure selectedValues is always an array
156+ if ( typeof selectedValues === 'string' ) {
157+ this . currentFilters . subs = [ selectedValues ] ;
158+ } else if ( Array . isArray ( selectedValues ) ) {
159+ this . currentFilters . subs = selectedValues ;
160+ } else {
161+ this . currentFilters . subs = [ ] ;
162+ }
163+
132164 this . saveState ( ) ;
133165 this . applyFilters ( ) ;
134166 } ,
135167
136168 // Filter by single subcategory (from badge click)
137169 filterBySub : function ( sub ) {
138- // Check if this is the only selected item - if so, select all (toggle behavior)
139- const $select = $ ( '#subject-select' ) ;
140- const currentlySelected = $select . val ( ) || [ ] ;
170+ // Check if this is currently the only selected filter
171+ const isOnlyFilter = this . currentFilters . subs . length === 1 &&
172+ this . currentFilters . subs [ 0 ] === sub ;
141173
142- if ( currentlySelected . length === 1 && currentlySelected [ 0 ] === sub ) {
143- // If clicking the same single selected item, select all
144- this . clearFilters ( ) ;
174+ if ( isOnlyFilter ) {
175+ // Toggle behavior: clear filters to show all
176+ this . currentFilters . subs = [ ] ;
145177 } else {
146- // Otherwise filter by this subcategory only
178+ // Otherwise filter to just this category
147179 this . currentFilters . subs = [ sub ] ;
180+ }
181+
182+ // Save state and apply filters
183+ this . saveState ( ) ;
184+ this . applyFilters ( ) ;
185+
186+ // Update multiselect UI if it exists
187+ const $select = $ ( '#subject-select' ) ;
188+ if ( $select . length && $select . data ( 'multiselect' ) ) {
189+ // Set flag to prevent feedback loop
190+ this . isUpdatingMultiselect = true ;
148191
149- // Update multiselect UI
150- $select . multiselect ( 'deselectAll' , false ) ;
151- $select . multiselect ( 'select' , sub ) ;
192+ if ( this . currentFilters . subs . length === 0 ) {
193+ // Select all
194+ $select . val ( this . allSubs ) ;
195+ } else {
196+ $select . val ( this . currentFilters . subs ) ;
197+ }
152198 $select . multiselect ( 'refresh' ) ;
153199
154- this . saveState ( ) ;
155- this . applyFilters ( ) ;
200+ // Reset flag after a short delay to ensure change event has fired
201+ setTimeout ( ( ) => {
202+ this . isUpdatingMultiselect = false ;
203+ } , 100 ) ;
156204 }
157205 } ,
158206
187235 bindEvents : function ( ) {
188236 const self = this ;
189237
238+ // Unbind any existing handlers first to prevent duplicates
239+ $ ( document ) . off ( 'change.conferenceFilter' ) ;
240+ $ ( document ) . off ( 'click.conferenceFilter' ) ;
241+ $ ( document ) . off ( 'mouseenter.conferenceFilter' ) ;
242+ $ ( document ) . off ( 'mouseleave.conferenceFilter' ) ;
243+
190244 // Handle multiselect changes
191- $ ( document ) . on ( 'change' , '#subject-select' , function ( ) {
245+ $ ( document ) . on ( 'change.conferenceFilter' , '#subject-select' , function ( ) {
246+ // Skip if we're programmatically updating
247+ if ( self . isUpdatingMultiselect ) {
248+ return ;
249+ }
192250 const selected = $ ( this ) . val ( ) || [ ] ;
193251 self . updateFromMultiselect ( selected ) ;
194252 } ) ;
195253
196- // Handle badge clicks
197- $ ( document ) . on ( 'click' , '.conf-sub' , function ( e ) {
198- e . preventDefault ( ) ;
199- e . stopPropagation ( ) ;
200- const sub = $ ( this ) . data ( 'sub' ) ;
201- if ( sub ) {
202- self . filterBySub ( sub ) ;
203- }
204- } ) ;
254+ // Handle badge clicks - use document delegation for dynamically loaded elements
255+ // First remove any legacy direct click handlers
256+ $ ( '.conf-sub' ) . off ( 'click' ) ;
257+
258+ // Then bind our delegated handler with specific selector
259+ $ ( document ) . off ( 'click.conferenceFilter' , '.conf-sub' )
260+ . on ( 'click.conferenceFilter' , '.conf-sub' , function ( e ) {
261+ e . preventDefault ( ) ;
262+ e . stopImmediatePropagation ( ) ; // Stop all other handlers
263+ const sub = $ ( this ) . data ( 'sub' ) ;
264+ if ( sub ) {
265+ // Call FilterManager directly to maintain context
266+ self . filterBySub ( sub ) ;
267+ }
268+ } ) ;
205269
206270 // Add hover effects to indicate clickability
207- $ ( document ) . on ( 'mouseenter' , '.conf-sub' , function ( ) {
271+ $ ( document ) . on ( 'mouseenter.conferenceFilter ' , '.conf-sub' , function ( ) {
208272 $ ( this ) . css ( 'opacity' , '0.8' ) ;
209273 } ) ;
210274
211- $ ( document ) . on ( 'mouseleave' , '.conf-sub' , function ( ) {
275+ $ ( document ) . on ( 'mouseleave.conferenceFilter ' , '.conf-sub' , function ( ) {
212276 $ ( this ) . css ( 'opacity' , '1' ) ;
213277 } ) ;
214278
221285
222286 // Apply initial filters on page load
223287 applyInitialFilters : function ( ) {
224- // Update multiselect to match current state
225- const $select = $ ( '#subject-select' ) ;
226- if ( $select . length ) {
227- $select . multiselect ( 'deselectAll' , false ) ;
228- this . currentFilters . subs . forEach ( sub => {
229- $select . multiselect ( 'select' , sub ) ;
230- } ) ;
231- $select . multiselect ( 'refresh' ) ;
288+ // If filters contain all categories, treat as "show all" (empty filters)
289+ if ( this . currentFilters . subs . length === this . allSubs . length ) {
290+ this . currentFilters . subs = [ ] ;
232291 }
233292
234- // Apply filters
293+ // Apply filters first
235294 this . applyFilters ( ) ;
295+
296+ // Update multiselect to match current state (without triggering change events)
297+ const $select = $ ( '#subject-select' ) ;
298+ if ( $select . length && $select . data ( 'multiselect' ) ) {
299+ // Set flag to prevent feedback loop during initialization
300+ this . isUpdatingMultiselect = true ;
301+
302+ // Update multiselect without triggering change events
303+ if ( this . currentFilters . subs . length === 0 ) {
304+ // Show all selected in multiselect
305+ $select . val ( this . allSubs ) ;
306+ } else {
307+ // Show only filtered categories selected
308+ $select . val ( this . currentFilters . subs ) ;
309+ }
310+ $select . multiselect ( 'refresh' ) ;
311+
312+ // Reset flag after update
313+ setTimeout ( ( ) => {
314+ this . isUpdatingMultiselect = false ;
315+ } , 100 ) ;
316+ }
236317 }
237318 } ;
238319
239320 // Public API
240321 window . ConferenceFilter = {
241- init : ( ) => FilterManager . init ( ) ,
242- filterBySub : ( sub ) => FilterManager . filterBySub ( sub ) ,
243- search : ( query ) => FilterManager . search ( query ) ,
244- clearFilters : ( ) => FilterManager . clearFilters ( ) ,
245- getCurrentFilters : ( ) => FilterManager . currentFilters ,
246- updateFromMultiselect : ( values ) => FilterManager . updateFromMultiselect ( values )
322+ init : function ( ) {
323+ return FilterManager . init ( ) ;
324+ } ,
325+ filterBySub : function ( sub ) {
326+ return FilterManager . filterBySub ( sub ) ;
327+ } ,
328+ search : function ( query ) {
329+ return FilterManager . search ( query ) ;
330+ } ,
331+ clearFilters : function ( ) {
332+ return FilterManager . clearFilters ( ) ;
333+ } ,
334+ getCurrentFilters : function ( ) {
335+ return FilterManager . currentFilters ;
336+ } ,
337+ updateFromMultiselect : function ( values ) {
338+ return FilterManager . updateFromMultiselect ( values ) ;
339+ }
247340 } ;
248341
342+ // Global alias for backward compatibility
343+ window . filterBySub = ( sub ) => FilterManager . filterBySub ( sub ) ;
344+
249345 // Auto-initialize when DOM is ready
250- if ( document . readyState === 'loading' ) {
251- document . addEventListener ( 'DOMContentLoaded' , ( ) => FilterManager . init ( ) ) ;
346+ // Wait for jQuery to be ready and for page setup to complete
347+ if ( typeof jQuery !== 'undefined' ) {
348+ // Use jQuery's ready handler which ensures DOM is ready
349+ jQuery ( document ) . ready ( function ( ) {
350+ // Add a small delay to ensure other scripts have finished setup
351+ setTimeout ( ( ) => FilterManager . init ( ) , 100 ) ;
352+ } ) ;
353+ } else if ( document . readyState === 'loading' ) {
354+ document . addEventListener ( 'DOMContentLoaded' , ( ) => {
355+ // Add delay for other scripts
356+ setTimeout ( ( ) => FilterManager . init ( ) , 100 ) ;
357+ } ) ;
252358 } else {
253- FilterManager . init ( ) ;
359+ // Already loaded, but still wait for other scripts
360+ setTimeout ( ( ) => FilterManager . init ( ) , 100 ) ;
254361 }
255362} ) ( ) ;
0 commit comments