UNPKG

9.19 kBJavaScriptView Raw
1import BaseComponent from 'bootstrap/js/src/base-component.js'
2
3import {
4 //defineJQueryPlugin,
5 getElementFromSelector,
6 isVisible,
7 reflow,
8 //typeCheckConfig,
9 //getSelectorFromElement,
10} from 'bootstrap/js/src/util'
11import EventHandler from 'bootstrap/js/src/dom/event-handler'
12import SelectorEngine from 'bootstrap/js/src/dom/selector-engine'
13
14import { isScreenMobile } from './util/device'
15import { getElementIndex } from './util/dom'
16import { disablePageScroll, enablePageScroll } from './util/pageScroll'
17
18const NAME = 'navbarcollapsible'
19const DATA_KEY = 'bs.navbarcollapsible'
20const EVENT_KEY = `.${DATA_KEY}`
21const DATA_API_KEY = '.data-api'
22
23const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
24const EVENT_CLICK = `click${EVENT_KEY}`
25const EVENT_KEYUP = `keyup${EVENT_KEY}`
26const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
27const EVENT_HIDE = `hide${EVENT_KEY}`
28const EVENT_HIDDEN = `hidden${EVENT_KEY}`
29const EVENT_SHOW = `show${EVENT_KEY}`
30const EVENT_SHOWN = `shown${EVENT_KEY}`
31const EVENT_RESIZE = `resize${EVENT_KEY}`
32
33const CLASS_NAME_FADE = 'fade'
34const CLASS_NAME_SHOW = 'show'
35const CLASS_NAME_EXPANDED = 'expanded'
36
37const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="navbarcollapsible"]'
38
39//const SELECTOR_NAVBAR = '.navbar-collapsable'
40const SELECTOR_BTN_CLOSE = '.close-div button'
41const SELECTOR_BTN_MENU_CLOSE = '.close-menu'
42const SELECTOR_BTN_BACK = '.it-back-button'
43const SELECTOR_OVERLAY = '.overlay'
44const SELECTOR_MENU_WRAPPER = '.menu-wrapper'
45const SELECTOR_NAVLINK = 'a.nav-link'
46const SELECTOR_MEGAMENUNAVLINK = '.nav-item .list-item'
47
48class NavBarCollapsible extends BaseComponent {
49 constructor(element) {
50 super(element)
51
52 this._isShown = this._element.classList.contains(CLASS_NAME_EXPANDED)
53 this._isTransitioning = false
54
55 this._isMobile = isScreenMobile()
56 this._isKeyShift = false
57
58 this._currItemIdx = 0
59
60 this._btnClose = SelectorEngine.findOne(SELECTOR_BTN_CLOSE, this._element)
61 this._btnBack = SelectorEngine.findOne(SELECTOR_BTN_BACK, this._element)
62 this._menuWrapper = SelectorEngine.findOne(SELECTOR_MENU_WRAPPER, this._element)
63 this._overlay = null
64 this._setOverlay()
65 this._menuItems = SelectorEngine.find([SELECTOR_NAVLINK, SELECTOR_MEGAMENUNAVLINK, SELECTOR_BTN_MENU_CLOSE].join(','), this._element)
66
67 this._bindEvents()
68 }
69 // Getters
70
71 static get NAME() {
72 return NAME
73 }
74
75 // Public
76
77 show(relatedTarget) {
78 if (this._isShown || this._isTransitioning) {
79 return
80 }
81
82 const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
83 relatedTarget,
84 })
85
86 if (showEvent.defaultPrevented) {
87 return
88 }
89
90 if (this._btnBack) {
91 this._btnBack.classList.add(CLASS_NAME_SHOW)
92 }
93
94 this._isShown = true
95
96 disablePageScroll()
97 this._showElement()
98 }
99
100 hide() {
101 if (!this._isShown || this._isTransitioning) {
102 return
103 }
104
105 const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
106
107 if (hideEvent.defaultPrevented) {
108 return
109 }
110
111 this._isShown = false
112
113 const isAnimated = this._isAnimated()
114
115 if (isAnimated) {
116 this._isTransitioning = true
117 }
118
119 if (this._btnBack) {
120 this._btnBack.classList.remove(CLASS_NAME_SHOW)
121 }
122 if (this._overlay) {
123 this._overlay.classList.remove(CLASS_NAME_SHOW)
124 }
125
126 this._element.classList.remove(CLASS_NAME_EXPANDED)
127
128 enablePageScroll()
129 this._queueCallback(() => this._hideElement(), this._menuWrapper, isAnimated)
130 }
131
132 toggle(relatedTarget) {
133 this._isShown ? this.hide() : this.show(relatedTarget)
134 }
135
136 dispose() {
137 EventHandler.off(window, EVENT_RESIZE)
138
139 super.dispose()
140 }
141
142 // Private
143
144 _bindEvents() {
145 EventHandler.on(window, EVENT_RESIZE, () => this._onResize())
146
147 if (this._overlay) {
148 EventHandler.on(this._overlay, EVENT_CLICK, () => this.hide())
149 }
150 EventHandler.on(this._btnClose, EVENT_CLICK, (evt) => {
151 evt.preventDefault()
152 this.hide()
153 })
154 EventHandler.on(this._btnBack, EVENT_CLICK, (evt) => {
155 evt.preventDefault()
156 this.hide()
157 })
158
159 this._menuItems.forEach((item) => {
160 EventHandler.on(item, EVENT_KEYDOWN, (evt) => this._isMobile && this._onMenuItemKeyDown(evt))
161 EventHandler.on(item, EVENT_KEYUP, (evt) => this._isMobile && this._onMenuItemKeyUp(evt))
162 EventHandler.on(item, EVENT_CLICK, (evt) => this._isMobile && this._onMenuItemClick(evt))
163 })
164 }
165
166 _onResize() {
167 this._isMobile = isScreenMobile()
168 }
169
170 _onMenuItemKeyUp(evt) {
171 if (evt.key === 'Shift') {
172 this._isKeyShift = false
173 }
174 }
175 _onMenuItemKeyDown(evt) {
176 if (evt.key === 'Shift') {
177 this._isKeyShift = true
178 }
179 if (evt.key === 'Tab') {
180 evt.preventDefault()
181 this._focusNext()
182 }
183 }
184 /**
185 * Update the last focused element when an interactive element is clicked
186 */
187 _onMenuItemClick(evt) {
188 this.currItemIdx = getElementIndex(evt.currentTarget, this._menuItems)
189 }
190
191 _isAnimated() {
192 //there's no an animation css class you can toggle with a "show" css class, so it is supposed true
193 return true //this._element.classList.contains(CLASS_NAME_EXPANDED)
194 }
195
196 _isElementHidden(element) {
197 return element.offsetParent === null
198 }
199
200 _showElement() {
201 const isAnimated = this._isAnimated()
202
203 this._element.style.display = 'block'
204 this._element.removeAttribute('aria-hidden')
205 this._element.setAttribute('aria-expanded', true)
206 //this._element.setAttribute('role', 'dialog')
207 if (this._overlay) {
208 this._overlay.style.display = 'block'
209 }
210
211 if (isAnimated) {
212 reflow(this._element)
213 }
214
215 this._element.classList.add(CLASS_NAME_EXPANDED)
216 if (this._overlay) {
217 this._overlay.classList.add(CLASS_NAME_SHOW)
218 }
219
220 const transitionComplete = () => {
221 this._isTransitioning = false
222 const firstItem = this._getNextVisibleItem(0) //at pos 0 there's the close button
223 if (firstItem.item) {
224 firstItem.item.focus()
225 this._currItemIdx = firstItem.index
226 }
227 EventHandler.trigger(this._element, EVENT_SHOWN)
228 }
229
230 this._queueCallback(transitionComplete, this._menuWrapper, isAnimated)
231 }
232
233 _hideElement() {
234 if (this._overlay) {
235 this._overlay.style.display = 'none'
236 }
237
238 this._element.style.display = 'none'
239 this._element.setAttribute('aria-hidden', true)
240 this._element.removeAttribute('aria-expanded')
241 //this._element.removeAttribute('aria-modal')
242 //this._element.removeAttribute('role')
243 this._isTransitioning = false
244 EventHandler.trigger(this._element, EVENT_HIDDEN)
245 }
246
247 _setOverlay() {
248 this._overlay = SelectorEngine.findOne(SELECTOR_OVERLAY, this._element)
249 if (this._isAnimated) {
250 this._overlay.classList.add(CLASS_NAME_FADE)
251 }
252 }
253
254 /**
255 * Moves focus to the next focusable element based on the DOM exploration direction
256 */
257 _focusNext() {
258 let nextIdx = this._currItemIdx + (this._isKeyShift ? -1 : 1)
259 if (nextIdx < 0) {
260 nextIdx = this._menuItems.length - 1
261 } else if (nextIdx >= this._menuItems.length) {
262 nextIdx = 0
263 }
264 const target = this._getNextVisibleItem(nextIdx, this._isKeyShift)
265 if (target.item) {
266 target.item.focus()
267 this._currItemIdx = target.index
268 }
269 }
270 /**
271 * Get the next focusable element from a starting point
272 * @param {int} start - the index of the array of the elements as starting point (included)
273 * @param {boolean} wayTop - the array search direction (true: bottom to top, false: top to bottom)
274 * @returns {Object} the item found and its index in the array
275 */
276 _getNextVisibleItem(start, wayTop) {
277 let found = null
278 let foundIdx = null
279
280 let i = start
281 let incr = wayTop ? -1 : 1
282 let firstCheck = false
283 while (!found && (i != start || !firstCheck)) {
284 if (i == start) {
285 firstCheck = true
286 }
287 if (!this._isElementHidden(this._menuItems[i])) {
288 found = this._menuItems[i]
289 foundIdx = i
290 }
291 i = i + incr
292 if (i < 0) {
293 i = this._menuItems.length - 1
294 } else if (i >= this._menuItems.length) {
295 i = 0
296 }
297 }
298
299 return {
300 item: found,
301 index: foundIdx,
302 }
303 }
304}
305
306/**
307 * ------------------------------------------------------------------------
308 * Data Api implementation
309 * ------------------------------------------------------------------------
310 */
311/*const navs = SelectorEngine.find(SELECTOR_NAVBAR)
312navs.forEach((nav) => {
313 NavBarCollapsible.getOrCreateInstance(nav)
314})*/
315
316EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
317 const target = getElementFromSelector(this)
318
319 if (['A', 'AREA'].includes(this.tagName)) {
320 event.preventDefault()
321 }
322
323 EventHandler.one(target, EVENT_SHOW, (showEvent) => {
324 if (showEvent.defaultPrevented) {
325 // only register focus restorer if modal will actually get shown
326 return
327 }
328
329 EventHandler.one(target, EVENT_HIDDEN, () => {
330 if (isVisible(this)) {
331 this.focus()
332 }
333 })
334 })
335
336 const data = NavBarCollapsible.getOrCreateInstance(target)
337
338 data.toggle(this)
339})
340
341export default NavBarCollapsible