1/** 2 * Collapse Component - Bootstrap Removal Phase 11 3 * 4 * Accessible collapse/accordion component. 5 * Replaces Bootstrap's collapse plugin. 6 * 7 * USAGE (Bootstrap compatibility): 8 * <a data-toggle="collapse" data-target="#content" aria-expanded="false">Toggle</a> 9 * <div class="collapse" id="content"> 10 * Content here... 11 * </div> 12 * 13 * TABLE ROW USAGE (with fade animation): 14 * <td data-toggle="collapse" href="#row-content">Toggle</td> 15 * <tr class="collapse fade" id="row-content">...</tr> 16 * 17 * NEW USAGE (Tailwind): 18 * <button data-collapse="#content" aria-expanded="false">Toggle</button> 19 * <div class="tw:collapse" id="content" hidden> 20 * Content here... 21 * </div> 22 */ 23 24(function(window) { 25 'use strict'; 26 27 var DURATION = 300; 28 var TABLE_ELEMENTS = ['TR', 'TBODY', 'TABLE', 'THEAD', 'TFOOT']; 29 30 /** 31 * Check if element is a table element that needs fade animation 32 */ 33 function isTableElement(el) { 34 return TABLE_ELEMENTS.indexOf(el.tagName) !== -1; 35 } 36 37 /** 38 * Collapse class 39 */ 40 function Collapse(trigger, options) { 41 this.trigger = trigger; 42 this.options = options || {}; 43 this.isAnimating = false; 44 45 /* Find target element */ 46 var targetSelector = trigger.dataset.target || 47 trigger.dataset.collapse || 48 trigger.getAttribute('href'); 49 50 if (!targetSelector) { 51 console.warn('Collapse: No target specified', trigger); 52 return; 53 } 54 55 this.target = document.querySelector(targetSelector); 56 57 if (!this.target) { 58 console.warn('Collapse: Target not found', targetSelector); 59 return; 60 } 61 62 /* Check if part of accordion */ 63 this.accordion = null; 64 var accordionSelector = trigger.dataset.parent; 65 if (accordionSelector) { 66 this.accordion = document.querySelector(accordionSelector); 67 } 68 69 /* Detect if target is a table element (uses fade instead of height) */ 70 this.isTable = isTableElement(this.target); 71 this.hasFade = this.target.classList.contains('fade'); 72 73 this.init(); 74 } 75 76 Collapse.prototype.init = function() { 77 var self = this; 78 79 /* Set initial ARIA state */ 80 var isExpanded; 81 82 if (this.target.classList.contains('collapse')) { 83 /* Bootstrap pattern: .collapse is hidden, .collapse.in/.show is visible */ 84 isExpanded = this.target.classList.contains('in') || 85 this.target.classList.contains('show'); 86 } else { 87 /* Tailwind pattern: hidden attribute controls visibility */ 88 isExpanded = !this.target.hasAttribute('hidden'); 89 } 90 91 this.trigger.setAttribute('aria-expanded', isExpanded ? 'true' : 'false'); 92 93 if (!isExpanded) { 94 if (this.isTable) { 95 /* Table elements: use display none via CSS class */ 96 this.target.classList.remove('in', 'show'); 97 this.target.classList.add('collapse'); 98 } else { 99 this.target.style.display = 'none'; 100 } 101 } 102 103 /* Click handler */ 104 this.trigger.addEventListener('click', function(e) { 105 e.preventDefault(); 106 self.toggle(); 107 }); 108 }; 109 110 Collapse.prototype.toggle = function() { 111 if (this.isAnimating) return; 112 113 var isExpanded = this.trigger.getAttribute('aria-expanded') === 'true'; 114 115 if (isExpanded) { 116 this.hide(); 117 } else { 118 this.show(); 119 } 120 }; 121 122 Collapse.prototype.show = function() { 123 var self = this; 124 125 if (this.isAnimating) return; 126 if (this.trigger.getAttribute('aria-expanded') === 'true') return; 127 128 this.isAnimating = true; 129 130 /* Close other items in accordion */ 131 if (this.accordion) { 132 var otherTriggers = this.accordion.querySelectorAll('[data-toggle="collapse"], [data-collapse]'); 133 otherTriggers.forEach(function(other) { 134 if (other !== self.trigger && other._collapse) { 135 other._collapse.hide(); 136 } 137 }); 138 } 139 140 /* Dispatch show event */ 141 var showEvent = new CustomEvent('show.bs.collapse', { bubbles: true, cancelable: true }); 142 this.target.dispatchEvent(showEvent); 143 144 if (showEvent.defaultPrevented) { 145 this.isAnimating = false;
146 return; 147 } 148 149 if (this.isTable || this.hasFade) { 150 /* Table elements / fade: use opacity animation */ 151 this.showWithFade(); 152 } else { 153 /* Regular elements: use height animation */ 154 this.showWithHeight(); 155 } 156 }; 157 158 Collapse.prototype.showWithFade = function() { 159 var self = this; 160 161 /* Show element and animate opacity */ 162 this.target.classList.add('in', 'show'); 163 this.target.classList.remove('collapse'); 164 165 if (this.hasFade) { 166 /* Animate opacity */ 167 var animation = this.target.animate( 168 [{ opacity: 0 }, { opacity: 1 }], 169 { duration: DURATION, easing: 'ease-out' } 170 ); 171 172 animation.onfinish = function() { 173 self.finishShow(); 174 }; 175 } else { 176 /* No animation, just show */ 177 this.finishShow(); 178 } 179 }; 180 181 Collapse.prototype.showWithHeight = function() { 182 var self = this; 183 184 /* Prepare for animation */ 185 this.target.style.display = ''; 186 this.target.style.overflow = 'hidden'; 187 this.target.classList.remove('collapse'); 188 this.target.classList.add('collapsing'); 189 190 var height = this.target.scrollHeight; 191 this.target.style.height = '0'; 192 193 /* Animate */ 194 var animation = this.target.animate( 195 [{ height: '0px' }, { height: height + 'px' }], 196 { duration: DURATION, easing: 'ease-out' } 197 ); 198 199 animation.onfinish = function() { 200 self.target.classList.remove('collapsing'); 201 self.target.style.height = ''; 202 self.target.style.overflow = ''; 203 self.finishShow(); 204 }; 205 }; 206 207 Collapse.prototype.finishShow = function() { 208 this.target.classList.add('collapse', 'in', 'show'); 209 this.target.removeAttribute('hidden'); 210 this.trigger.setAttribute('aria-expanded', 'true'); 211 this.isAnimating = false; 212 213 /* Dispatch shown event */ 214 var shownEvent = new CustomEvent('shown.bs.collapse', { bubbles: true }); 215 this.target.dispatchEvent(shownEvent); 216 }; 217 218 Collapse.prototype.hide = function() { 219 var self = this; 220 221 if (this.isAnimating) return; 222 if (this.trigger.getAttribute('aria-expanded') === 'false') return; 223 224 this.isAnimating = true; 225 226 /* Dispatch hide event */ 227 var hideEvent = new CustomEvent('hide.bs.collapse', { bubbles: true, cancelable: true }); 228 this.target.dispatchEvent(hideEvent); 229 230 if (hideEvent.defaultPrevented) { 231 this.isAnimating = false; 232 return; 233 } 234 235 if (this.isTable || this.hasFade) { 236 /* Table elements / fade: use opacity animation */ 237 this.hideWithFade(); 238 } else { 239 /* Regular elements: use height animation */ 240 this.hideWithHeight(); 241 } 242 }; 243 244 Collapse.prototype.hideWithFade = function() { 245 var self = this; 246 247 if (this.hasFade) { 248 /* Animate opacity */ 249 var animation = this.target.animate( 250 [{ opacity: 1 }, { opacity: 0 }], 251 { duration: DURATION, easing: 'ease-out' } 252 ); 253 254 animation.onfinish = function() { 255 self.finishHide(); 256 }; 257 } else { 258 /* No animation, just hide */ 259 this.finishHide(); 260 } 261 }; 262 263 Collapse.prototype.hideWithHeight = function() { 264 var self = this; 265 266 /* Prepare for animation */ 267 this.target.style.overflow = 'hidden'; 268 var height = this.target.scrollHeight; 269 this.target.style.height = height + 'px'; 270 this.target.classList.remove('collapse', 'in', 'show'); 271 this.target.classList.add('collapsing'); 272 273 /* Force reflow */ 274 this.target.offsetHeight; 275 276 /* Animate */ 277 var animation = this.target.animate( 278 [{ height: height + 'px' }, { height: '0px' }], 279 { duration: DURATION, easing: 'ease-out' } 280 ); 281 282 animation.onfinish = function() { 283 self.target.classList.remove('collapsing'); 284 self.target.style.height = ''; 285 self.target.style.overflow = ''; 286 self.finishHide(); 287 }; 288 }; 289 290 Collapse.prototype.finishHide = function() { 291 this.target.classList.remove('in', 'show'); 292 this.target.classList.add('collapse'); 293 294 if (!this.isTable) { 295 this.target.style.display = 'none'; 296 } 297 298 this.trigger.setAttribute('aria-expanded', 'false'); 299 this.isAnimating = false;
300 301 /* Dispatch hidden event */ 302 var hiddenEvent = new CustomEvent('hidden.bs.collapse', { bubbles: true }); 303 this.target.dispatchEvent(hiddenEvent); 304 }; 305 306 /** 307 * Initialize all collapse elements 308 */ 309 Collapse.initAll = function() { 310 /* Find all collapse triggers */ 311 var triggers = document.querySelectorAll('[data-toggle="collapse"], [data-collapse]'); 312 313 triggers.forEach(function(trigger) { 314 if (!trigger._collapse) { 315 trigger._collapse = new Collapse(trigger); 316 } 317 }); 318 }; 319 320 /** 321 * Get collapse instance from trigger element 322 */ 323 Collapse.getInstance = function(trigger) { 324 return trigger._collapse || null; 325 }; 326 327 /* Initialize on DOMContentLoaded */ 328 if (document.readyState === 'loading') { 329 document.addEventListener('DOMContentLoaded', Collapse.initAll); 330 } else { 331 Collapse.initAll(); 332 } 333 334 /* Export */ 335 window.Collapse = Collapse; 336 337 /* Module export */ 338 if (typeof module !== 'undefined' && module.exports) { 339 module.exports = Collapse; 340 } 341 342})(window);
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.