source: imaps-frontend/node_modules/bootstrap/js/src/tab.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 8.9 KB
Line 
1/**
2 * --------------------------------------------------------------------------
3 * Bootstrap tab.js
4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 * --------------------------------------------------------------------------
6 */
7
8import BaseComponent from './base-component.js'
9import EventHandler from './dom/event-handler.js'
10import SelectorEngine from './dom/selector-engine.js'
11import { defineJQueryPlugin, getNextActiveElement, isDisabled } from './util/index.js'
12
13/**
14 * Constants
15 */
16
17const NAME = 'tab'
18const DATA_KEY = 'bs.tab'
19const EVENT_KEY = `.${DATA_KEY}`
20
21const EVENT_HIDE = `hide${EVENT_KEY}`
22const EVENT_HIDDEN = `hidden${EVENT_KEY}`
23const EVENT_SHOW = `show${EVENT_KEY}`
24const EVENT_SHOWN = `shown${EVENT_KEY}`
25const EVENT_CLICK_DATA_API = `click${EVENT_KEY}`
26const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
27const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`
28
29const ARROW_LEFT_KEY = 'ArrowLeft'
30const ARROW_RIGHT_KEY = 'ArrowRight'
31const ARROW_UP_KEY = 'ArrowUp'
32const ARROW_DOWN_KEY = 'ArrowDown'
33const HOME_KEY = 'Home'
34const END_KEY = 'End'
35
36const CLASS_NAME_ACTIVE = 'active'
37const CLASS_NAME_FADE = 'fade'
38const CLASS_NAME_SHOW = 'show'
39const CLASS_DROPDOWN = 'dropdown'
40
41const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
42const SELECTOR_DROPDOWN_MENU = '.dropdown-menu'
43const NOT_SELECTOR_DROPDOWN_TOGGLE = `:not(${SELECTOR_DROPDOWN_TOGGLE})`
44
45const SELECTOR_TAB_PANEL = '.list-group, .nav, [role="tablist"]'
46const SELECTOR_OUTER = '.nav-item, .list-group-item'
47const SELECTOR_INNER = `.nav-link${NOT_SELECTOR_DROPDOWN_TOGGLE}, .list-group-item${NOT_SELECTOR_DROPDOWN_TOGGLE}, [role="tab"]${NOT_SELECTOR_DROPDOWN_TOGGLE}`
48const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]' // TODO: could only be `tab` in v6
49const SELECTOR_INNER_ELEM = `${SELECTOR_INNER}, ${SELECTOR_DATA_TOGGLE}`
50
51const SELECTOR_DATA_TOGGLE_ACTIVE = `.${CLASS_NAME_ACTIVE}[data-bs-toggle="tab"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="pill"], .${CLASS_NAME_ACTIVE}[data-bs-toggle="list"]`
52
53/**
54 * Class definition
55 */
56
57class Tab extends BaseComponent {
58 constructor(element) {
59 super(element)
60 this._parent = this._element.closest(SELECTOR_TAB_PANEL)
61
62 if (!this._parent) {
63 return
64 // TODO: should throw exception in v6
65 // throw new TypeError(`${element.outerHTML} has not a valid parent ${SELECTOR_INNER_ELEM}`)
66 }
67
68 // Set up initial aria attributes
69 this._setInitialAttributes(this._parent, this._getChildren())
70
71 EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))
72 }
73
74 // Getters
75 static get NAME() {
76 return NAME
77 }
78
79 // Public
80 show() { // Shows this elem and deactivate the active sibling if exists
81 const innerElem = this._element
82 if (this._elemIsActive(innerElem)) {
83 return
84 }
85
86 // Search for active tab on same parent to deactivate it
87 const active = this._getActiveElem()
88
89 const hideEvent = active ?
90 EventHandler.trigger(active, EVENT_HIDE, { relatedTarget: innerElem }) :
91 null
92
93 const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, { relatedTarget: active })
94
95 if (showEvent.defaultPrevented || (hideEvent && hideEvent.defaultPrevented)) {
96 return
97 }
98
99 this._deactivate(active, innerElem)
100 this._activate(innerElem, active)
101 }
102
103 // Private
104 _activate(element, relatedElem) {
105 if (!element) {
106 return
107 }
108
109 element.classList.add(CLASS_NAME_ACTIVE)
110
111 this._activate(SelectorEngine.getElementFromSelector(element)) // Search and activate/show the proper section
112
113 const complete = () => {
114 if (element.getAttribute('role') !== 'tab') {
115 element.classList.add(CLASS_NAME_SHOW)
116 return
117 }
118
119 element.removeAttribute('tabindex')
120 element.setAttribute('aria-selected', true)
121 this._toggleDropDown(element, true)
122 EventHandler.trigger(element, EVENT_SHOWN, {
123 relatedTarget: relatedElem
124 })
125 }
126
127 this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE))
128 }
129
130 _deactivate(element, relatedElem) {
131 if (!element) {
132 return
133 }
134
135 element.classList.remove(CLASS_NAME_ACTIVE)
136 element.blur()
137
138 this._deactivate(SelectorEngine.getElementFromSelector(element)) // Search and deactivate the shown section too
139
140 const complete = () => {
141 if (element.getAttribute('role') !== 'tab') {
142 element.classList.remove(CLASS_NAME_SHOW)
143 return
144 }
145
146 element.setAttribute('aria-selected', false)
147 element.setAttribute('tabindex', '-1')
148 this._toggleDropDown(element, false)
149 EventHandler.trigger(element, EVENT_HIDDEN, { relatedTarget: relatedElem })
150 }
151
152 this._queueCallback(complete, element, element.classList.contains(CLASS_NAME_FADE))
153 }
154
155 _keydown(event) {
156 if (!([ARROW_LEFT_KEY, ARROW_RIGHT_KEY, ARROW_UP_KEY, ARROW_DOWN_KEY, HOME_KEY, END_KEY].includes(event.key))) {
157 return
158 }
159
160 event.stopPropagation()// stopPropagation/preventDefault both added to support up/down keys without scrolling the page
161 event.preventDefault()
162
163 const children = this._getChildren().filter(element => !isDisabled(element))
164 let nextActiveElement
165
166 if ([HOME_KEY, END_KEY].includes(event.key)) {
167 nextActiveElement = children[event.key === HOME_KEY ? 0 : children.length - 1]
168 } else {
169 const isNext = [ARROW_RIGHT_KEY, ARROW_DOWN_KEY].includes(event.key)
170 nextActiveElement = getNextActiveElement(children, event.target, isNext, true)
171 }
172
173 if (nextActiveElement) {
174 nextActiveElement.focus({ preventScroll: true })
175 Tab.getOrCreateInstance(nextActiveElement).show()
176 }
177 }
178
179 _getChildren() { // collection of inner elements
180 return SelectorEngine.find(SELECTOR_INNER_ELEM, this._parent)
181 }
182
183 _getActiveElem() {
184 return this._getChildren().find(child => this._elemIsActive(child)) || null
185 }
186
187 _setInitialAttributes(parent, children) {
188 this._setAttributeIfNotExists(parent, 'role', 'tablist')
189
190 for (const child of children) {
191 this._setInitialAttributesOnChild(child)
192 }
193 }
194
195 _setInitialAttributesOnChild(child) {
196 child = this._getInnerElement(child)
197 const isActive = this._elemIsActive(child)
198 const outerElem = this._getOuterElement(child)
199 child.setAttribute('aria-selected', isActive)
200
201 if (outerElem !== child) {
202 this._setAttributeIfNotExists(outerElem, 'role', 'presentation')
203 }
204
205 if (!isActive) {
206 child.setAttribute('tabindex', '-1')
207 }
208
209 this._setAttributeIfNotExists(child, 'role', 'tab')
210
211 // set attributes to the related panel too
212 this._setInitialAttributesOnTargetPanel(child)
213 }
214
215 _setInitialAttributesOnTargetPanel(child) {
216 const target = SelectorEngine.getElementFromSelector(child)
217
218 if (!target) {
219 return
220 }
221
222 this._setAttributeIfNotExists(target, 'role', 'tabpanel')
223
224 if (child.id) {
225 this._setAttributeIfNotExists(target, 'aria-labelledby', `${child.id}`)
226 }
227 }
228
229 _toggleDropDown(element, open) {
230 const outerElem = this._getOuterElement(element)
231 if (!outerElem.classList.contains(CLASS_DROPDOWN)) {
232 return
233 }
234
235 const toggle = (selector, className) => {
236 const element = SelectorEngine.findOne(selector, outerElem)
237 if (element) {
238 element.classList.toggle(className, open)
239 }
240 }
241
242 toggle(SELECTOR_DROPDOWN_TOGGLE, CLASS_NAME_ACTIVE)
243 toggle(SELECTOR_DROPDOWN_MENU, CLASS_NAME_SHOW)
244 outerElem.setAttribute('aria-expanded', open)
245 }
246
247 _setAttributeIfNotExists(element, attribute, value) {
248 if (!element.hasAttribute(attribute)) {
249 element.setAttribute(attribute, value)
250 }
251 }
252
253 _elemIsActive(elem) {
254 return elem.classList.contains(CLASS_NAME_ACTIVE)
255 }
256
257 // Try to get the inner element (usually the .nav-link)
258 _getInnerElement(elem) {
259 return elem.matches(SELECTOR_INNER_ELEM) ? elem : SelectorEngine.findOne(SELECTOR_INNER_ELEM, elem)
260 }
261
262 // Try to get the outer element (usually the .nav-item)
263 _getOuterElement(elem) {
264 return elem.closest(SELECTOR_OUTER) || elem
265 }
266
267 // Static
268 static jQueryInterface(config) {
269 return this.each(function () {
270 const data = Tab.getOrCreateInstance(this)
271
272 if (typeof config !== 'string') {
273 return
274 }
275
276 if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
277 throw new TypeError(`No method named "${config}"`)
278 }
279
280 data[config]()
281 })
282 }
283}
284
285/**
286 * Data API implementation
287 */
288
289EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
290 if (['A', 'AREA'].includes(this.tagName)) {
291 event.preventDefault()
292 }
293
294 if (isDisabled(this)) {
295 return
296 }
297
298 Tab.getOrCreateInstance(this).show()
299})
300
301/**
302 * Initialize on focus
303 */
304EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
305 for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE_ACTIVE)) {
306 Tab.getOrCreateInstance(element)
307 }
308})
309/**
310 * jQuery
311 */
312
313defineJQueryPlugin(Tab)
314
315export default Tab
Note: See TracBrowser for help on using the repository browser.