UNPKG

7.68 kBJavaScriptView Raw
1import BaseComponent from 'bootstrap/js/src/base-component.js'
2
3import EventHandler from 'bootstrap/js/src/dom/event-handler'
4import SelectorEngine from 'bootstrap/js/src/dom/selector-engine'
5import Manipulator from 'bootstrap/js/src/dom/manipulator'
6
7import onDocumentScroll from './util/on-document-scroll'
8import NavBarCollapsible from './navbar-collapsible'
9
10import { documentScrollTo } from './util/tween'
11
12const NAME = 'navscroll'
13//const DATA_KEY = 'bs.navscroll'
14//const EVENT_KEY = `.${DATA_KEY}`
15//const DATA_API_KEY = '.data-api'
16
17//const EVENT_SCROLL = `scroll${EVENT_KEY}`
18
19const CLASS_NAME_ACTIVE = 'active'
20
21const SELECTOR_NAVSCROLL = '[data-bs-navscroll]' //'.it-navscroll-wrapper'
22const SELECTOR_LIST = 'ul.link-list'
23//const SELECTOR_ITEM = '.nav-item'
24const SELECTOR_LINK_CONTAINER = 'li.nav-link, li.nav-item'
25const SELECTOR_LINK = 'a.nav-link'
26const SELECTOR_LINK_ACTIVE = `${SELECTOR_LINK}.${CLASS_NAME_ACTIVE}`
27const SELECTOR_LINK_CLICKABLE = `${SELECTOR_LINK}[href^="#"]`
28const SELECTOR_CONTAINER = '.it-page-sections-container'
29const SELECTOR_PAGE_SECTION = '.it-page-section'
30const SELECTOR_TOGGLER = '.custom-navbar-toggler'
31const SELECTOR_TOGGLER_ICON = '.it-list'
32const SELECTOR_COLLAPSIBLE = '.navbar-collapsable'
33const SELECTOR_PROGRESS_BAR = '.it-navscroll-progressbar'
34
35const Default = {
36 scrollPadding: 10,
37 duration: 800,
38 easing: 'easeInOutSine',
39}
40class NavScroll extends BaseComponent {
41 constructor(element, config) {
42 super(element)
43
44 this._config = this._getConfig(config)
45
46 this._togglerElement = SelectorEngine.findOne(SELECTOR_TOGGLER, this._element)
47 this._sectionContainer = SelectorEngine.findOne(SELECTOR_CONTAINER)
48 this._collapsible = this._getCollapsible()
49 this._isCollapseOpened = false
50 this._callbackQueue = []
51 this._scrollCb = null
52
53 this._bindEvents()
54 }
55 // Getters
56
57 static get NAME() {
58 return NAME
59 }
60
61 // Public
62 setScrollPadding(scrollPadding) {
63 this._config.scrollPadding = scrollPadding
64 }
65
66 dispose() {
67 //EventHandler.off(window, EVENT_SCROLL, this._onScroll)
68 if (this._scrollCb) {
69 this._scrollCb.dispose()
70 }
71
72 super.dispose()
73 }
74
75 // Private
76 _getConfig(config) {
77 config = {
78 ...Default,
79 ...Manipulator.getDataAttributes(this._element),
80 ...(typeof config === 'object' ? config : {}),
81 }
82 return config
83 }
84
85 _bindEvents() {
86 //EventHandler.on(window, EVENT_SCROLL, this._onScroll)
87
88 this._scrollCb = onDocumentScroll(() => this._onScroll())
89
90 if (this._collapsible) {
91 EventHandler.on(this._collapsible._element, 'shown.bs.navbarcollapsible', () => this._onCollapseOpened())
92 EventHandler.on(this._collapsible._element, 'hidden.bs.navbarcollapsible', () => this._onCollapseClosed())
93 }
94
95 SelectorEngine.find(SELECTOR_LINK_CLICKABLE, this._element).forEach((link) => {
96 link.addEventListener('click', (event) => {
97 event.preventDefault()
98 const scrollHash = () => this._scrollToHash(link.hash)
99 if (this._isCollapseOpened) {
100 this._callbackQueue.push(scrollHash)
101 this._collapsible.hide()
102 } else {
103 scrollHash()
104 }
105 })
106 })
107
108 EventHandler.on(window, 'load', () => {
109 //if page is already scrolled
110 setTimeout(() => this._onScroll(), 150)
111 })
112 }
113
114 _onCollapseOpened() {
115 this._isCollapseOpened = true
116 }
117 _onCollapseClosed() {
118 while (this._callbackQueue.length > 0) {
119 const cb = this._callbackQueue.shift()
120 if (typeof cb === 'function') {
121 cb()
122 }
123 }
124 this._isCollapseOpened = false
125 }
126
127 _getParentLinks(element) {
128 const parents = []
129 let parentContainer = element.closest(SELECTOR_LIST)
130 let parentContainerPrev = null
131 let exit = false
132 while (parentContainer && !exit) {
133 const parentLinkContainer = parentContainer.closest(SELECTOR_LINK_CONTAINER)
134 if (parentLinkContainer) {
135 const link = parentLinkContainer.querySelector(SELECTOR_LINK)
136 if (link) {
137 parents.push(link)
138 }
139 }
140 parentContainerPrev = parentContainer
141 parentContainer = (parentContainer.parentElement || parentContainer).closest(SELECTOR_LIST) //avoid self select of closest
142 if (parentContainer === parentContainerPrev) {
143 exit = true
144 }
145 }
146 return parents
147 }
148
149 _decorateToggler(text) {
150 if (this._togglerElement) {
151 const icon = SelectorEngine.findOne(SELECTOR_TOGGLER_ICON, this._togglerElement)
152 this._togglerElement.innerText = ''
153 this._togglerElement.textContent = ''
154 this._togglerElement.append(icon)
155 this._togglerElement.append(text)
156 }
157 }
158
159 _scrollToHash(hash) {
160 const target = SelectorEngine.findOne(hash, this._sectionContainer)
161 if (target) {
162 documentScrollTo(target.offsetTop - this._getScrollPadding(), {
163 duration: this._config.duration,
164 easing: this._config.easing,
165 /*complete: () => {
166 },*/
167 })
168
169 if (history.pushState) {
170 history.pushState(null, null, hash)
171 } else {
172 location.hash = hash
173 }
174 }
175 }
176
177 _updateProgress(content) {
178 const progressBars = SelectorEngine.find(SELECTOR_PROGRESS_BAR)
179 if (progressBars) {
180 const offset = Math.abs(content.getBoundingClientRect().top)
181 const height = content.getBoundingClientRect().height
182 const scrollAmount = (offset / height) * 100
183 const scrollValue = Math.min(100, Math.max(0, scrollAmount))
184
185 progressBars.forEach((progressBar) => {
186 if (content.getBoundingClientRect().top <= 0) {
187 progressBar.style.width = scrollValue + '%'
188 progressBar.setAttribute('aria-valuenow', scrollValue)
189 } else {
190 progressBar.style.width = 0 + '%'
191 progressBar.setAttribute('aria-valuenow', 0)
192 }
193 })
194 }
195 }
196
197 _onScroll() {
198 const sectionsContainerTop = this._sectionContainer ? this._sectionContainer.offsetTop : 0
199 const scrollDistance = document.scrollingElement.scrollTop - sectionsContainerTop
200
201 const navItems = SelectorEngine.find(SELECTOR_LINK, this._element)
202
203 const scrollPadding = this._getScrollPadding()
204
205 SelectorEngine.find(SELECTOR_PAGE_SECTION).forEach((pageSec, idx) => {
206 if (pageSec.offsetTop - sectionsContainerTop <= scrollDistance + scrollPadding) {
207 SelectorEngine.find(SELECTOR_LINK_ACTIVE, this._element).forEach((link) => {
208 link.classList.remove(CLASS_NAME_ACTIVE)
209 })
210 if (idx < navItems.length) {
211 const currActive = navItems[idx]
212 this._getParentLinks(currActive).forEach((parentLink) => {
213 parentLink.classList.add(CLASS_NAME_ACTIVE)
214 })
215 currActive.classList.add(CLASS_NAME_ACTIVE)
216 this._decorateToggler(currActive.innerText)
217 }
218 }
219 })
220 this._updateProgress(this._sectionContainer)
221 }
222
223 _getCollapsible() {
224 const coll = SelectorEngine.findOne(SELECTOR_COLLAPSIBLE, this._element)
225 if (coll) {
226 return NavBarCollapsible.getOrCreateInstance(coll)
227 }
228 return null
229 }
230
231 _getScrollPadding() {
232 if (typeof this._config.scrollPadding === 'function') {
233 return this._config.scrollPadding()
234 }
235 return this._config.scrollPadding
236 }
237}
238
239/**
240 * ------------------------------------------------------------------------
241 * Data Api implementation
242 * ------------------------------------------------------------------------
243 */
244
245const dataApiCb = onDocumentScroll(() => {
246 const navs = SelectorEngine.find(SELECTOR_NAVSCROLL)
247 navs.forEach((nav) => {
248 NavScroll.getOrCreateInstance(nav)
249 })
250 dataApiCb.dispose()
251})
252
253export default NavScroll