PageSourceSearch

https://www.kickitout.org/core/misc/vertical-tabs.js?v=11.4.5

js kickitout.org collected 2026-10-02 02:52:34 UTC 10,079 bytes, 323 lines download raw bytes

1/**
2 * @file
3 * Define vertical tabs functionality.
4 */
5
6/**
7 * Triggers when form values inside a vertical tab changes.
8 *
9 * This is used to update the summary in vertical tabs in order to know what
10 * are the important fields' values.
11 *
12 * @event summaryUpdated
13 */
14
15(function ($, Drupal, drupalSettings) {
16  /**
17   * Show the parent vertical tab pane of a targeted page fragment.
18   *
19   * In order to make sure a targeted element inside a vertical tab pane is
20   * visible on a hash change or fragment link click, show all parent panes.
21   *
22   * @param {jQuery.Event} e
23   *   The event triggered.
24   * @param {jQuery} $target
25   *   The targeted node as a jQuery object.
26   */
27  const handleFragmentLinkClickOrHashChange = (e, $target) => {
28    $target.parents('.vertical-tabs__pane').each((index, pane) => {
29      $(pane).data('verticalTab').focus();
30    });
31  };
32
33  /**
34   * This script transforms a set of details into a stack of vertical tabs.
35   *
36   * Each tab may have a summary which can be updated by another
37   * script. For that to work, each details element has an associated
38   * 'verticalTabCallback' (with jQuery.data() attached to the details),
39   * which is called every time the user performs an update to a form
40   * element inside the tab pane.
41   *
42   * @type {Drupal~behavior}
43   *
44   * @prop {Drupal~behaviorAttach} attach
45   *   Attaches behaviors for vertical tabs.
46   */
47  Drupal.behaviors.verticalTabs = {
48    attach(context) {
49      const width = drupalSettings.widthBreakpoint || 640;
50      const mq = `(max-width: ${width}px)`;
51
52      if (window.matchMedia(mq).matches) {
53        return;
54      }
55
56      /**
57       * Binds a listener to handle fragment link clicks and URL hash changes.
58       */
59      $(once('vertical-tabs-fragments', 'body')).on(
60        'formFragmentLinkClickOrHashChange.verticalTabs',
61        handleFragmentLinkClickOrHashChange,
62      );
63
64      once('vertical-tabs', '[data-vertical-tabs-panes]', context).forEach(
65        (verticalTab) => {
66          const $this = $(verticalTab).addClass('vertical-tabs__panes');
67          const focusID = $this.find(':hidden.vertical-tabs__active-tab')[0]
68            .value;
69          let tabFocus;
70
71          // Check if there are some details that can be converted to
72          // vertical-tabs.
73          const $details = $this.find('> details');
74          if ($details.length === 0) {
75            return;
76          }
77
78          // Create the tab column.
79          const tabList = $('<ul class="vertical-tabs__menu"></ul>');
80          $this
81            .wrap('<div class="vertical-tabs clearfix"></div>')
82            .before(tabList);
83
84          // Transform each details into a tab.
85          $details.each(function () {
86            const $that = $(this);
87            const $summary = $that.find('> summary');
88            // Summary elements often have 2 child nodes: a text title and a
89            // dynamic summary wrapped in <span>. To set the vertical tab title,
90            // we only want to copy the summary title, which is the first child
91            // node.
92            const title = $summary[0]?.firstChild?.textContent || '';
93            const verticalTab = new Drupal.verticalTab({
94              title,
95              details: $that,
96            });
97            tabList.append(verticalTab.item);
98            $that
99              .removeClass('collapsed')
100              .removeAttr('open')
101              .addClass('vertical-tabs__pane')
102              .data('verticalTab', verticalTab);
103            if (this.id === focusID) {
104              tabFocus = $that;
105            }
106          });
107
108          $(tabList).find('> li').eq(0).addClass('first');
109          $(tabList).find('> li').eq(-1).addClass('last');
110
111          if (!tabFocus) {
112            // If the current URL has a fragment and one of the tabs contains an
113            // element that matches the URL fragment, activate that tab.
114            const $locationHash = $this.find(window.location.hash);
115            if (window.location.hash && $locationHash.length) {
116              tabFocus = $locationHash.closest('.vertical-tabs__pane');
117            } else {
118              tabFocus = $this.find('> .vertical-tabs__pane').eq(0);
119            }
120          }
121          if (tabFocus.length) {
122            tabFocus.data('verticalTab').focus();
123          }
124        },
125      );
126
127      // If a validation error is within a vertical tab, open that tab.
128      context.querySelectorAll('details .form-item .error').forEach((item) => {
129        const details = item.closest('details');
130
131        if (details.style.display === 'none') {
132          const tabSelect = document.querySelector(
133            "[href='#".concat(details.id, "']"),
134          );
135
136          if (tabSelect) {
137            tabSelect.click();
138          }
139        }
140      });
141    },
142  };
143
144  /**
145   * The vertical tab object represents a single tab within a tab group.
146   *
147   * @constructor
148   *
149   * @param {object} settings
150   *   Settings object.
151   * @param {string} settings.title
152   *   The name of the tab.
153   * @param {jQuery} settings.details
154   *   The jQuery object of the details element that is the tab pane.
155   *
156   * @fires event:summaryUpdated
157   *
158   * @listens event:summaryUpdated
159   */
160  Drupal.verticalTab = function (settings) {
161    const self = this;
162    $.extend(this, settings, Drupal.theme('verticalTab', settings));
163
164    this.link.attr('href', `#${settings.details.attr('id')}`);
165
166    this.link.on('click', (e) => {
167      e.preventDefault();
168      self.focus();
169    });
170
171    // Keyboard events added:
172    // Pressing the Enter key will open the tab pane.
173    this.link.on('keydown', (event) => {
174      if (event.keyCode === 13) {
175        event.preventDefault();
176        self.focus();
177        // Set focus on the first input field of the visible details/tab pane.
178        $('.vertical-tabs__pane :input:visible:enabled').eq(0).trigger('focus');
179      }
180    });
181
182    this.details
183      .on('summaryUpdated', () => {
184        self.updateSummary();
185      })
186      .trigger('summaryUpdated');
187  };
188
189  Drupal.verticalTab.prototype = {
190    /**
191     * Displays the tab's content pane.
192     */
193    focus() {
194      this.details
195        .siblings('.vertical-tabs__pane')
196        .each(function () {
197          const tab = $(this).data('verticalTab');
198          tab.details.hide();
199          tab.details.removeAttr('open');
200          tab.item.removeClass('is-selected');
201        })
202        .end()
203        .show()
204        .siblings(':hidden.vertical-tabs__active-tab')
205        .get()
206        .forEach((hidden) => {
207          hidden.value = this.details.attr('id');
208        });
209      this.details.attr('open', true);
210      this.item.addClass('is-selected');
211      // Mark the active tab for screen readers.
212      $('#active-vertical-tab').remove();
213      this.link.append(
214        `<span id="active-vertical-tab" class="visually-hidden">${Drupal.t(
215          '(active tab)',
216        )}</span>`,
217      );
218    },
219
220    /**
221     * Updates the tab's summary.
222     */
223    updateSummary() {
224      this.summary.html(this.details.drupalGetSummary());
225    },
226
227    /**
228     * Shows a vertical tab pane.
229     *
230     * @return {Drupal.verticalTab}
231     *   The verticalTab instance.
232     */
233    tabShow() {
234      // Display the tab.
235      this.item.show();
236      // Show the vertical tabs.
237      this.item.closest('.js-form-type-vertical-tabs').show();
238      // Update .first marker for items. We need recurse from parent to retain
239      // the actual DOM element order as jQuery implements sortOrder, but not
240      // as public method.
241      this.item
242        .parent()
243        .children('.vertical-tabs__menu-item')
244        .removeClass('first')
245        .filter(':visible')
246        .eq(0)
247        .addClass('first');
248      // Display the details element.
249      this.details.removeClass('vertical-tab--hidden').show();
250      // Focus this tab.
251      this.focus();
252      return this;
253    },
254
255    /**
256     * Hides a vertical tab pane.
257     *
258     * @return {Drupal.verticalTab}
259     *   The verticalTab instance.
260     */
261    tabHide() {
262      // Hide this tab.
263      this.item.hide();
264      // Update .first marker for items. We need recurse from parent to retain
265      // the actual DOM element order as jQuery implements sortOrder, but not
266      // as public method.
267      this.item
268        .parent()
269        .children('.vertical-tabs__menu-item')
270        .removeClass('first')
271        .filter(':visible')
272        .eq(0)
273        .addClass('first');
274      // Hide the details element.
275      this.details.addClass('vertical-tab--hidden').hide().removeAttr('open');
276      // Focus the first visible tab (if there is one).
277      const $firstTab = this.details
278        .siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)')
279        .eq(0);
280      if ($firstTab.length) {
281        $firstTab.data('verticalTab').focus();
282      }
283      // Hide the vertical tabs (if no tabs remain).
284      else {
285        this.item.closest('.js-form-type-vertical-tabs').hide();
286      }
287      return this;
288    },
289  };
290
291  /**
292   * Theme function for a vertical tab.
293   *
294   * @param {object} settings
295   *   An object with the following keys:
296   * @param {string} settings.title
297   *   The name of the tab.
298   *
299   * @return {object}
300   *   This function has to return an object with at least these keys:
301   *   - item: The root tab jQuery element
302   *   - link: The anchor tag that acts as the clickable area of the tab
303   *       (jQuery version)
304   *   - summary: The jQuery element that contains the tab summary
305   */
306  Drupal.theme.verticalTab = function (settings) {
307    const tab = {};
308    tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>');
309    tab.title[0].textContent = settings.title;
310    tab.item = $(
311      '<li class="vertical-tabs__menu-item" tabindex="-1"></li>',
312    ).append(
313      (tab.link = $('<a href="#"></a>')
314        .append(tab.title)
315        .append(
316          (tab.summary = $(
317            '<span class="vertical-tabs__menu-item-summary"></span>',
318          )),
319        )),
320    );
321    return tab;
322  };
323})(jQuery, Drupal, drupalSettings);

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.