]> git.ipfire.org Git - thirdparty/bootstrap.git/blob - js/src/tab.js
Merge remote-tracking branch 'remotes/origin/v513'
[thirdparty/bootstrap.git] / js / src / tab.js
1 /**
2 * --------------------------------------------------------------------------
3 * Bootstrap (v5.1.3): tab.js
4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 * --------------------------------------------------------------------------
6 */
7
8 import {
9 defineJQueryPlugin,
10 getElementFromSelector,
11 isDisabled,
12 reflow
13 } from './util/index'
14 import EventHandler from './dom/event-handler'
15 import SelectorEngine from './dom/selector-engine'
16 import BaseComponent from './base-component'
17
18 /**
19 * ------------------------------------------------------------------------
20 * Constants
21 * ------------------------------------------------------------------------
22 */
23
24 const NAME = 'tab'
25 const DATA_KEY = 'bs.tab'
26 const EVENT_KEY = `.${DATA_KEY}`
27 const DATA_API_KEY = '.data-api'
28
29 const EVENT_HIDE = `hide${EVENT_KEY}`
30 const EVENT_HIDDEN = `hidden${EVENT_KEY}`
31 const EVENT_SHOW = `show${EVENT_KEY}`
32 const EVENT_SHOWN = `shown${EVENT_KEY}`
33 const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
34
35 const CLASS_NAME_DROPDOWN_MENU = 'dropdown-menu'
36 const CLASS_NAME_ACTIVE = 'active'
37 const CLASS_NAME_FADE = 'fade'
38 const CLASS_NAME_SHOW = 'show'
39
40 const SELECTOR_DROPDOWN = '.dropdown'
41 const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
42 const SELECTOR_ACTIVE = '.active'
43 const SELECTOR_ACTIVE_UL = ':scope > li > .active'
44 const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]'
45 const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
46 const SELECTOR_DROPDOWN_ACTIVE_CHILD = ':scope > .dropdown-menu .active'
47
48 /**
49 * ------------------------------------------------------------------------
50 * Class Definition
51 * ------------------------------------------------------------------------
52 */
53
54 class Tab extends BaseComponent {
55 // Getters
56
57 static get NAME() {
58 return NAME
59 }
60
61 // Public
62
63 show() {
64 if ((this._element.parentNode &&
65 this._element.parentNode.nodeType === Node.ELEMENT_NODE &&
66 this._element.classList.contains(CLASS_NAME_ACTIVE))) {
67 return
68 }
69
70 let previous
71 const target = getElementFromSelector(this._element)
72 const listElement = this._element.closest(SELECTOR_NAV_LIST_GROUP)
73
74 if (listElement) {
75 const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? SELECTOR_ACTIVE_UL : SELECTOR_ACTIVE
76 previous = SelectorEngine.find(itemSelector, listElement)
77 previous = previous[previous.length - 1]
78 }
79
80 const hideEvent = previous ?
81 EventHandler.trigger(previous, EVENT_HIDE, {
82 relatedTarget: this._element
83 }) :
84 null
85
86 const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
87 relatedTarget: previous
88 })
89
90 if (showEvent.defaultPrevented || (hideEvent !== null && hideEvent.defaultPrevented)) {
91 return
92 }
93
94 this._activate(this._element, listElement)
95
96 const complete = () => {
97 EventHandler.trigger(previous, EVENT_HIDDEN, {
98 relatedTarget: this._element
99 })
100 EventHandler.trigger(this._element, EVENT_SHOWN, {
101 relatedTarget: previous
102 })
103 }
104
105 if (target) {
106 this._activate(target, target.parentNode, complete)
107 } else {
108 complete()
109 }
110 }
111
112 // Private
113
114 _activate(element, container, callback) {
115 const activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') ?
116 SelectorEngine.find(SELECTOR_ACTIVE_UL, container) :
117 SelectorEngine.children(container, SELECTOR_ACTIVE)
118
119 const active = activeElements[0]
120 const isTransitioning = callback && (active && active.classList.contains(CLASS_NAME_FADE))
121
122 const complete = () => this._transitionComplete(element, active, callback)
123
124 if (active && isTransitioning) {
125 active.classList.remove(CLASS_NAME_SHOW)
126 this._queueCallback(complete, element, true)
127 } else {
128 complete()
129 }
130 }
131
132 _transitionComplete(element, active, callback) {
133 if (active) {
134 active.classList.remove(CLASS_NAME_ACTIVE)
135
136 const dropdownChild = SelectorEngine.findOne(SELECTOR_DROPDOWN_ACTIVE_CHILD, active.parentNode)
137
138 if (dropdownChild) {
139 dropdownChild.classList.remove(CLASS_NAME_ACTIVE)
140 }
141
142 if (active.getAttribute('role') === 'tab') {
143 active.setAttribute('aria-selected', false)
144 }
145 }
146
147 element.classList.add(CLASS_NAME_ACTIVE)
148 if (element.getAttribute('role') === 'tab') {
149 element.setAttribute('aria-selected', true)
150 }
151
152 reflow(element)
153
154 if (element.classList.contains(CLASS_NAME_FADE)) {
155 element.classList.add(CLASS_NAME_SHOW)
156 }
157
158 let parent = element.parentNode
159 if (parent && parent.nodeName === 'LI') {
160 parent = parent.parentNode
161 }
162
163 if (parent && parent.classList.contains(CLASS_NAME_DROPDOWN_MENU)) {
164 const dropdownElement = element.closest(SELECTOR_DROPDOWN)
165
166 if (dropdownElement) {
167 for (const dropdown of SelectorEngine.find(SELECTOR_DROPDOWN_TOGGLE, dropdownElement)) {
168 dropdown.classList.add(CLASS_NAME_ACTIVE)
169 }
170 }
171
172 element.setAttribute('aria-expanded', true)
173 }
174
175 if (callback) {
176 callback()
177 }
178 }
179
180 // Static
181
182 static jQueryInterface(config) {
183 return this.each(function () {
184 const data = Tab.getOrCreateInstance(this)
185
186 if (typeof config === 'string') {
187 if (typeof data[config] === 'undefined') {
188 throw new TypeError(`No method named "${config}"`)
189 }
190
191 data[config]()
192 }
193 })
194 }
195 }
196
197 /**
198 * ------------------------------------------------------------------------
199 * Data Api implementation
200 * ------------------------------------------------------------------------
201 */
202
203 EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
204 if (['A', 'AREA'].includes(this.tagName)) {
205 event.preventDefault()
206 }
207
208 if (isDisabled(this)) {
209 return
210 }
211
212 const data = Tab.getOrCreateInstance(this)
213 data.show()
214 })
215
216 /**
217 * ------------------------------------------------------------------------
218 * jQuery
219 * ------------------------------------------------------------------------
220 * add .Tab to jQuery only if jQuery is present
221 */
222
223 defineJQueryPlugin(Tab)
224
225 export default Tab