3 * Overrides core/misc/vertical-tabs.js.
6 (function ($, window, Drupal, drupalSettings) {
10 * This script transforms a set of details into a stack of vertical
11 * tabs. Another tab pane can be selected by clicking on the respective
14 * Each tab may have a summary which can be updated by another
15 * script. For that to work, each details element has an associated
16 * 'verticalTabCallback' (with jQuery.data() attached to the details),
17 * which is called every time the user performs an update to a form
18 * element inside the tab pane.
20 Drupal.behaviors.verticalTabs = {
21 attach: function (context) {
22 var width = drupalSettings.widthBreakpoint || 640;
23 var mq = '(max-width: ' + width + 'px)';
25 if (window.matchMedia(mq).matches) {
29 $(context).find('[data-vertical-tabs-panes]').once('vertical-tabs').each(function () {
30 var $this = $(this).addClass('tab-content vertical-tabs-panes');
32 var focusID = $(':hidden.vertical-tabs__active-tab', this).val();
33 if (typeof focusID === 'undefined' || !focusID.length) {
38 // Check if there are some details that can be converted to vertical-tabs
39 var $details = $this.find('> .panel');
40 if ($details.length === 0) {
44 // Create the tab column.
45 var tab_list = $('<ul class="nav nav-tabs vertical-tabs-list"></ul>');
46 $this.wrap('<div class="tabbable tabs-left vertical-tabs clearfix"></div>').before(tab_list);
48 // Transform each details into a tab.
49 $details.each(function () {
51 var vertical_tab = new Drupal.verticalTab({
52 title: $that.find('> .panel-heading > .panel-title, > .panel-heading').last().html(),
55 tab_list.append(vertical_tab.item);
57 .removeClass('collapsed')
58 // prop() can't be used on browsers not supporting details element,
59 // the style won't apply to them if prop() is used.
61 .removeClass('collapsible collapsed panel panel-default')
62 .addClass('tab-pane vertical-tabs-pane')
63 .data('verticalTab', vertical_tab)
64 .find('> .panel-heading').remove();
65 if (this.id === focusID) {
70 $(tab_list).find('> li:first').addClass('first');
71 $(tab_list).find('> li:last').addClass('last');
74 // If the current URL has a fragment and one of the tabs contains an
75 // element that matches the URL fragment, activate that tab.
76 var $locationHash = $this.find(window.location.hash);
77 if (window.location.hash && $locationHash.length) {
78 tab_focus = $locationHash.closest('.vertical-tabs-pane');
81 tab_focus = $this.find('> .vertical-tabs-pane:first');
84 if (tab_focus.length) {
85 tab_focus.data('verticalTab').focus();
89 // Provide some Bootstrap tab/Drupal integration.
90 // @todo merge this into the above code from core.
91 $(context).find('.tabbable').once('bootstrap-tabs').each(function () {
92 var $wrapper = $(this);
93 var $tabs = $wrapper.find('.nav-tabs');
94 var $content = $wrapper.find('.tab-content');
95 var borderRadius = parseInt($content.css('borderBottomRightRadius'), 10);
96 var bootstrapTabResize = function() {
97 if ($wrapper.hasClass('tabs-left') || $wrapper.hasClass('tabs-right')) {
98 $content.css('min-height', $tabs.outerHeight());
101 // Add min-height on content for left and right tabs.
102 bootstrapTabResize();
103 // Detect tab switch.
104 if ($wrapper.hasClass('tabs-left') || $wrapper.hasClass('tabs-right')) {
105 $tabs.on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
106 bootstrapTabResize();
107 if ($wrapper.hasClass('tabs-left')) {
108 if ($(e.target).parent().is(':first-child')) {
109 $content.css('borderTopLeftRadius', '0');
112 $content.css('borderTopLeftRadius', borderRadius + 'px');
116 if ($(e.target).parent().is(':first-child')) {
117 $content.css('borderTopRightRadius', '0');
120 $content.css('borderTopRightRadius', borderRadius + 'px');
130 * The vertical tab object represents a single tab within a tab group.
133 * An object with the following keys:
134 * - title: The name of the tab.
135 * - details: The jQuery object of the details element that is the tab pane.
137 Drupal.verticalTab = function (settings) {
139 $.extend(this, settings, Drupal.theme('verticalTab', settings));
141 this.link.attr('href', '#' + settings.details.attr('id'));
143 this.link.on('click', function (e) {
148 // Keyboard events added:
149 // Pressing the Enter key will open the tab pane.
150 this.link.on('keydown', function (event) {
151 event.preventDefault();
152 if (event.keyCode === 13) {
154 // Set focus on the first input field of the visible details/tab pane.
155 $(".vertical-tabs-pane :input:visible:enabled:first").trigger('focus');
160 .on('summaryUpdated', function () {
161 self.updateSummary();
163 .trigger('summaryUpdated');
166 Drupal.verticalTab.prototype = {
168 * Displays the tab's content pane.
172 .siblings('.vertical-tabs-pane')
174 $(this).removeClass('active').find('> div').removeClass('in');
175 var tab = $(this).data('verticalTab');
176 tab.item.removeClass('selected');
180 .siblings(':hidden.vertical-tabs-active-tab')
181 .val(this.details.attr('id'));
182 this.details.find('> div').addClass('in');
183 this.details.data('verticalTab').item.find('a').tab('show');
184 this.item.addClass('selected');
185 // Mark the active tab for screen readers.
186 $('#active-vertical-tab').remove();
187 this.link.append('<span id="active-vertical-tab" class="visually-hidden">' + Drupal.t('(active tab)') + '</span>');
191 * Updates the tab's summary.
193 updateSummary: function () {
194 this.summary.html(this.details.drupalGetSummary());
198 * Shows a vertical tab pane.
200 tabShow: function () {
203 // Show the vertical tabs.
204 this.item.closest('.form-type-vertical-tabs').show();
205 // Update .first marker for items. We need recurse from parent to retain the
206 // actual DOM element order as jQuery implements sortOrder, but not as public
208 this.item.parent().children('.vertical-tab-button').removeClass('first')
209 .filter(':visible:first').addClass('first');
210 // Display the details element.
211 this.details.removeClass('vertical-tab-hidden').show();
218 * Hides a vertical tab pane.
220 tabHide: function () {
223 // Update .first marker for items. We need recurse from parent to retain the
224 // actual DOM element order as jQuery implements sortOrder, but not as public
226 this.item.parent().children('.vertical-tab-button').removeClass('first')
227 .filter(':visible:first').addClass('first');
228 // Hide the details element.
229 this.details.addClass('vertical-tab-hidden').hide();
230 // Focus the first visible tab (if there is one).
231 var $firstTab = this.details.siblings('.vertical-tabs-pane:not(.vertical-tab-hidden):first');
232 if ($firstTab.length) {
233 $firstTab.data('verticalTab').focus();
235 // Hide the vertical tabs (if no tabs remain).
237 this.item.closest('.form-type-vertical-tabs').hide();
244 * Theme function for a vertical tab.
247 * An object with the following keys:
248 * - title: The name of the tab.
250 * This function has to return an object with at least these keys:
251 * - item: The root tab jQuery element
252 * - link: The anchor tag that acts as the clickable area of the tab
254 * - summary: The jQuery element that contains the tab summary
256 Drupal.theme.verticalTab = function (settings) {
258 tab.item = $('<li class="vertical-tab-button" tabindex="-1"></li>')
259 .append(tab.link = $('<a href="#' + settings.details[0].id + '" data-toggle="tab"></a>')
260 .append(tab.title = $('<span></span>').html(settings.title))
261 .append(tab.summary = $('<div class="summary"></div>')
267 })(jQuery, this, Drupal, drupalSettings);