PageSourceSearch

https://www.freelo.io/js/collapse.js?v=tltff4

js freelo.io collected 2026-09-24 10:12:09 UTC 10,327 bytes, 342 lines download raw bytes

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.