PageSourceSearch

https://www.brivo.com/wp-content/plugins/brivo-feature-comparison-table/assets/js/script.js?v=1783996343

js brivo.com collected 2026-10-01 12:30:55 UTC 18,469 bytes, 408 lines download raw bytes

1/**
2 * Feature Comparison Table — Accordion Script
3 *
4 * RESPONSIBILITY
5 * ──────────────
6 * This script handles ONLY the accordion open/close behaviour.
7 * No data fetching, no rendering — that's all done server-side in PHP.
8 *
9 * WHAT IT DOES:
10 *   1. On load, sets the initial height of closed section bodies to 0
11 *      so they're hidden (without relying on display:none, which prevents
12 *      smooth animation).
13 *
14 *   2. When a section header is clicked:
15 *      - Measures the section body's natural (scrollHeight) height.
16 *      - Animates from 0px → measuredHeight (opening) or reverse (closing).
17 *      - Updates aria-expanded for screen reader accessibility.
18 *      - Rotates the chevron via CSS class toggle.
19 *
20 *   3. Prevents section link clicks (↗) from toggling the accordion —
21 *      those should navigate to the linked page.
22 *
23 *   4. Supports keyboard navigation: Enter and Space trigger toggle.
24 *
25 *   5. Re-initialises when Elementor's frontend rerenders (editor preview).
26 *
27 * WHY NOT CSS MAX-HEIGHT ANIMATION?
28 * ──────────────────────────────────
29 * The common CSS trick is: transition: max-height 0.3s. The problem is
30 * you must set max-height to a large arbitrary value (e.g. 2000px) to
31 * accommodate any content height. This makes the easing feel wrong because
32 * CSS animates from 0 → 2000px even if the content is only 200px tall,
33 * making the animation feel fast at first and then seemingly pause.
34 *
35 * Our JS approach measures the EXACT height using scrollHeight, then
36 * animates height from 0 → exactPx. The CSS transition in style.css
37 * handles the actual animation (transition: height 0.28s ease).
38 *
39 * ANIMATION CLEANUP:
40 * After the opening animation completes (transitionend), we set height
41 * back to 'auto'. This allows the section to grow if the viewport resizes
42 * or if dynamic content is added later.
43 *
44 * @package BFCT
45 */
46
47( function( $ ) {
48    'use strict';
49
50    // ── Constants ─────────────────────────────────────────────────────────────
51    var SELECTOR_WRAPPER = '.bfct-wrapper';
52    var SELECTOR_SECTION = '.bfct-section';
53    var SELECTOR_HEADER  = '.bfct-section-header';
54    var SELECTOR_BODY    = '.bfct-section-body';
55    var SELECTOR_LINK    = '.bfct-section-link';
56    var CLASS_OPEN       = 'bfct-section--open';
57
58    // ── Mobile Edition Switcher constants ────────────────────────────────────
59    var SELECTOR_SWITCHER        = '.bfct-mobile-switcher';
60    var SELECTOR_SWITCHER_OPTION = '.bfct-mobile-switcher__option';
61    var SELECTOR_VALUE_CELL      = '.bfct-cell--icon, .bfct-cell--text';
62    var CLASS_SWITCHER_ACTIVE    = 'bfct-mobile-switcher__option--active';
63    var CLASS_CELL_MOBILE_HIDDEN = 'bfct-cell--mobile-hidden';
64
65    /**
66     * initTable( $wrapper )
67     *
68     * Initialises accordion behaviour for a single .bfct-wrapper instance.
69     * Called for each widget on the page, and again when Elementor re-renders.
70     *
71     * @param {jQuery} $wrapper  The .bfct-wrapper element.
72     */
73    function initTable( $wrapper ) {
74
75        // ── Step 1: Set initial heights ────────────────────────────────────────
76        /**
77         * For sections that start CLOSED:
78         *   Set height to '0px' explicitly so the CSS transition has a
79         *   defined start point. Without this, the first open animation
80         *   won't animate (there's no 'from' value to transition from).
81         *
82         * For sections that start OPEN (have .bfct-section--open):
83         *   Set height to 'auto' so the content is fully visible and
84         *   the section can resize naturally.
85         */
86        $wrapper.find( SELECTOR_SECTION ).each( function() {
87            var $section = $( this );
88            var $body    = $section.find( SELECTOR_BODY );
89
90            if ( $section.hasClass( CLASS_OPEN ) ) {
91                // Open: let it be its natural height.
92                $body.css( 'height', 'auto' );
93            } else {
94                // Closed: collapse to zero, no animation on init.
95                // We temporarily disable transition so this happens instantly.
96                $body.css({
97                    'transition': 'none',
98                    'height':     '0px',
99                    'overflow':   'hidden'
100                });
101
102                // Re-enable transition on the next frame.
103                // requestAnimationFrame ensures the 'none' transition has
104                // been applied before we restore it.
105                requestAnimationFrame( function() {
106                    $body.css( 'transition', '' ); // Restores the CSS-defined transition
107                });
108            }
109        });
110
111        // ── Step 2: Section header click handler ───────────────────────────────
112        /**
113         * We use event delegation on the wrapper rather than binding directly
114         * to each header. This is more efficient and works correctly when
115         * Elementor re-renders the widget in the editor.
116         *
117         * .off() removes any previously bound handler before adding a new one,
118         * preventing duplicate handlers on re-init.
119         */
120        $wrapper
121            .off( 'click.bfct', SELECTOR_HEADER )
122            .on(  'click.bfct', SELECTOR_HEADER, function( event ) {
123
124                // ── Guard: ignore clicks on the section link (↗) ──────────────
125                // The .bfct-section-link is a real <a> tag inside the header.
126                // We want clicks on IT to navigate, not toggle the accordion.
127                // event.target is the actual element clicked (may be a child).
128                // .closest() checks if the click was on or inside the link.
129                if ( $( event.target ).closest( SELECTOR_LINK ).length ) {
130                    return; // Let the default link behaviour proceed.
131                }
132
133                var $header  = $( this );
134                var $section = $header.closest( SELECTOR_SECTION );
135                var $body    = $section.find( SELECTOR_BODY );
136                var isOpen   = $section.hasClass( CLASS_OPEN );
137
138                if ( isOpen ) {
139                    closeSection( $section, $header, $body );
140                } else {
141                    openSection( $section, $header, $body );
142                }
143            });
144
145        // ── Step 3: Keyboard accessibility ─────────────────────────────────────
146        /**
147         * The section header has tabindex="0" in the PHP template, making it
148         * focusable. We listen for Enter and Space to trigger the same toggle
149         * behaviour as a mouse click.
150         *
151         * This makes the accordion fully keyboard-navigable without needing
152         * a real <button> element (which would complicate the header layout).
153         */
154        $wrapper
155            .off( 'keydown.bfct', SELECTOR_HEADER )
156            .on(  'keydown.bfct', SELECTOR_HEADER, function( event ) {
157                // key codes: Enter = 13, Space = 32
158                if ( event.key === 'Enter' || event.key === ' ' || event.keyCode === 13 || event.keyCode === 32 ) {
159                    event.preventDefault(); // Prevent page scroll on Space.
160                    $( this ).trigger( 'click' );
161                }
162            });
163
164        // ── Step 4: Prevent link click from bubbling to header ─────────────────
165        /**
166         * Even though we handle this in the click guard above, explicitly
167         * stopping propagation on the link provides a cleaner separation.
168         * Without this, clicks on the link could reach other delegated handlers.
169         */
170        $wrapper
171            .off( 'click.bfct-link', SELECTOR_LINK )
172            .on(  'click.bfct-link', SELECTOR_LINK, function( event ) {
173                event.stopPropagation();
174                // Default behaviour (navigation) continues.
175            });
176
177        // ── Step 5: Mobile edition switcher ─────────────────────────────────────
178        // Separate function — see initEditionSwitcher() below for the full
179        // explanation of how it shows/hides value cells per edition.
180        initEditionSwitcher( $wrapper );
181    }
182
183    /**
184     * initEditionSwitcher( $wrapper )
185     *
186     * Wires up the mobile pill switcher (.bfct-mobile-switcher) that lets
187     * someone pick ONE edition to view at a time on small screens. Does
188     * nothing if the wrapper has no switcher in it (e.g. CSS was modified
189     * to remove it) — safe to call unconditionally from initTable().
190     *
191     * HOW IT WORKS
192     * ────────────
193     * Every value cell in the table (.bfct-cell--icon / .bfct-cell--text)
194     * carries a data-edition-index attribute set in templates/render.php,
195     * matching its position among the edition columns (0, 1, 2…). Each pill
196     * in the switcher carries the same attribute. Clicking a pill:
197     *   1. Marks that pill active (visually + aria-checked).
198     *   2. Adds .bfct-cell--mobile-hidden to every value cell whose index
199     *      does NOT match the clicked pill, and removes it from the one
200     *      that does. CSS (only inside the @media max-width:768px block)
201     *      hides anything carrying that class — desktop is unaffected
202     *      since the class has no effect at wider viewports.
203     *
204     * This runs purely on click — there's no need to recompute anything
205     * on resize, since the CSS itself only acts on the class at mobile
206     * widths regardless of when it was added.
207     *
208     * @param {jQuery} $wrapper  The .bfct-wrapper element.
209     */
210    function initEditionSwitcher( $wrapper ) {
211        var $switcher = $wrapper.find( SELECTOR_SWITCHER );
212
213        if ( ! $switcher.length ) {
214            return; // No switcher in this instance — nothing to wire up.
215        }
216
217        var $options = $switcher.find( SELECTOR_SWITCHER_OPTION );
218        var $cells   = $wrapper.find( SELECTOR_VALUE_CELL );
219
220        /**
221         * setActiveEdition( index )
222         *
223         * Updates both the switcher's visual/ARIA state and every value
224         * cell's visibility to reflect the newly selected edition index.
225         */
226        function setActiveEdition( index ) {
227
228            // Update the pills: exactly one becomes active.
229            $options.each( function() {
230                var $opt     = $( this );
231                var optIndex = parseInt( $opt.attr( 'data-edition-index' ), 10 );
232                var isActive = ( optIndex === index );
233
234                $opt.toggleClass( CLASS_SWITCHER_ACTIVE, isActive );
235                $opt.attr( 'aria-checked', isActive ? 'true' : 'false' );
236            });
237
238            // Update every value cell across the whole table: show the one
239            // matching this edition, hide all others.
240            $cells.each( function() {
241                var $cell      = $( this );
242                var cellIndex  = parseInt( $cell.attr( 'data-edition-index' ), 10 );
243                var shouldHide = ( cellIndex !== index );
244
245                $cell.toggleClass( CLASS_CELL_MOBILE_HIDDEN, shouldHide );
246            });
247        }
248
249        // ── Click handler ────────────────────────────────────────────────────
250        // Event delegation on $switcher (not each $option individually) so
251        // this keeps working correctly if Elementor re-renders the widget.
252        $switcher
253            .off( 'click.bfct', SELECTOR_SWITCHER_OPTION )
254            .on(  'click.bfct', SELECTOR_SWITCHER_OPTION, function() {
255                var index = parseInt( $( this ).attr( 'data-edition-index' ), 10 );
256                setActiveEdition( index );
257            });
258
259        // ── Default state ────────────────────────────────────────────────────
260        // Always start on the first edition (index 0) on load. The PHP
261        // template already marks the first pill active and shows only its
262        // cells server-side, so this call mainly keeps JS state in sync —
263        // but running it explicitly also self-heals if Elementor's editor
264        // re-renders the widget mid-session and the DOM gets reset.
265        setActiveEdition( 0 );
266    }
267
268    /**
269     * openSection( $section, $header, $body )
270     *
271     * Animates a section open:
272     *   1. Remove overflow:hidden temporarily (set in closed state).
273     *   2. Measure the natural height via scrollHeight.
274     *   3. Animate height from 0 → measured value.
275     *   4. On animation complete, set height:'auto' so it reflows naturally.
276     *   5. Update classes and ARIA attribute.
277     */
278    function openSection( $section, $header, $body ) {
279        var bodyEl = $body[0]; // Native DOM element (scrollHeight is a DOM property)
280
281        // Update state immediately (so CSS transition target is correct).
282        $section.addClass( CLASS_OPEN );
283        $header.attr( 'aria-expanded', 'true' );
284
285        // Ensure overflow is hidden during animation so content doesn't flash.
286        $body.css( 'overflow', 'hidden' );
287
288        // Measure the full natural height.
289        // scrollHeight returns the height the element WOULD be if not constrained.
290        var targetHeight = bodyEl.scrollHeight + 'px';
291
292        // Set target height — the CSS transition animates this change.
293        $body.css( 'height', targetHeight );
294
295        // After the animation completes, switch to 'auto' height.
296        // This allows the section to expand/contract if content changes later
297        // (e.g. images load, viewport resizes).
298        bodyEl.addEventListener( 'transitionend', function onTransitionEnd( e ) {
299            // Only react to the 'height' property transition ending.
300            if ( e.propertyName !== 'height' ) return;
301
302            $body.css({
303                'height':   'auto',
304                'overflow': '' // Remove inline overflow, let CSS take over.
305            });
306
307            // Remove listener to avoid stacking up multiple handlers.
308            bodyEl.removeEventListener( 'transitionend', onTransitionEnd );
309        });
310    }
311
312    /**
313     * closeSection( $section, $header, $body )
314     *
315     * Animates a section closed:
316     *   1. Measure current rendered height (it may be 'auto').
317     *   2. Set that explicit pixel value so CSS knows where to animate FROM.
318     *   3. On the next frame, set height to 0 to trigger the animation.
319     *   4. Update classes and ARIA attribute.
320     */
321    function closeSection( $section, $header, $body ) {
322        var bodyEl = $body[0];
323
324        // Capture the current rendered height as a pixel value.
325        // We need this because we can't animate FROM 'auto'.
326        var currentHeight = bodyEl.getBoundingClientRect().height + 'px';
327
328        // Set height to explicit pixels — required as animation start point.
329        $body.css({
330            'height':   currentHeight,
331            'overflow': 'hidden'
332        });
333
334        // Update state (class + aria) before animation.
335        $section.removeClass( CLASS_OPEN );
336        $header.attr( 'aria-expanded', 'false' );
337
338        // Tiny delay (one animation frame) ensures the browser has painted
339        // the explicit height before we transition to 0.
340        // Without this delay, some browsers skip the animation entirely.
341        requestAnimationFrame( function() {
342            requestAnimationFrame( function() {
343                // Now animate to zero — CSS transition handles the easing.
344                $body.css( 'height', '0px' );
345            });
346        });
347    }
348
349    /**
350     * init()
351     *
352     * Finds all .bfct-wrapper elements on the page and initialises each one.
353     * Called both on DOM ready and when Elementor rerenders in the editor.
354     */
355    function init() {
356        $( SELECTOR_WRAPPER ).each( function() {
357            initTable( $( this ) );
358        });
359    }
360
361    // ── Entry Points ──────────────────────────────────────────────────────────
362
363    /**
364     * Standard DOM ready — initialises on all normal (non-Elementor) page loads.
365     */
366    $( document ).ready( function() {
367        init();
368    });
369
370    /**
371     * Elementor frontend init hook.
372     *
373     * Elementor fires 'elementor/frontend/init' after its own JS has
374     * initialised on the frontend. For the editor specifically, it also
375     * fires events when widgets are added or re-rendered. We hook into both
376     * to ensure the accordion works in:
377     *   - Normal page load (frontend)
378     *   - Elementor editor preview
379     *   - After Elementor re-renders the widget (settings change in editor)
380     */
381    $( window ).on( 'elementor/frontend/init', function() {
382
383        if ( typeof elementorFrontend !== 'undefined' && elementorFrontend.hooks ) {
384
385            /**
386             * 'frontend/element_ready/bfct-feature-table.default'
387             *
388             * Fires whenever an instance of our specific widget is ready
389             * in the DOM — both on the frontend and in the editor preview.
390             * The widget's get_name() value ('bfct-feature-table') forms
391             * part of the hook name.
392             *
393             * $scope is the jQuery object wrapping the .elementor-widget element.
394             * We find our .bfct-wrapper inside it.
395             */
396            elementorFrontend.hooks.addAction(
397                'frontend/element_ready/bfct-feature-table.default',
398                function( $scope ) {
399                    var $wrapper = $scope.find( SELECTOR_WRAPPER );
400                    if ( $wrapper.length ) {
401                        initTable( $wrapper );
402                    }
403                }
404            );
405        }
406    });
407
408} )( jQuery );

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.