PageSourceSearch

https://donors.vitalant.org/dwp/common/js/foundation.tabs-74f39812b21b9ddee695bc11d8020d25.js

js vitalant.org collected 2026-10-01 11:37:07 UTC 14,977 bytes, 551 lines download raw bytes

vendor: 14,977 bytes, lines 1-551
1import $ from 'jquery';
2import { Plugin } from './foundation.core.plugin';
3import { onLoad } from './foundation.core.utils';
4import { Keyboard } from './foundation.util.keyboard';
5import { onImagesLoaded } from './foundation.util.imageLoader';
6/**
7 * Tabs module.
8 * @module foundation.tabs
9 * @requires foundation.util.keyboard
10 * @requires foundation.util.imageLoader if tabs contain images
11 */
12
13class Tabs extends Plugin {
14  /**
15   * Creates a new instance of tabs.
16   * @class
17   * @name Tabs
18   * @fires Tabs#init
19   * @param {jQuery} element - jQuery object to make into tabs.
20   * @param {Object} options - Overrides to the default plugin settings.
21   */
22  _setup(element, options) {
23    this.$element = element;
24    this.options = $.extend({}, Tabs.defaults, this.$element.data(), options);
25    this.className = 'Tabs'; // ie9 back compat
26
27    this._init();
28    Keyboard.register('Tabs', {
29      'ENTER': 'open',
30      'SPACE': 'open',
31      'ARROW_RIGHT': 'next',
32      'ARROW_UP': 'previous',
33      'ARROW_DOWN': 'next',
34      'ARROW_LEFT': 'previous'
35      // 'TAB': 'next',
36      // 'SHIFT_TAB': 'previous'
37    });
38  }
39
40  /**
41   * Initializes the tabs by showing and focusing (if autoFocus=true) the preset active tab.
42   * @private
43   */
44  _init() {
45    var _this = this;
46    this._isInitializing = true;
47
48    this.$element.attr({'role': 'tablist'});
49    this.$tabTitles = this.$element.find(`.${this.options.linkClass}`);
50    this.$tabContent = $(`[data-tabs-content="${this.$element[0].id}"]`);
51
52    this.$tabTitles.each(function(){
53      var $elem = $(this),
54          $link = $elem.find('a'),
55          isActive = $elem.hasClass(`${_this.options.linkActiveClass}`),
56          hash = $link.attr('data-tabs-target') || $link[0].hash.slice(1),
57          linkId = $link[0].id ? $link[0].id : `${hash}-label`,
58          $tabContent = $(`#${hash}`);
59
60      $elem.attr({'role': 'presentation'});
61
62      $link.attr({
63        'role': 'tab',
64        'aria-controls': hash,
65        'aria-selected': isActive,
66        'id': linkId,
67        'tabindex': isActive ? '0' : '-1'
68      });
69
70      $tabContent.attr({
71        'role': 'tabpanel',
72        'aria-labelledby': linkId
73      });
74
75      // Save up the initial hash to return to it later when going back in history
76      if (isActive) {
77        _this._initialAnchor = `#${hash}`;
78      }
79
80      if(!isActive) {
81        $tabContent.attr('aria-hidden', 'true');
82      }
83
84      if(isActive && _this.options.autoFocus){
85        _this.onLoadListener = onLoad($(window), function() {
86          $('html, body').animate({ scrollTop: $elem.offset().top }, _this.options.deepLinkSmudgeDelay, () => {
87            $link.focus();
88          });
89        });
90      }
91    });
92
93    if(this.options.matchHeight) {
94      var $images = this.$tabContent.find('img');
95
96      if ($images.length) {
97        onImagesLoaded($images, this._setHeight.bind(this));
98      } else {
99        this._setHeight();
100      }
101    }
102
103     // Current context-bound function to open tabs on page load or history hashchange
104    this._checkDeepLink = () => {
105      var anchor = window.location.hash;
106
107      if (!anchor.length) {
108        // If we are still initializing and there is no anchor, then there is nothing to do
109        if (this._isInitializing) return;
110        // Otherwise, move to the initial anchor
111        if (this._initialAnchor) anchor = this._initialAnchor;
112      }
113
114      var anchorNoHash = anchor.indexOf('#') >= 0 ? anchor.slice(1) : anchor;
115      var $anchor = anchorNoHash && $(`#${anchorNoHash}`);
116      var $link = anchor && this.$element.find(`[href$="${anchor}"],[data-tabs-target="${anchorNoHash}"]`).first();
117      // Whether the anchor element that has been found is part of this element
118      var isOwnAnchor = !!($anchor.length && $link.length);
119
120      if (isOwnAnchor) {
121        // If there is an anchor for the hash, select it
122        if ($anchor && $anchor.length && $link && $link.length) {
123          this.selectTab($anchor, true);
124        }
125        // Otherwise, collapse everything
126        else {
127          this._collapse();
128        }
129
130        // Roll up a little to show the titles
131        if (this.options.deepLinkSmudge) {
132          var offset = this.$element.offset();
133          $('html, body').animate({ scrollTop: offset.top - this.options.deepLinkSmudgeOffset}, this.options.deepLinkSmudgeDelay);
134        }
135
136        /**
137         * Fires when the plugin has deeplinked at pageload
138         * @event Tabs#deeplink
139         */
140        this.$element.trigger('deeplink.zf.tabs', [$link, $anchor]);
141      }
142    }
143
144    //use browser to open a tab, if it exists in this tabset
145    if (this.options.deepLink) {
146      this._checkDeepLink();
147    }
148
149    this._events();
150
151    this._isInitializing = false;
152  }
153
154  /**
155   * Adds event handlers for items within the tabs.
156   * @private
157   */
158  _events() {
159    this._addKeyHandler();
160    this._addClickHandler();
161    this._setHeightMqHandler = null;
162
163    if (this.options.matchHeight) {
164      this._setHeightMqHandler = this._setHeight.bind(this);
165
166      $(window).on('changed.zf.mediaquery', this._setHeightMqHandler);
167    }
168
169    if(this.options.deepLink) {
170      $(window).on('hashchange', this._checkDeepLink);
171    }
172  }
173
174  /**
175   * Adds click handlers for items within the tabs.
176   * @private
177   */
178  _addClickHandler() {
179    var _this = this;
180
181    this.$element
182      .off('click.zf.tabs')
183      .on('click.zf.tabs', `.${this.options.linkClass}`, function(e){
184        e.preventDefault();
185        _this._handleTabChange($(this));
186      });
187  }
188
189  /**
190   * Adds keyboard event handlers for items within the tabs.
191   * @private
192   */
193  _addKeyHandler() {
194    var _this = this;
195
196    this.$tabTitles.off('keydown.zf.tabs').on('keydown.zf.tabs', function(e){
197      if (e.which === 9) return;
198
199
200      var $element = $(this),
201        $elements = $element.parent('ul').children('li'),
202        $prevElement,
203        $nextElement;
204
205      $elements.each(function(i) {
206        if ($(this).is($element)) {
207          if (_this.options.wrapOnKeys) {
208            $prevElement = i === 0 ? $elements.last() : $elements.eq(i-1);
209            $nextElement = i === $elements.length -1 ? $elements.first() : $elements.eq(i+1);
210          } else {
211            $prevElement = $elements.eq(Math.max(0, i-1));
212            $nextElement = $elements.eq(Math.min(i+1, $elements.length-1));
213          }
214          return;
215        }
216      });
217
218      // handle keyboard event with keyboard util
219      Keyboard.handleKey(e, 'Tabs', {
220        open: function() {
221          $element.find('[role="tab"]').focus();
222          _this._handleTabChange($element);
223        },
224        previous: function() {
225          $prevElement.find('[role="tab"]').focus();
226          _this._handleTabChange($prevElement);
227        },
228        next: function() {
229          $nextElement.find('[role="tab"]').focus();
230          _this._handleTabChange($nextElement);
231        },
232        handled: function() {
233          e.preventDefault();
234        }
235      });
236    });
237  }
238
239  /**
240   * Opens the tab `$targetContent` defined by `$target`. Collapses active tab.
241   * @param {jQuery} $target - Tab to open.
242   * @param {boolean} historyHandled - browser has already handled a history update
243   * @fires Tabs#change
244   * @function
245   */
246  _handleTabChange($target, historyHandled) {
247
248    // With `activeCollapse`, if the target is the active Tab, collapse it.
249    if ($target.hasClass(`${this.options.linkActiveClass}`)) {
250        if(this.options.activeCollapse) {
251            this._collapse();
252        }
253        return;
254    }
255
256    var $oldTab = this.$element.
257          find(`.${this.options.linkClass}.${this.options.linkActiveClass}`),
258          $tabLink = $target.find('[role="tab"]'),
259          target = $tabLink.attr('data-tabs-target'),
260          anchor = target && target.length ? `#${target}` : $tabLink[0].hash,
261          $targetContent = this.$tabContent.find(anchor);
262
263    //close old tab
264    this._collapseTab($oldTab);
265
266    //open new tab
267    this._openTab($target);
268
269    //either replace or update browser history
270    if (this.options.deepLink && !historyHandled) {
271      if (this.options.updateHistory) {
272        history.pushState({}, '', location.pathname + location.search + anchor);
273      } else {
274        history.replaceState({}, '', location.pathname + location.search + anchor);
275      }
276    }
277
278    /**
279     * Fires when the plugin has successfully changed tabs.
280     * @event Tabs#change
281     */
282    this.$element.trigger('change.zf.tabs', [$target, $targetContent]);
283
284    //fire to children a mutation event
285    $targetContent.find("[data-mutate]").trigger("mutateme.zf.trigger");
286  }
287
288  /**
289   * Opens the tab `$targetContent` defined by `$target`.
290   * @param {jQuery} $target - Tab to open.
291   * @function
292   */
293  _openTab($target) {
294      var $tabLink = $target.find('[role="tab"]'),
295          hash = $tabLink.attr('data-tabs-target') || $tabLink[0].hash.slice(1),
296          $targetContent = this.$tabContent.find(`#${hash}`);
297
298      $target.addClass(`${this.options.linkActiveClass}`);
299
300      $tabLink.attr({
301        'aria-selected': 'true',
302        'tabindex': '0'
303      });
304
305      $targetContent
306        .addClass(`${this.options.panelActiveClass}`).removeAttr('aria-hidden');
307  }
308
309  /**
310   * Collapses `$targetContent` defined by `$target`.
311   * @param {jQuery} $target - Tab to collapse.
312   * @function
313   */
314  _collapseTab($target) {
315    var $targetAnchor = $target
316      .removeClass(`${this.options.linkActiveClass}`)
317      .find('[role="tab"]')
318      .attr({
319        'aria-selected': 'false',
320        'tabindex': -1
321      });
322
323    $(`#${$targetAnchor.attr('aria-controls')}`)
324      .removeClass(`${this.options.panelActiveClass}`)
325      .attr({ 'aria-hidden': 'true' })
326  }
327
328  /**
329   * Collapses the active Tab.
330   * @fires Tabs#collapse
331   * @function
332   */
333  _collapse() {
334    var $activeTab = this.$element.find(`.${this.options.linkClass}.${this.options.linkActiveClass}`);
335
336    if ($activeTab.length) {
337      this._collapseTab($activeTab);
338
339      /**
340      * Fires when the plugin has successfully collapsed tabs.
341      * @event Tabs#collapse
342      */
343      this.$element.trigger('collapse.zf.tabs', [$activeTab]);
344    }
345  }
346
347  /**
348   * Public method for selecting a content pane to display.
349   * @param {jQuery | String} elem - jQuery object or string of the id of the pane to display.
350   * @param {boolean} historyHandled - browser has already handled a history update
351   * @function
352   */
353  selectTab(elem, historyHandled) {
354    var idStr, hashIdStr;
355
356    if (typeof elem === 'object') {
357      idStr = elem[0].id;
358    } else {
359      idStr = elem;
360    }
361
362    if (idStr.indexOf('#') < 0) {
363      hashIdStr = `#${idStr}`;
364    } else {
365      hashIdStr = idStr;
366      idStr = idStr.slice(1);
367    }
368
369    var $target = this.$tabTitles.has(`[href$="${hashIdStr}"],[data-tabs-target="${idStr}"]`).first();
370
371    this._handleTabChange($target, historyHandled);
372  };
373
374  /**
375   * Sets the height of each panel to the height of the tallest panel.
376   * If enabled in options, gets called on media query change.
377   * If loading content via external source, can be called directly or with _reflow.
378   * If enabled with `data-match-height="true"`, tabs sets to equal height
379   * @function
380   * @private
381   */
382  _setHeight() {
383    var max = 0,
384        _this = this; // Lock down the `this` value for the root tabs object
385
386    if (!this.$tabContent) {
387      return;
388    }
389
390    this.$tabContent
391      .find(`.${this.options.panelClass}`)
392      .css('min-height', '')
393      .each(function() {
394
395        var panel = $(this),
396            isActive = panel.hasClass(`${_this.options.panelActiveClass}`); // get the options from the parent instead of trying to get them from the child
397
398        if (!isActive) {
399          panel.css({'visibility': 'hidden', 'display': 'block'});
400        }
401
402        var temp = this.getBoundingClientRect().height;
403
404        if (!isActive) {
405          panel.css({
406            'visibility': '',
407            'display': ''
408          });
409        }
410
411        max = temp > max ? temp : max;
412      })
413      .css('min-height', `${max}px`);
414  }
415
416  /**
417   * Destroys an instance of tabs.
418   * @fires Tabs#destroyed
419   */
420  _destroy() {
421    this.$element
422      .find(`.${this.options.linkClass}`)
423      .off('.zf.tabs').hide().end()
424      .find(`.${this.options.panelClass}`)
425      .hide();
426
427    if (this.options.matchHeight) {
428      if (this._setHeightMqHandler != null) {
429         $(window).off('changed.zf.mediaquery', this._setHeightMqHandler);
430      }
431    }
432
433    if (this.options.deepLink) {
434      $(window).off('hashchange', this._checkDeepLink);
435    }
436
437    if (this.onLoadListener) {
438      $(window).off(this.onLoadListener);
439    }
440  }
441}
442
443Tabs.defaults = {
444  /**
445   * Link the location hash to the active pane.
446   * Set the location hash when the active pane changes, and open the corresponding pane when the location changes.
447   * @option
448   * @type {boolean}
449   * @default false
450   */
451  deepLink: false,
452
453  /**
454   * If `deepLink` is enabled, adjust the deep link scroll to make sure the top of the tab panel is visible
455   * @option
456   * @type {boolean}
457   * @default false
458   */
459  deepLinkSmudge: false,
460
461  /**
462   * If `deepLinkSmudge` is enabled, animation time (ms) for the deep link adjustment
463   * @option
464   * @type {number}
465   * @default 300
466   */
467  deepLinkSmudgeDelay: 300,
468
469  /**
470   * If `deepLinkSmudge` is enabled, animation offset from the top for the deep link adjustment
471   * @option
472   * @type {number}
473   * @default 0
474   */
475  deepLinkSmudgeOffset: 0,
476
477  /**
478   * If `deepLink` is enabled, update the browser history with the open tab
479   * @option
480   * @type {boolean}
481   * @default false
482   */
483  updateHistory: false,
484
485  /**
486   * Allows the window to scroll to content of active pane on load.
487   * Not recommended if more than one tab panel per page.
488   * @option
489   * @type {boolean}
490   * @default false
491   */
492  autoFocus: false,
493
494  /**
495   * Allows keyboard input to 'wrap' around the tab links.
496   * @option
497   * @type {boolean}
498   * @default true
499   */
500  wrapOnKeys: true,
501
502  /**
503   * Allows the tab content panes to match heights if set to true.
504   * @option
505   * @type {boolean}
506   * @default false
507   */
508  matchHeight: false,
509
510  /**
511   * Allows active tabs to collapse when clicked.
512   * @option
513   * @type {boolean}
514   * @default false
515   */
516  activeCollapse: false,
517
518  /**
519   * Class applied to `li`'s in tab link list.
520   * @option
521   * @type {string}
522   * @default 'tabs-title'
523   */
524  linkClass: 'tabs-title',
525
526  /**
527   * Class applied to the active `li` in tab link list.
528   * @option
529   * @type {string}
530   * @default 'is-active'
531   */
532  linkActiveClass: 'is-active',
533
534  /**
535   * Class applied to the content containers.
536   * @option
537   * @type {string}
538   * @default 'tabs-panel'
539   */
540  panelClass: 'tabs-panel',
541
542  /**
543   * Class applied to the active content container.
544   * @option
545   * @type {string}
546   * @default 'is-active'
547   */
548  panelActiveClass: 'is-active'
549};
550
551export {Tabs};

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.