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