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);
|