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.