PageSourceSearch

https://customer.nationalgrid.co.uk/public/javascripts/wpd.simple-accordions.js

js nationalgrid.co.uk collected 2026-09-24 09:02:20 UTC 10,364 bytes, 250 lines download raw bytes

1/**
2 * A vanilla-ish script controlling all wpd-simple-accordions, supplying accessible functionality, and the ability to let
3 *  accordions "unaccordionize" at particular breakpoints.
4 *
5 * This version SHOULD be the most up-to-date version. If your site has a dependency on the Utils module and still has a lingering
6 *  copy of its own accordions script, ditch it in favour of this one.
7 *
8 * @author noahm for Ability Net [WSME-556]
9 */
10(function($) {
11    'use strict';
12
13    const EVENT_TRIGGER_STATE_CHANGED_NAME = 'WPDSimpleAccordions__triggerStateChanged';
14
15    const DEFER_PAGE_SCROLL = 1;
16    const NO_DEFER_PAGE_SCROLL = 2;
17    const IGNORE_PAGE_SCROLL = 3;
18
19    /**
20     * As the name implies, will only attach a particular handler for an event once, no matter how many times
21     *  it's told to attach that handler to the specified event.
22     *
23     * @param {jQuery} $targetElem - the element in the DOM triggering the named event
24     * @param {string} eventName - the name of the event being triggered
25     * @param {function} handler - the handler function to provide as a callback
26     * @returns {undefined}
27     */
28    var attachHandlerOnce = function($targetElem, eventName, handler) {
29        var han = $._data($targetElem.get(0), "events");
30        if (!han || !han[eventName] || han[eventName].map(function(a) {return a.handler.name}).indexOf(handler.name) < 0) {
31            $targetElem.on(eventName, handler);
32        }
33    };
34
35    /**
36     *
37     * @param {jQuery} $button
38     * @returns {undefined}
39     */
40    var triggerPageScroll = function($button) {
41        if ($button.attr('data-accordion-noscroll') !== 'true') {
42            $([document.documentElement, document.body]).animate({
43                scrollTop: $button.offset().top - (window.fixedHeaderOffset || 0)
44            }, 250);
45        }
46    };
47
48    /**
49     * Opens/closes the specified `$button`'s owner. An event is emitted at the close of this function which may be listened
50     *  to by using the following:
51     *  window.addEventListener(window.WPDSimpleAccordions.EVENT_TRIGGER_STATE_CHANGED_NAME, handlerFn);
52     *
53     * @param {jQuery} $button - the trigger button element belonging to the accordion whose state will change
54     * @param {boolean} opened - the new state to be applied to the accordion
55     * @param {int} pageScrollFlag
56     * @returns {null|function} - if the page trigger is being deferred, then the triggerPageScroll function is returned, bound to
57     *                              the supplied `$button`, so that it may be called later on
58     */
59    var buttonToggle = function($button, opened, pageScrollFlag, updateDataExpandedFlag) {
60        $button.attr('aria-expanded', opened);
61        if (updateDataExpandedFlag) {
62            $button.attr('data-expanded', opened);
63        }
64        $button.parent().siblings('.accordion__item-content').attr('aria-hidden', !opened);
65
66        let ret = null;
67
68        if (opened) {
69            $button.parent().parent().addClass('expanded');
70            switch (pageScrollFlag) {
71                case DEFER_PAGE_SCROLL:
72                    ret = triggerPageScroll.bind(null, $button);
73
74                case NO_DEFER_PAGE_SCROLL:
75                    triggerPageScroll($button);
76                    break;
77            }
78        }
79        else {
80            $button.parent().parent().removeClass('expanded');
81        }
82
83        const event = new CustomEvent(EVENT_TRIGGER_STATE_CHANGED_NAME, {
84            detail: {opened: opened, triggerElement: $button}
85        });
86
87        window.dispatchEvent(event);
88
89        return ret;
90    };
91
92    /**
93    * Trigger all accordions to close and show the trigger buttons, also reinstating accordion CSS classes.
94    *   To be used in conjunction with unaccordionize.
95    *
96    * @param {jQuery} $elem - the owning root accordion group element
97    * @return {undefined}
98    */
99    var accordionize = function($elem) {
100        // reinstate the accordion CSS classes, by removing the 'x_' prefix that unaccordionize would have introduced
101        var persistState = $elem.attr('data-accordion-persiststate') === 'true';
102        $elem.find('[class*="accordion"]').each(function(idx, obj) {
103            const classList = obj.classList;
104
105            classList.forEach(function(cssClass) {
106                if (cssClass.startsWith('x_accordion')) {
107                    obj.classList.replace(cssClass, cssClass.substring(2));
108                }
109            });
110        });
111
112        let $buttons = $elem.find('.accordion__item-title');
113
114        $buttons.each(function(idx, button) {
115            var $button = $(button);
116            // close all the accordions, not triggering any page scroll
117            buttonToggle($button, persistState && $button.attr('data-expanded') == 'true', IGNORE_PAGE_SCROLL, false);
118            // show the buttons
119            $button.show();
120        });
121    };
122
123    /**
124    * Trigger all accordions to open and hide the trigger buttons, also disabling accordion CSS classes.
125    *   To be used in conjunction with accordionize.
126    *
127    * @param {jQuery} $elem - the owning root accordion group element
128    * @return {undefined}
129    */
130    var unaccordionize = function($elem) {
131        let $buttons = $elem.find('.accordion__item-title');
132
133        $buttons.each(function(idx, button) {
134            var $button = $(button);
135            // open all the accordions, not triggering any page scroll
136            buttonToggle($button, true, IGNORE_PAGE_SCROLL, false);
137            // hide the buttons
138            $button.hide();
139        });
140
141        // disable the accordion css classes. They will become 'x_accordion'
142        $elem.find('[class*="accordion"]').each(function(idx, obj) {
143            const classList = obj.classList;
144
145            classList.forEach(function(cssClass) {
146                if (cssClass.startsWith('accordion')) {
147                    obj.classList.replace(cssClass, 'x_' + cssClass);
148                }
149            });
150        });
151    };
152
153    /**
154    * Find all wpd-simple-accordions and set up their functionality.
155    * @returns {undefined}
156    */
157    var initAccordions = function() {
158        var $accordions = $('.wpd-simple-accordion');
159        var unaccordionizeMap = {};
160
161        $accordions.each(function(idx, elem) {
162            var $elem = $(elem);
163            var unaccordionizeAt = $elem.attr('data-unaccordionize-at');
164
165            if (unaccordionizeAt) {
166                // if the value supplied is a CSS variable, we need to resolve it to a concrete value, as it won't work with matchMedia
167                if (unaccordionizeAt.startsWith('--')) {
168                    unaccordionizeAt = getComputedStyle(document.body).getPropertyValue(unaccordionizeAt);
169                }
170
171                if (unaccordionizeAt) {
172                    if (!unaccordionizeMap[unaccordionizeAt]) {
173                        // construct a new object which keeps track of the accordions intended to unaccordionize at the specified
174                        //  breakpoint
175                        unaccordionizeMap[unaccordionizeAt] = {
176                            accordions: [],
177                            mq: window.matchMedia('(min-width: ' + unaccordionizeAt + ')'),
178                            // this is used to keep track of when there is a switch from a matches to a !matches
179                            lastMatchesResult: null
180                        };
181                    }
182
183                    unaccordionizeMap[unaccordionizeAt].accordions.push($elem);
184                }
185            }
186
187            var $buttons = $elem.find('.accordion__item-title');
188
189            var wpdAccButtonHandler = function() {
190               var $myself = $(this);
191
192               var isExopen = $elem.attr('data-accordion-exopen') === 'true';
193
194               var deferredScrollTo = buttonToggle($myself, !($myself.attr('data-expanded') === 'true'), isExopen ? DEFER_PAGE_SCROLL : NO_DEFER_PAGE_SCROLL, true);
195
196               if (isExopen && $myself.attr('data-expanded') === 'true') {
197                   // close all siblings if we're exclusive open
198                   $myself.parent().parent().siblings().find('.accordion__item-title').each(function(jdx, sibling) {
199                      buttonToggle($(sibling), false, NO_DEFER_PAGE_SCROLL, true);
200                   });
201
202                   // THEN trigger the scroll to $myself (otherwise calculation for scroll may be taking into account heights of open siblings before they're closed).
203                   if (typeof deferredScrollTo === 'function') {
204                       deferredScrollTo();
205                   }
206               }
207            };
208
209            $buttons.each(function(kdx, zelem) {
210                attachHandlerOnce($(zelem), 'click', wpdAccButtonHandler);
211            });
212        });
213
214        // if we have accordions that need to unaccoridonize at a certain breakpoint, we need to construct a resize handler
215        if (unaccordionizeMap) {
216            var wpdAccWindowResizeHandler = function() {
217                for (const key in unaccordionizeMap) {
218                    const val = unaccordionizeMap[key];
219
220                    // we only want to trigger an (un)accordionize when there's actually a change in the matches boolean
221                    if (val.lastMatchesResult === null || val.lastMatchesResult !== val.mq.matches) {
222                        val.lastMatchesResult = val.mq.matches;
223                        val.accordions.forEach(val.lastMatchesResult ? unaccordionize : accordionize);
224                    }
225                }
226            };
227
228            // do initial check for current page width
229            wpdAccWindowResizeHandler();
230            // begin listening to window width
231            attachHandlerOnce($(window), 'resize', wpdAccWindowResizeHandler);
232        }
233    };
234
235
236    // Expose an object on the window
237    // Allow init to be called again if necessary, as this script is meant to be included with #{page.script src:''}
238    window.WPDSimpleAccordions = {};
239    window.WPDSimpleAccordions.init = initAccordions;
240    window.WPDSimpleAccordions.EVENT_TRIGGER_STATE_CHANGED_NAME = EVENT_TRIGGER_STATE_CHANGED_NAME;
241
242    if (typeof Object.freeze === 'function') {
243        Object.freeze(window.WPDSimpleAccordions);
244    }
245
246    //do first setup
247    $(document).ready(function() {
248        window.WPDSimpleAccordions.init();
249    });
250})(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.