vanilla-framework 4.34.1 → 4.34.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -2
- package/scss/_layouts_application.scss +3 -3
- package/scss/_layouts_docs.scss +1 -1
- package/scss/_layouts_full-width.scss +1 -1
- package/scss/_layouts_site.scss +1 -1
- package/scss/_patterns_navigation.scss +5 -4
- package/scss/_patterns_side-navigation.scss +1 -1
- package/templates/static/js/example-tools.js +181 -0
- package/templates/static/js/example.js +554 -0
- package/templates/static/js/index.js +16 -0
- package/templates/static/js/package.json +3 -0
- package/templates/static/js/prism.min.js +8 -0
- package/templates/static/js/scripts.js +418 -0
- package/templates/static/js/tabs.js +111 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// Setup toggling of side navigation drawer
|
|
2
|
+
(function () {
|
|
3
|
+
// throttling function calls, by Remy Sharp
|
|
4
|
+
// http://remysharp.com/2010/07/21/throttling-function-calls/
|
|
5
|
+
const throttle = function (fn, delay) {
|
|
6
|
+
let timer = null;
|
|
7
|
+
return function () {
|
|
8
|
+
let context = this,
|
|
9
|
+
args = arguments;
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
timer = setTimeout(function () {
|
|
12
|
+
fn.apply(context, args);
|
|
13
|
+
}, delay);
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
var expandedSidenavContainer = null;
|
|
18
|
+
var lastFocus = null;
|
|
19
|
+
var ignoreFocusChanges = false;
|
|
20
|
+
var focusAfterClose = null;
|
|
21
|
+
|
|
22
|
+
// Traps the focus within the currently expanded sidenav drawer
|
|
23
|
+
function trapFocus(event) {
|
|
24
|
+
if (ignoreFocusChanges || !expandedSidenavContainer) return;
|
|
25
|
+
// skip the focus trap if the sidenav is not in the expanded status (large screens)
|
|
26
|
+
if (!expandedSidenavContainer.classList.contains('is-drawer-expanded')) return;
|
|
27
|
+
var sidenavDrawer = expandedSidenavContainer.querySelector('.p-side-navigation__drawer');
|
|
28
|
+
|
|
29
|
+
if (sidenavDrawer.contains(event.target)) {
|
|
30
|
+
lastFocus = event.target;
|
|
31
|
+
} else {
|
|
32
|
+
focusFirstDescendant(sidenavDrawer);
|
|
33
|
+
if (lastFocus == document.activeElement) {
|
|
34
|
+
focusLastDescendant(sidenavDrawer);
|
|
35
|
+
}
|
|
36
|
+
lastFocus = document.activeElement;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Attempts to focus given element
|
|
41
|
+
function attemptFocus(child) {
|
|
42
|
+
if (child.focus) {
|
|
43
|
+
ignoreFocusChanges = true;
|
|
44
|
+
child.focus();
|
|
45
|
+
ignoreFocusChanges = false;
|
|
46
|
+
return document.activeElement === child;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Focuses first child element
|
|
53
|
+
function focusFirstDescendant(element) {
|
|
54
|
+
for (var i = 0; i < element.childNodes.length; i++) {
|
|
55
|
+
var child = element.childNodes[i];
|
|
56
|
+
if (attemptFocus(child) || focusFirstDescendant(child)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Focuses last child element
|
|
64
|
+
function focusLastDescendant(element) {
|
|
65
|
+
for (var i = element.childNodes.length - 1; i >= 0; i--) {
|
|
66
|
+
var child = element.childNodes[i];
|
|
67
|
+
if (attemptFocus(child) || focusLastDescendant(child)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
Toggles the expanded/collapsed classes on side navigation element.
|
|
76
|
+
|
|
77
|
+
@param {HTMLElement} sideNavigation The side navigation element.
|
|
78
|
+
@param {Boolean} show Whether to show or hide the drawer.
|
|
79
|
+
*/
|
|
80
|
+
function toggleDrawer(sideNavigation, show) {
|
|
81
|
+
expandedSidenavContainer = show ? sideNavigation : null;
|
|
82
|
+
const toggleButtonOutsideDrawer = sideNavigation.querySelector('.p-side-navigation__toggle, .js-drawer-toggle');
|
|
83
|
+
const toggleButtonInsideDrawer = sideNavigation.querySelector('.p-side-navigation__toggle--in-drawer');
|
|
84
|
+
|
|
85
|
+
if (sideNavigation) {
|
|
86
|
+
if (show) {
|
|
87
|
+
sideNavigation.classList.remove('is-drawer-collapsed');
|
|
88
|
+
sideNavigation.classList.add('is-drawer-expanded');
|
|
89
|
+
|
|
90
|
+
toggleButtonInsideDrawer.focus();
|
|
91
|
+
toggleButtonOutsideDrawer.setAttribute('aria-expanded', true);
|
|
92
|
+
toggleButtonInsideDrawer.setAttribute('aria-expanded', true);
|
|
93
|
+
focusFirstDescendant(sideNavigation);
|
|
94
|
+
focusAfterClose = toggleButtonOutsideDrawer;
|
|
95
|
+
document.addEventListener('focus', trapFocus, true);
|
|
96
|
+
} else {
|
|
97
|
+
sideNavigation.classList.remove('is-drawer-expanded');
|
|
98
|
+
sideNavigation.classList.add('is-drawer-collapsed');
|
|
99
|
+
|
|
100
|
+
toggleButtonOutsideDrawer.focus();
|
|
101
|
+
toggleButtonOutsideDrawer.setAttribute('aria-expanded', false);
|
|
102
|
+
toggleButtonInsideDrawer.setAttribute('aria-expanded', false);
|
|
103
|
+
if (focusAfterClose && focusAfterClose.focus) {
|
|
104
|
+
focusAfterClose.focus();
|
|
105
|
+
}
|
|
106
|
+
document.removeEventListener('focus', trapFocus, true);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
Attaches event listeners for the side navigation toggles
|
|
113
|
+
@param {HTMLElement} sideNavigation The side navigation element.
|
|
114
|
+
*/
|
|
115
|
+
function setupSideNavigation(sideNavigation) {
|
|
116
|
+
var toggles = [].slice.call(sideNavigation.querySelectorAll('.js-drawer-toggle'));
|
|
117
|
+
var drawerEl = sideNavigation.querySelector('.p-side-navigation__drawer');
|
|
118
|
+
|
|
119
|
+
// hide navigation drawer on small screens
|
|
120
|
+
sideNavigation.classList.add('is-drawer-hidden');
|
|
121
|
+
|
|
122
|
+
// setup drawer element
|
|
123
|
+
drawerEl.addEventListener('animationend', () => {
|
|
124
|
+
if (!sideNavigation.classList.contains('is-drawer-expanded')) {
|
|
125
|
+
sideNavigation.classList.add('is-drawer-hidden');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
window.addEventListener('keydown', (e) => {
|
|
130
|
+
if (e.key === 'Escape') {
|
|
131
|
+
toggleDrawer(sideNavigation, false);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// setup toggle buttons
|
|
136
|
+
toggles.forEach(function (toggle) {
|
|
137
|
+
toggle.addEventListener('click', function (event) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
|
|
140
|
+
if (sideNavigation) {
|
|
141
|
+
sideNavigation.classList.remove('is-drawer-hidden');
|
|
142
|
+
toggleDrawer(sideNavigation, !sideNavigation.classList.contains('is-drawer-expanded'));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// hide side navigation drawer when screen is resized
|
|
148
|
+
window.addEventListener(
|
|
149
|
+
'resize',
|
|
150
|
+
throttle(function () {
|
|
151
|
+
toggles.forEach((toggle) => {
|
|
152
|
+
return toggle.setAttribute('aria-expanded', false);
|
|
153
|
+
});
|
|
154
|
+
// remove expanded/collapsed class names to avoid unexpected animations
|
|
155
|
+
sideNavigation.classList.remove('is-drawer-expanded');
|
|
156
|
+
sideNavigation.classList.remove('is-drawer-collapsed');
|
|
157
|
+
sideNavigation.classList.add('is-drawer-hidden');
|
|
158
|
+
}, 10),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
Attaches event listeners for all the side navigations in the document.
|
|
164
|
+
@param {String} sideNavigationSelector The CSS selector matching side navigation elements.
|
|
165
|
+
*/
|
|
166
|
+
function setupSideNavigations(sideNavigationSelector) {
|
|
167
|
+
// Setup all side navigations on the page.
|
|
168
|
+
var sideNavigations = [].slice.call(document.querySelectorAll(sideNavigationSelector));
|
|
169
|
+
|
|
170
|
+
sideNavigations.forEach(setupSideNavigation);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setupSideNavigations('.p-side-navigation, [class*="p-side-navigation--"]');
|
|
174
|
+
|
|
175
|
+
// Add table of contents to side navigation on documentation pages
|
|
176
|
+
const sideNav = document.querySelector('.p-side-navigation, [class*="p-side-navigation--"]');
|
|
177
|
+
|
|
178
|
+
// Generate id from H2s content when it does not exist
|
|
179
|
+
document.querySelectorAll('main h2:not([id])').forEach(function (heading) {
|
|
180
|
+
// Only get direct text from h2 node, excluding any child nodes
|
|
181
|
+
var id = heading.childNodes[0].textContent
|
|
182
|
+
.trim()
|
|
183
|
+
.toLowerCase()
|
|
184
|
+
.replaceAll(/\s+/g, '-')
|
|
185
|
+
.replaceAll(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '');
|
|
186
|
+
heading.setAttribute('id', id);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// get all headings from page and add it to table of contents
|
|
190
|
+
var list = document.createElement('ul');
|
|
191
|
+
list.classList.add('p-table-of-contents__list');
|
|
192
|
+
|
|
193
|
+
var item = document.createElement('li');
|
|
194
|
+
item.classList.add('p-table-of-contents__item');
|
|
195
|
+
|
|
196
|
+
var anchor = document.createElement('a');
|
|
197
|
+
anchor.classList.add('p-table-of-contents__link');
|
|
198
|
+
|
|
199
|
+
// Add all H2s with IDs to the table of contents list
|
|
200
|
+
[].slice.call(document.querySelectorAll('main h2[id]')).forEach(function (heading) {
|
|
201
|
+
var thisItem = item.cloneNode();
|
|
202
|
+
var thisAnchor = anchor.cloneNode();
|
|
203
|
+
thisAnchor.setAttribute('href', '#' + heading.id);
|
|
204
|
+
// Only get direct text from h2 node, excluding any child nodes
|
|
205
|
+
thisAnchor.textContent = heading.childNodes[0].textContent.trim();
|
|
206
|
+
thisItem.appendChild(thisAnchor);
|
|
207
|
+
list.appendChild(thisItem);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Add table of contents as nested list to side navigation
|
|
211
|
+
if (list.querySelectorAll('li').length > 0) {
|
|
212
|
+
var toc = document.querySelector('#toc');
|
|
213
|
+
if (toc) {
|
|
214
|
+
toc.appendChild(list);
|
|
215
|
+
toc.closest('.u-hide').classList.remove('u-hide');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// accordion side navigation
|
|
220
|
+
var currentPage = document.querySelector('.p-side-navigation__link[aria-current="page"]');
|
|
221
|
+
if (currentPage) {
|
|
222
|
+
var parentList = currentPage.parentNode.parentNode;
|
|
223
|
+
parentList.setAttribute('aria-expanded', true);
|
|
224
|
+
parentList.previousElementSibling.setAttribute('aria-expanded', true);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function setupSideNavigationExpandToggle(toggle) {
|
|
228
|
+
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
|
|
229
|
+
if (!isExpanded) {
|
|
230
|
+
toggle.setAttribute('aria-expanded', isExpanded);
|
|
231
|
+
}
|
|
232
|
+
const item = toggle.closest('.p-side-navigation__item');
|
|
233
|
+
const link = item.querySelector('.p-side-navigation__link');
|
|
234
|
+
const nestedList = item.querySelector('.p-side-navigation__list');
|
|
235
|
+
if (!link?.hasAttribute('aria-expanded')) {
|
|
236
|
+
link.setAttribute('aria-expanded', isExpanded);
|
|
237
|
+
}
|
|
238
|
+
if (!nestedList?.hasAttribute('aria-expanded')) {
|
|
239
|
+
nestedList.setAttribute('aria-expanded', isExpanded);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function handleExpandToggle(event) {
|
|
244
|
+
const item = event.currentTarget.closest('.p-side-navigation__item');
|
|
245
|
+
const button = item.querySelector('.p-side-navigation__expand, .p-side-navigation__accordion-button');
|
|
246
|
+
const link = item.querySelector('.p-side-navigation__link');
|
|
247
|
+
const nestedList = item.querySelector('.p-side-navigation__list');
|
|
248
|
+
|
|
249
|
+
[button, link, nestedList].forEach((el) => {
|
|
250
|
+
el.setAttribute('aria-expanded', el.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function setupSideNavigationExpands() {
|
|
255
|
+
var expandToggles = document.querySelectorAll('.p-side-navigation__expand, .p-side-navigation__accordion-button');
|
|
256
|
+
expandToggles.forEach((toggle) => {
|
|
257
|
+
setupSideNavigationExpandToggle(toggle);
|
|
258
|
+
toggle.addEventListener('click', (e) => {
|
|
259
|
+
handleExpandToggle(e);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setupSideNavigationExpands();
|
|
265
|
+
})();
|
|
266
|
+
|
|
267
|
+
// scroll active side navigation item into view (without scrolling whole page)
|
|
268
|
+
(function () {
|
|
269
|
+
var sideNav = document.querySelector('.p-side-navigation');
|
|
270
|
+
var currentItem = document.querySelector('.p-side-navigation__link[aria-current="page"]');
|
|
271
|
+
|
|
272
|
+
if (sideNav && currentItem) {
|
|
273
|
+
// calculate scroll by comparing top of side nav and top of active item
|
|
274
|
+
var currentItemOffset = currentItem.getBoundingClientRect().top;
|
|
275
|
+
var offset = currentItemOffset - sideNav.getBoundingClientRect().top;
|
|
276
|
+
|
|
277
|
+
// only scroll if active link is off screen or close to bottom of the window
|
|
278
|
+
if (currentItemOffset > window.innerHeight * 0.7) {
|
|
279
|
+
setTimeout(function () {
|
|
280
|
+
sideNav.scrollTop = offset;
|
|
281
|
+
}, 0);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
})();
|
|
285
|
+
|
|
286
|
+
// Docs search functions
|
|
287
|
+
(function () {
|
|
288
|
+
var searchDocsReset = document.getElementById('search-docs-reset');
|
|
289
|
+
var searchBox = document.getElementById('search-docs');
|
|
290
|
+
|
|
291
|
+
if (searchDocsReset) {
|
|
292
|
+
searchDocsReset.addEventListener('click', function (e) {
|
|
293
|
+
searchBox.value = '';
|
|
294
|
+
searchBox.focus();
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
})();
|
|
299
|
+
|
|
300
|
+
(function () {
|
|
301
|
+
function initNavigationSearch(element) {
|
|
302
|
+
const searchButtons = element.querySelectorAll('.js-search-button');
|
|
303
|
+
|
|
304
|
+
searchButtons.forEach((searchButton) => {
|
|
305
|
+
searchButton.addEventListener('click', toggleSearch);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const menuButton = element.querySelector('.js-menu-button');
|
|
309
|
+
if (menuButton) {
|
|
310
|
+
menuButton.addEventListener('click', toggleMenu);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const overlay = element.querySelector('.p-navigation__search-overlay');
|
|
314
|
+
if (overlay) {
|
|
315
|
+
overlay.addEventListener('click', closeAll);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function toggleMenu(e) {
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
|
|
321
|
+
var navigation = e.target.closest('.p-navigation');
|
|
322
|
+
if (navigation.classList.contains('has-menu-open')) {
|
|
323
|
+
closeAll();
|
|
324
|
+
} else {
|
|
325
|
+
closeAll();
|
|
326
|
+
openMenu(e);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function toggleSearch(e) {
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
|
|
333
|
+
var navigation = e.target.closest('.p-navigation');
|
|
334
|
+
if (navigation.classList.contains('has-search-open')) {
|
|
335
|
+
closeAll();
|
|
336
|
+
} else {
|
|
337
|
+
closeAll();
|
|
338
|
+
openSearch(e);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function openSearch(e) {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
var navigation = e.target.closest('.p-navigation');
|
|
345
|
+
var nav = navigation.querySelector('.p-navigation__nav');
|
|
346
|
+
|
|
347
|
+
var searchInput = navigation.querySelector('.p-search-box__input');
|
|
348
|
+
var buttons = document.querySelectorAll('.js-search-button');
|
|
349
|
+
|
|
350
|
+
buttons.forEach((searchButton) => {
|
|
351
|
+
searchButton.setAttribute('aria-pressed', true);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
navigation.classList.add('has-search-open');
|
|
355
|
+
searchInput.focus();
|
|
356
|
+
document.addEventListener('keyup', keyPressHandler);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function openMenu(e) {
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
var navigation = e.target.closest('.p-navigation');
|
|
362
|
+
var nav = navigation.querySelector('.p-navigation__nav');
|
|
363
|
+
|
|
364
|
+
var buttons = document.querySelectorAll('.js-menu-button');
|
|
365
|
+
|
|
366
|
+
buttons.forEach((searchButton) => {
|
|
367
|
+
searchButton.setAttribute('aria-pressed', true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
navigation.classList.add('has-menu-open');
|
|
371
|
+
document.addEventListener('keyup', keyPressHandler);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function closeSearch() {
|
|
375
|
+
var navigation = document.querySelector('.p-navigation');
|
|
376
|
+
var nav = navigation.querySelector('.p-navigation__nav');
|
|
377
|
+
|
|
378
|
+
var banner = document.querySelector('.p-navigation__banner');
|
|
379
|
+
var buttons = document.querySelectorAll('.js-search-button');
|
|
380
|
+
|
|
381
|
+
buttons.forEach((searchButton) => {
|
|
382
|
+
searchButton.removeAttribute('aria-pressed');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
navigation.classList.remove('has-search-open');
|
|
386
|
+
document.removeEventListener('keyup', keyPressHandler);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function closeMenu() {
|
|
390
|
+
var navigation = document.querySelector('.p-navigation');
|
|
391
|
+
var nav = navigation.querySelector('.p-navigation__nav');
|
|
392
|
+
|
|
393
|
+
var banner = document.querySelector('.p-navigation__banner');
|
|
394
|
+
var buttons = document.querySelectorAll('.js-menu-button');
|
|
395
|
+
|
|
396
|
+
buttons.forEach((searchButton) => {
|
|
397
|
+
searchButton.removeAttribute('aria-pressed');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
navigation.classList.remove('has-menu-open');
|
|
401
|
+
document.removeEventListener('keyup', keyPressHandler);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function closeAll() {
|
|
405
|
+
closeSearch();
|
|
406
|
+
closeMenu();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function keyPressHandler(e) {
|
|
410
|
+
if (e.key === 'Escape') {
|
|
411
|
+
closeAll();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
var navigation = document.querySelector('#navigation');
|
|
417
|
+
initNavigationSearch(navigation);
|
|
418
|
+
})();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const keys = {
|
|
2
|
+
left: 'ArrowLeft',
|
|
3
|
+
right: 'ArrowRight',
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const direction = {
|
|
7
|
+
ArrowLeft: -1,
|
|
8
|
+
ArrowRight: 1,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
Attaches a number of events that each trigger
|
|
13
|
+
the reveal of the chosen tab content
|
|
14
|
+
@param {Array} tabs an array of tabs within a container
|
|
15
|
+
*/
|
|
16
|
+
function attachEvents(tabs) {
|
|
17
|
+
tabs.forEach(function (tab, index) {
|
|
18
|
+
tab.addEventListener('keyup', function (e) {
|
|
19
|
+
if (e.code === keys.left || e.code === keys.right) {
|
|
20
|
+
switchTabOnArrowPress(e, tabs);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
tab.addEventListener('click', function (e) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
setActiveTab(tab, tabs);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
tab.addEventListener('focus', function () {
|
|
30
|
+
setActiveTab(tab, tabs);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
tab.index = index;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
Determine which tab to show when an arrow key is pressed
|
|
39
|
+
@param {KeyboardEvent} event
|
|
40
|
+
@param {Array} tabs an array of tabs within a container
|
|
41
|
+
*/
|
|
42
|
+
function switchTabOnArrowPress(event, tabs) {
|
|
43
|
+
var pressed = event.code;
|
|
44
|
+
|
|
45
|
+
if (direction[pressed]) {
|
|
46
|
+
var target = event.target;
|
|
47
|
+
if (target.index !== undefined) {
|
|
48
|
+
if (tabs[target.index + direction[pressed]]) {
|
|
49
|
+
tabs[target.index + direction[pressed]].focus();
|
|
50
|
+
} else if (pressed === keys.left) {
|
|
51
|
+
tabs[tabs.length - 1].focus();
|
|
52
|
+
} else if (pressed === keys.right) {
|
|
53
|
+
tabs[0].focus();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
Cycles through an array of tab elements and ensures
|
|
61
|
+
only the target tab and its content are selected
|
|
62
|
+
@param {HTMLElement} tab the tab whose content will be shown
|
|
63
|
+
@param {Array} tabs an array of tabs within a container
|
|
64
|
+
*/
|
|
65
|
+
function setActiveTab(tab, tabs) {
|
|
66
|
+
tabs.forEach(function (tabElement) {
|
|
67
|
+
var tabContent = document.getElementById(tabElement.getAttribute('aria-controls'));
|
|
68
|
+
|
|
69
|
+
if (tabElement === tab) {
|
|
70
|
+
tabElement.setAttribute('aria-selected', true);
|
|
71
|
+
tabContent.removeAttribute('hidden');
|
|
72
|
+
} else {
|
|
73
|
+
tabElement.setAttribute('aria-selected', false);
|
|
74
|
+
tabContent.setAttribute('hidden', true);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
Attaches events to tab links within a given parent element,
|
|
81
|
+
and sets the active tab if the current hash matches the id
|
|
82
|
+
of an element controlled by a tab link
|
|
83
|
+
@param {String} selector class name of the element
|
|
84
|
+
containing the tabs we want to attach events to
|
|
85
|
+
*/
|
|
86
|
+
export function initTabs(selector) {
|
|
87
|
+
const tabContainers = [].slice.call(document.querySelectorAll(selector));
|
|
88
|
+
|
|
89
|
+
tabContainers.forEach(function (tabContainer) {
|
|
90
|
+
const tabs = [].slice.call(tabContainer.querySelectorAll('[aria-controls]'));
|
|
91
|
+
attachEvents(tabs);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Auto-initialize tabs when DOM is loaded
|
|
97
|
+
* This maintains backward compatibility for existing implementations
|
|
98
|
+
*/
|
|
99
|
+
export function autoInit() {
|
|
100
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
101
|
+
initTabs('[role="tablist"]');
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Export individual functions for more granular usage
|
|
106
|
+
export {attachEvents, switchTabOnArrowPress, setActiveTab};
|
|
107
|
+
|
|
108
|
+
// Auto-initialize by default to maintain backward compatibility
|
|
109
|
+
if (typeof document !== 'undefined') {
|
|
110
|
+
autoInit();
|
|
111
|
+
}
|