1/** 2 * Class representing an advanced tabbed interface with support for fully independent nested tabs. 3 */ 4class AdvancedTabs { 5 /** 6 * Create a tabbed interface. 7 * @param {HTMLElement} container - The container element for the tabs. 8 */ 9 constructor( container ) { 10 this.container = container; 11 this.tabs = container.querySelectorAll( 12 ':scope > .dsm-advanced-tabs-wrapper > .dsm-tab' 13 ); 14 this.tabsContent = container.querySelectorAll( 15 ':scope > .dsm-advanced-tabs-content-wrapper > .dsm-content-wrapper' 16 ); 17 this.trigger = container.dataset.trigger; 18 this.animationName = container.dataset.animation; 19 this.innerAnimationName = container.dataset.inner_animation; 20 this.deepLink = container.dataset.deep_link === 'on'; 21 this.autoScrollMobile = container.dataset.auto_scroll_mobile === 'on'; 22 this.anchorLink = container.dataset.anchor_link === 'on'; 23 24 this.init(); 25 } 26 27 /** 28 * Initialize the tabbed interface. 29 */ 30 init() { 31 this.setupEventListeners(); 32 this.handleInitialState(); 33 } 34 35 /** 36 * Set up event listeners for the tabs. 37 */ 38 setupEventListeners() {
39 this.tabs.forEach( ( tab ) => { 40 if ( this.trigger === 'click' ) { 41 tab.addEventListener( 'click', ( e ) => 42 this.handleEvent( e, tab ) 43 ); 44 } else if ( this.trigger === 'hover' ) { 45 tab.addEventListener( 'mouseover', ( e ) => 46 this.handleEvent( e, tab ) 47 ); 48 } 49 tab.addEventListener( 'keydown', ( event ) => { 50 if ( event.key === 'Enter' ) { 51 this.handleEvent( event, tab ); 52 } 53 } ); 54 } ); 55 } 56 57 /** 58 * Handle the initial state of the tabs. 59 */ 60 handleInitialState() { 61 let activeTab = this.tabs[ 0 ]; // Default to first tab 62 63 // Agar hash majood hai, uske mutabiq tab dhond lein 64 if ( this.deepLink && location.hash ) { 65 const hash = location.hash.substring( 1 ); 66 const matchingTab = Array.from( this.tabs ).find( ( tab ) => { 67 const tabTitle = tab.querySelector( '.dsm-title' ); 68 return ( 69 tabTitle && 70 tabTitle.textContent 71 .toLowerCase() 72 .replace( /\s+/g, '-' ) === hash 73 ); 74 } ); 75 if ( matchingTab ) { 76 activeTab = matchingTab; 77 } 78 } 79 80 // CHANGED: Pass "false" so that we do NOT pushState on page load 81 this.setActiveTab( activeTab, false ); 82 83 this.anchorInitScroll(); 84 85 // Agar hash hai (yaani user ne #kuchHash ke sath page open kiya), 86 // to scroll karein. Agar hash nahin, to skip. 87 if ( this.deepLink && location.hash && activeTab ) { 88 const tabId = activeTab.dataset.tabid.trim(); 89 const content = Array.from( this.tabsContent ).find( 90 ( content ) => content.dataset.contentid.trim() === tabId 91 ); 92 93 if ( content ) { 94 setTimeout( () => { 95 content.scrollIntoView( { 96 behavior: 'smooth', 97 block: 'start', 98 inline: 'center', 99 } ); 100 }, 300 ); // Adjust timeout as needed 101 } 102 } 103 } 104 105 /** 106 * Handle tab events (click or hover). 107 * @param {Event} event - The event object. 108 * @param {HTMLElement} tab - The tab element. 109 */ 110 handleEvent( event, tab ) { 111 event.stopPropagation(); 112 // CHANGED: On click, let it push the state (default = true) 113 this.setActiveTab( tab ); 114 this.autoScrollOnMobile( tab ); 115 } 116 117 /** 118 * Set the active tab. 119 * @param {HTMLElement} activeTab - The tab to set as active. 120 * @param {boolean} [shouldPushState=true] - Whether or not to push hash to URL. (CHANGED) 121 */ 122 setActiveTab( activeTab, shouldPushState = true ) { 123 const tabId = activeTab.dataset.tabid.trim(); 124 const content = Array.from( this.tabsContent ).find( 125 ( content ) => content.dataset.contentid.trim() === tabId 126 ); 127 128 if ( ! content ) { 129 console.error( 130 `No matching content found for tab with id: ${ tabId }` 131 ); 132 return; 133 } 134 135 // Remove active class from all tabs and content
136 this.tabs.forEach( ( tab ) => 137 tab.classList.remove( 'dsm-active', this.animationName ) 138 ); 139 this.tabsContent.forEach( ( content ) => { 140 content.classList.remove( 'dsm-active', this.animationName ); 141 const inner = content.querySelector( '.dsm-inner-content-wrapper' ); 142 if ( inner ) { 143 inner.classList.remove( 'animated', this.innerAnimationName ); 144 } 145 } ); 146 147 // Add active class to selected tab and content 148 activeTab.classList.add( 'dsm-active' ); 149 content.classList.add( 'dsm-active', this.animationName ); 150 const innerContent = content.querySelector( 151 '.dsm-inner-content-wrapper' 152 ); 153 if ( innerContent ) { 154 innerContent.classList.add( 'animated', this.innerAnimationName ); 155 } 156 157 // Handle deep linking, but ONLY if shouldPushState == true 158 if ( this.deepLink && shouldPushState ) { 159 const tabTitle = activeTab.querySelector( '.dsm-title' ); 160 if ( tabTitle ) { 161 const hash = tabTitle.textContent 162 .toLowerCase() 163 .replace( /\s+/g, '-' ); 164 history.pushState( null, null, `#${ hash }` ); 165 this.container.classList.add( 'dsm-active-deep-link' ); 166 } 167 } 168 } 169 170 /** 171 * Handle auto scroll on mobile. 172 * @param {HTMLElement} activeTab - The active tab element. 173 */ 174 autoScrollOnMobile( activeTab ) { 175 const tabId = activeTab.dataset.tabid.trim(); 176 const content = Array.from( this.tabsContent ).find( 177 ( content ) => content.dataset.contentid.trim() === tabId 178 ); 179 180 // Handle auto scroll on mobile 181 if ( this.autoScrollMobile && window.innerWidth <= 768 ) { 182 content.scrollIntoView( { 183 behavior: 'smooth', 184 block: 'start', 185 inline: 'nearest', 186 } ); 187 } 188 } 189 190 /** 191 * Scroll to the target element smoothly. 192 * @param {HTMLElement} target - The target element to scroll to. 193 */ 194 anchorSmoothScroll( target ) { 195 // Check if anchor linking is enabled 196 if ( ! this.anchorLink ) { 197 return; 198 } 199 200 target.scrollIntoView( { 201 behavior: 'smooth', 202 block: 'start', 203 } ); 204 } 205 206 /** 207 * Find the matching tab by hash. 208 * @param {string} hash - The hash to find the matching tab by. 209 * @returns {HTMLElement|null} The matching tab or null if no match is found. 210 */ 211 findMatchingTab( hash ) { 212 if ( ! hash ) return null; 213 214 // First try to find tab by matching title 215 const matchingTab = Array.from( this.tabs ).find( ( tab ) => { 216 const tabTitle = tab.querySelector( '.dsm-title' ); 217 return ( 218 tabTitle && 219 tabTitle.textContent.toLowerCase().replace( /\s+/g, '-' ) === 220 hash 221 ); 222 } ); 223 224 if ( matchingTab ) { 225 this.setActiveTab( matchingTab ); 226 return matchingTab; 227 } 228 229 // If no match by title, try matching by class 230 const matchingTabByClass = Array.from( this.tabs ).find( ( tab ) => 231 tab.classList.contains( hash ) 232 ); 233 234 if ( matchingTabByClass ) { 235 this.setActiveTab( matchingTabByClass ); 236 return matchingTabByClass; 237 } 238 239 return null; 240 } 241 242 /** 243 * Initialize anchor linking. 244 */ 245 anchorInitScroll() { 246 const links = document.querySelectorAll( 'a' ); 247 248 links.forEach( ( link ) => { 249 const href = link.getAttribute( 'href' ); 250 251 if ( href?.startsWith( '#' ) && href.length > 1 ) { 252 link.addEventListener( 'click', ( e ) => { 253 const targetId = href.slice( 1 ); 254 const matchingTab = this.findMatchingTab( targetId ); 255 256 if ( matchingTab ) { 257 e.preventDefault(); // Only prevent default if we found a matching tab 258 this.anchorSmoothScroll( matchingTab ); 259 } 260 } ); 261 } 262 } ); 263 } 264} 265 266// Initialize tabs when the DOM is fully loaded 267window.addEventListener( 'load', () => { 268 const initTabs = ( container ) => { 269 new AdvancedTabs( container ); 270 271 // Initialize nested tabs 272 const nestedContainers = container.querySelectorAll( 273 ':scope .dsm-content-wrapper .dsm-advanced-tabs-container' 274 ); 275 nestedContainers.forEach( ( nestedContainer ) => { 276 initTabs( nestedContainer ); 277 } ); 278 }; 279 280 const topLevelContainers = document.querySelectorAll( 281 '.dsm-advanced-tabs-container:not(.dsm-content-wrapper .dsm-advanced-tabs-container)' 282 ); 283 topLevelContainers.forEach( ( container ) => { 284 initTabs( container ); 285 } ); 286} );
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.