Security update for Core, with self-updated composer
[yaffs-website] / web / core / misc / vertical-tabs.es6.js
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       $('body').once('vertical-tabs-fragments').on('formFragmentLinkClickOrHashChange.verticalTabs', handleFragmentLinkClickOrHashChange);
60
61       $(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () {
62         const $this = $(this).addClass('vertical-tabs__panes');
63         const focusID = $this.find(':hidden.vertical-tabs__active-tab').val();
64         let tab_focus;
65
66         // Check if there are some details that can be converted to
67         // vertical-tabs.
68         const $details = $this.find('> details');
69         if ($details.length === 0) {
70           return;
71         }
72
73         // Create the tab column.
74         const tab_list = $('<ul class="vertical-tabs__menu"></ul>');
75         $this.wrap('<div class="vertical-tabs clearfix"></div>').before(tab_list);
76
77         // Transform each details into a tab.
78         $details.each(function () {
79           const $that = $(this);
80           const vertical_tab = new Drupal.verticalTab({
81             title: $that.find('> summary').text(),
82             details: $that,
83           });
84           tab_list.append(vertical_tab.item);
85           $that
86             .removeClass('collapsed')
87             // prop() can't be used on browsers not supporting details element,
88             // the style won't apply to them if prop() is used.
89             .attr('open', true)
90             .addClass('vertical-tabs__pane')
91             .data('verticalTab', vertical_tab);
92           if (this.id === focusID) {
93             tab_focus = $that;
94           }
95         });
96
97         $(tab_list).find('> li').eq(0).addClass('first');
98         $(tab_list).find('> li').eq(-1).addClass('last');
99
100         if (!tab_focus) {
101           // If the current URL has a fragment and one of the tabs contains an
102           // element that matches the URL fragment, activate that tab.
103           const $locationHash = $this.find(window.location.hash);
104           if (window.location.hash && $locationHash.length) {
105             tab_focus = $locationHash.closest('.vertical-tabs__pane');
106           }
107           else {
108             tab_focus = $this.find('> .vertical-tabs__pane').eq(0);
109           }
110         }
111         if (tab_focus.length) {
112           tab_focus.data('verticalTab').focus();
113         }
114       });
115     },
116   };
117
118   /**
119    * The vertical tab object represents a single tab within a tab group.
120    *
121    * @constructor
122    *
123    * @param {object} settings
124    *   Settings object.
125    * @param {string} settings.title
126    *   The name of the tab.
127    * @param {jQuery} settings.details
128    *   The jQuery object of the details element that is the tab pane.
129    *
130    * @fires event:summaryUpdated
131    *
132    * @listens event:summaryUpdated
133    */
134   Drupal.verticalTab = function (settings) {
135     const self = this;
136     $.extend(this, settings, Drupal.theme('verticalTab', settings));
137
138     this.link.attr('href', `#${settings.details.attr('id')}`);
139
140     this.link.on('click', (e) => {
141       e.preventDefault();
142       self.focus();
143     });
144
145     // Keyboard events added:
146     // Pressing the Enter key will open the tab pane.
147     this.link.on('keydown', (event) => {
148       if (event.keyCode === 13) {
149         event.preventDefault();
150         self.focus();
151         // Set focus on the first input field of the visible details/tab pane.
152         $('.vertical-tabs__pane :input:visible:enabled').eq(0).trigger('focus');
153       }
154     });
155
156     this.details
157       .on('summaryUpdated', () => {
158         self.updateSummary();
159       })
160       .trigger('summaryUpdated');
161   };
162
163   Drupal.verticalTab.prototype = {
164
165     /**
166      * Displays the tab's content pane.
167      */
168     focus() {
169       this.details
170         .siblings('.vertical-tabs__pane')
171         .each(function () {
172           const tab = $(this).data('verticalTab');
173           tab.details.hide();
174           tab.item.removeClass('is-selected');
175         })
176         .end()
177         .show()
178         .siblings(':hidden.vertical-tabs__active-tab')
179         .val(this.details.attr('id'));
180       this.item.addClass('is-selected');
181       // Mark the active tab for screen readers.
182       $('#active-vertical-tab').remove();
183       this.link.append(`<span id="active-vertical-tab" class="visually-hidden">${Drupal.t('(active tab)')}</span>`);
184     },
185
186     /**
187      * Updates the tab's summary.
188      */
189     updateSummary() {
190       this.summary.html(this.details.drupalGetSummary());
191     },
192
193     /**
194      * Shows a vertical tab pane.
195      *
196      * @return {Drupal.verticalTab}
197      *   The verticalTab instance.
198      */
199     tabShow() {
200       // Display the tab.
201       this.item.show();
202       // Show the vertical tabs.
203       this.item.closest('.js-form-type-vertical-tabs').show();
204       // Update .first marker for items. We need recurse from parent to retain
205       // the actual DOM element order as jQuery implements sortOrder, but not
206       // as public method.
207       this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
208         .filter(':visible').eq(0).addClass('first');
209       // Display the details element.
210       this.details.removeClass('vertical-tab--hidden').show();
211       // Focus this tab.
212       this.focus();
213       return this;
214     },
215
216     /**
217      * Hides a vertical tab pane.
218      *
219      * @return {Drupal.verticalTab}
220      *   The verticalTab instance.
221      */
222     tabHide() {
223       // Hide this tab.
224       this.item.hide();
225       // Update .first marker for items. We need recurse from parent to retain
226       // the actual DOM element order as jQuery implements sortOrder, but not
227       // as public method.
228       this.item.parent().children('.vertical-tabs__menu-item').removeClass('first')
229         .filter(':visible').eq(0).addClass('first');
230       // Hide the details element.
231       this.details.addClass('vertical-tab--hidden').hide();
232       // Focus the first visible tab (if there is one).
233       const $firstTab = this.details.siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)').eq(0);
234       if ($firstTab.length) {
235         $firstTab.data('verticalTab').focus();
236       }
237       // Hide the vertical tabs (if no tabs remain).
238       else {
239         this.item.closest('.js-form-type-vertical-tabs').hide();
240       }
241       return this;
242     },
243   };
244
245   /**
246    * Theme function for a vertical tab.
247    *
248    * @param {object} settings
249    *   An object with the following keys:
250    * @param {string} settings.title
251    *   The name of the tab.
252    *
253    * @return {object}
254    *   This function has to return an object with at least these keys:
255    *   - item: The root tab jQuery element
256    *   - link: The anchor tag that acts as the clickable area of the tab
257    *       (jQuery version)
258    *   - summary: The jQuery element that contains the tab summary
259    */
260   Drupal.theme.verticalTab = function (settings) {
261     const tab = {};
262     tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>')
263       .append(tab.link = $('<a href="#"></a>')
264         .append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').text(settings.title))
265         .append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>'),
266         ),
267       );
268     return tab;
269   };
270 }(jQuery, Drupal, drupalSettings));