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