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.
@@ -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
+ }