annotate core/misc/vertical-tabs.es6.js @ 19:fa3358dc1485 tip

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