61ea218709ef2539f0d507beec5c61ca18621793
[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 tabFocus;
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 tabList = $('<ul class="vertical-tabs__menu"></ul>');
75         $this.wrap('<div class="vertical-tabs clearfix"></div>').before(tabList);
76
77         // Transform each details into a tab.
78         $details.each(function () {
79           const $that = $(this);
80           const verticalTab = new Drupal.verticalTab({
81             title: $that.find('> summary').text(),
82             details: $that,
83           });
84           tabList.append(verticalTab.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', verticalTab);
92           if (this.id === focusID) {
93             tabFocus = $that;
94           }
95         });
96
97         $(tabList).find('> li').eq(0).addClass('first');
98         $(tabList).find('> li').eq(-1).addClass('last');
99
100         if (!tabFocus) {
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             tabFocus = $locationHash.closest('.vertical-tabs__pane');
106           }
107           else {
108             tabFocus = $this.find('> .vertical-tabs__pane').eq(0);
109           }
110         }
111         if (tabFocus.length) {
112           tabFocus.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
208         .parent()
209         .children('.vertical-tabs__menu-item')
210         .removeClass('first')
211         .filter(':visible')
212         .eq(0)
213         .addClass('first');
214       // Display the details element.
215       this.details.removeClass('vertical-tab--hidden').show();
216       // Focus this tab.
217       this.focus();
218       return this;
219     },
220
221     /**
222      * Hides a vertical tab pane.
223      *
224      * @return {Drupal.verticalTab}
225      *   The verticalTab instance.
226      */
227     tabHide() {
228       // Hide this tab.
229       this.item.hide();
230       // Update .first marker for items. We need recurse from parent to retain
231       // the actual DOM element order as jQuery implements sortOrder, but not
232       // as public method.
233       this.item
234         .parent()
235         .children('.vertical-tabs__menu-item')
236         .removeClass('first')
237         .filter(':visible')
238         .eq(0)
239         .addClass('first');
240       // Hide the details element.
241       this.details.addClass('vertical-tab--hidden').hide();
242       // Focus the first visible tab (if there is one).
243       const $firstTab = this.details.siblings('.vertical-tabs__pane:not(.vertical-tab--hidden)').eq(0);
244       if ($firstTab.length) {
245         $firstTab.data('verticalTab').focus();
246       }
247       // Hide the vertical tabs (if no tabs remain).
248       else {
249         this.item.closest('.js-form-type-vertical-tabs').hide();
250       }
251       return this;
252     },
253   };
254
255   /**
256    * Theme function for a vertical tab.
257    *
258    * @param {object} settings
259    *   An object with the following keys:
260    * @param {string} settings.title
261    *   The name of the tab.
262    *
263    * @return {object}
264    *   This function has to return an object with at least these keys:
265    *   - item: The root tab jQuery element
266    *   - link: The anchor tag that acts as the clickable area of the tab
267    *       (jQuery version)
268    *   - summary: The jQuery element that contains the tab summary
269    */
270   Drupal.theme.verticalTab = function (settings) {
271     const tab = {};
272     tab.item = $('<li class="vertical-tabs__menu-item" tabindex="-1"></li>')
273       .append(tab.link = $('<a href="#"></a>')
274         .append(tab.title = $('<strong class="vertical-tabs__menu-item-title"></strong>').text(settings.title))
275         .append(tab.summary = $('<span class="vertical-tabs__menu-item-summary"></span>'),
276         ),
277       );
278     return tab;
279   };
280 }(jQuery, Drupal, drupalSettings));