PageSourceSearch

https://eng.taiwan.net.tw/common/js/plugins/a11y-tabs.js

js taiwan.net.tw collected 2026-09-24 08:42:12 UTC 7,116 bytes, 272 lines download raw bytes

1/*
2*   This content is licensed according to the W3C Software License at
3*   https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
4*/
5function a11ytabsInit($el, onChangeCallBackFunc = null) {
6  var tablist = $el;
7  if(!tablist) return;
8  var tabs;
9  var panels;
10  var delay = determineDelay();
11  var isAfterInit = false;
12  var defaultTab;
13
14  generateArrays();
15
16  function generateArrays () {
17    tabs = tablist.querySelector('[role="tablist"]').querySelectorAll(':scope > [role="tab"]');
18    panels = tablist.querySelectorAll(':scope > [role="tabpanel"]');
19    defaultTab = Array.from(tabs).find((tab) => tab.getAttribute('aria-selected') === 'true');
20  };
21
22  // For easy reference
23  var keys = {
24    end: 35,
25    home: 36,
26    left: 37,
27    up: 38,
28    right: 39,
29    down: 40,
30    delete: 46
31  };
32
33  // Add or substract depending on key pressed
34  var direction = {
35    37: -1,
36    38: -1,
37    39: 1,
38    40: 1
39  };
40
41  // Bind listeners
42  for (i = 0; i < tabs.length; ++i) {
43    addListeners(i);
44  };
45
46  function addListeners (index) {
47    $(tabs[index]).on('click', clickEventListener);
48    $(tabs[index]).on('keydown', keydownEventListener);
49    $(tabs[index]).on('keyup', keyupEventListener);
50
51    // Build an array with all tabs (<button>s) in it
52    tabs[index].index = index;
53  };
54  activateTab(defaultTab ? defaultTab : tabs[0], false);
55  isAfterInit = true;
56
57  // When a tab is clicked, activateTab is fired to activate it
58  function clickEventListener (event) {
59    event.preventDefault();
60    var tab = event.target;
61    activateTab(tab, false);
62  };
63
64  // Handle keydown on tabs
65  function keydownEventListener (event) {
66    var key = event.keyCode;
67
68    switch (key) {
69      case keys.end:
70        event.preventDefault();
71        // Activate last tab
72        activateTab(tabs[tabs.length - 1]);
73        break;
74      case keys.home:
75        event.preventDefault();
76        // Activate first tab
77        activateTab(tabs[0]);
78        break;
79
80      // Up and down are in keydown
81      // because we need to prevent page scroll >:)
82      case keys.up:
83      case keys.down:
84        determineOrientation(event);
85        break;
86    };
87  };
88
89  // Handle keyup on tabs
90  function keyupEventListener (event) {
91    var key = event.keyCode;
92
93    switch (key) {
94      case keys.left:
95      case keys.right:
96        determineOrientation(event);
97        break;
98      case keys.delete:
99        determineDeletable(event);
100        break;
101    };
102  };
103
104  // When a tablist’s aria-orientation is set to vertical,
105  // only up and down arrow should function.
106  // In all other cases only left and right arrow function.
107  function determineOrientation (event) {
108    var key = event.keyCode;
109    var vertical = tablist.getAttribute('aria-orientation') == 'vertical';
110    var proceed = false;
111    if (vertical) {
112      if (key === keys.up || key === keys.down) {
113        event.preventDefault();
114        proceed = true;
115      };
116    }
117    else {
118      if (key === keys.left || key === keys.right) {
119        proceed = true;
120      };
121    };
122
123    if (proceed) {
124      switchTabOnArrowPress(event);
125    };
126  };
127
128  // Either focus the next, previous, first, or last tab
129  // depening on key pressed
130  function switchTabOnArrowPress (event) {
131    var pressed = event.keyCode;
132
133    for (x = 0; x < tabs.length; x++) {
134      tabs[x].addEventListener('focus', focusEventHandler);
135    };
136
137    if (direction[pressed]) {
138      var target = event.target;
139      if (target.index !== undefined) {
140        if (tabs[target.index + direction[pressed]]) {
141          tabs[target.index + direction[pressed]].focus();
142        }
143        else if (pressed === keys.left || pressed === keys.up) {
144          focusLastTab();
145        }
146        else if (pressed === keys.right || pressed == keys.down) {
147          focusFirstTab();
148        };
149      };
150    };
151  };
152
153  // Activates any given tab panel
154  function activateTab (tab, setFocus) {
155    // Deactivate all other tabs
156    deactivateTabs();
157    var setFocus = setFocus === undefined ? true : setFocus;
158
159    // Remove tabindex attribute
160    tab.removeAttribute('tabindex');
161
162    // Set the tab as selected
163    tab.setAttribute('aria-selected', 'true')
164    tab.classList.add('is-active');
165
166    // Get the value of aria-controls (which is an ID)
167    var controls = tab.getAttribute('aria-controls');
168
169    // Remove hidden attribute from tab panel to make it visible
170    document.getElementById(controls).removeAttribute('hidden');
171
172    if(onChangeCallBackFunc !== null && isAfterInit) {
173      onChangeCallBackFunc(tab, controls);
174    }
175
176    // Set focus when required
177    if (setFocus) {
178      tab.focus();
179    };
180  };
181
182  // Deactivate all tabs and tab panels
183  function deactivateTabs () {
184    for (t = 0; t < tabs.length; t++) {
185      tabs[t].setAttribute('tabindex', '-1');
186      tabs[t].setAttribute('aria-selected', 'false')
187      tabs[t].classList.remove('is-active');
188      tabs[t].removeEventListener('focus', focusEventHandler);
189    };
190
191    for (p = 0; p < panels.length; p++) {
192      panels[p].setAttribute('hidden', 'hidden');
193    };
194  };
195
196  // Make a guess
197  function focusFirstTab () {
198    tabs[0].focus();
199  };
200
201  // Make a guess
202  function focusLastTab () {
203    tabs[tabs.length - 1].focus();
204  };
205
206  // Detect if a tab is deletable
207  function determineDeletable (event) {
208    target = event.target;
209
210    if (target.getAttribute('data-deletable') !== null) {
211      // Delete target tab
212      deleteTab(event, target);
213
214      // Update arrays related to tabs widget
215      generateArrays();
216
217      // Activate the closest tab to the one that was just deleted
218      if (target.index - 1 < 0) {
219        activateTab(tabs[0]);
220      }
221      else {
222        activateTab(tabs[target.index - 1]);
223      };
224    };
225  };
226
227  // Deletes a tab and its panel
228  function deleteTab (event) {
229    var target = event.target;
230    var panel = document.getElementById(target.getAttribute('aria-controls'));
231
232    target.parentElement.removeChild(target);
233    panel.parentElement.removeChild(panel);
234  };
235
236  // Determine whether there should be a delay
237  // when user navigates with the arrow keys
238  function determineDelay () {
239    var hasDelay = tablist.hasAttribute('data-delay');
240    var delay = 0;
241
242    if (hasDelay) {
243      var delayValue = tablist.getAttribute('data-delay');
244      if (delayValue) {
245        delay = delayValue;
246      }
247      else {
248        // If no value is specified, default to 300ms
249        delay = 300;
250      };
251    };
252
253    return delay;
254  };
255
256  //
257  function focusEventHandler (event) {
258    var target = event.target;
259
260    setTimeout(checkTabFocus, delay, target);
261  };
262
263  // Only activate tab on focus if it still has focus after the delay
264  function checkTabFocus (target) {
265    focused = document.activeElement;
266
267    if (target === focused) {
268      activateTab(target, false);
269    };
270  };
271
272};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.