1 | /**
|
---|
2 | * Zenscroll 4.0.2
|
---|
3 | * https://github.com/zengabor/zenscroll/
|
---|
4 | *
|
---|
5 | * Copyright 2015–2018 Gabor Lenard
|
---|
6 | *
|
---|
7 | * This is free and unencumbered software released into the public domain.
|
---|
8 | *
|
---|
9 | * Anyone is free to copy, modify, publish, use, compile, sell, or
|
---|
10 | * distribute this software, either in source code form or as a compiled
|
---|
11 | * binary, for any purpose, commercial or non-commercial, and by any
|
---|
12 | * means.
|
---|
13 | *
|
---|
14 | * In jurisdictions that recognize copyright laws, the author or authors
|
---|
15 | * of this software dedicate any and all copyright interest in the
|
---|
16 | * software to the public domain. We make this dedication for the benefit
|
---|
17 | * of the public at large and to the detriment of our heirs and
|
---|
18 | * successors. We intend this dedication to be an overt act of
|
---|
19 | * relinquishment in perpetuity of all present and future rights to this
|
---|
20 | * software under copyright law.
|
---|
21 | *
|
---|
22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
---|
23 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
---|
24 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
---|
25 | * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
---|
26 | * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
---|
27 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
---|
28 | * OTHER DEALINGS IN THE SOFTWARE.
|
---|
29 | *
|
---|
30 | * For more information, please refer to <http://unlicense.org>
|
---|
31 | *
|
---|
32 | */
|
---|
33 |
|
---|
34 | /*jshint devel:true, asi:true */
|
---|
35 |
|
---|
36 | /*global define, module */
|
---|
37 |
|
---|
38 |
|
---|
39 | (function (root, factory) {
|
---|
40 | if (typeof define === "function" && define.amd) {
|
---|
41 | define([], factory())
|
---|
42 | } else if (typeof module === "object" && module.exports) {
|
---|
43 | module.exports = factory()
|
---|
44 | } else {
|
---|
45 | (function install() {
|
---|
46 | // To make sure Zenscroll can be referenced from the header, before `body` is available
|
---|
47 | if (document && document.body) {
|
---|
48 | root.zenscroll = factory()
|
---|
49 | } else {
|
---|
50 | // retry 9ms later
|
---|
51 | setTimeout(install, 9)
|
---|
52 | }
|
---|
53 | })()
|
---|
54 | }
|
---|
55 | }(this, function () {
|
---|
56 | "use strict"
|
---|
57 |
|
---|
58 |
|
---|
59 | // Detect if the browser already supports native smooth scrolling (e.g., Firefox 36+ and Chrome 49+) and it is enabled:
|
---|
60 | var isNativeSmoothScrollEnabledOn = function (elem) {
|
---|
61 | return elem && "getComputedStyle" in window &&
|
---|
62 | window.getComputedStyle(elem)["scroll-behavior"] === "smooth"
|
---|
63 | }
|
---|
64 |
|
---|
65 |
|
---|
66 | // Exit if it’s not a browser environment:
|
---|
67 | if (typeof window === "undefined" || !("document" in window)) {
|
---|
68 | return {}
|
---|
69 | }
|
---|
70 |
|
---|
71 |
|
---|
72 | var makeScroller = function (container, defaultDuration, edgeOffset) {
|
---|
73 |
|
---|
74 | // Use defaults if not provided
|
---|
75 | defaultDuration = defaultDuration || 999 //ms
|
---|
76 | if (!edgeOffset && edgeOffset !== 0) {
|
---|
77 | // When scrolling, this amount of distance is kept from the edges of the container:
|
---|
78 | edgeOffset = 9 //px
|
---|
79 | }
|
---|
80 |
|
---|
81 | // Handling the life-cycle of the scroller
|
---|
82 | var scrollTimeoutId
|
---|
83 | var setScrollTimeoutId = function (newValue) {
|
---|
84 | scrollTimeoutId = newValue
|
---|
85 | }
|
---|
86 |
|
---|
87 | /**
|
---|
88 | * Stop the current smooth scroll operation immediately
|
---|
89 | */
|
---|
90 | var stopScroll = function () {
|
---|
91 | clearTimeout(scrollTimeoutId)
|
---|
92 | setScrollTimeoutId(0)
|
---|
93 | }
|
---|
94 |
|
---|
95 | var getTopWithEdgeOffset = function (elem) {
|
---|
96 | return Math.max(0, container.getTopOf(elem) - edgeOffset)
|
---|
97 | }
|
---|
98 |
|
---|
99 | /**
|
---|
100 | * Scrolls to a specific vertical position in the document.
|
---|
101 | *
|
---|
102 | * @param {targetY} The vertical position within the document.
|
---|
103 | * @param {duration} Optionally the duration of the scroll operation.
|
---|
104 | * If not provided the default duration is used.
|
---|
105 | * @param {onDone} An optional callback function to be invoked once the scroll finished.
|
---|
106 | */
|
---|
107 | var scrollToY = function (targetY, duration, onDone) {
|
---|
108 | stopScroll()
|
---|
109 | if (duration === 0 || (duration && duration < 0) || isNativeSmoothScrollEnabledOn(container.body)) {
|
---|
110 | container.toY(targetY)
|
---|
111 | if (onDone) {
|
---|
112 | onDone()
|
---|
113 | }
|
---|
114 | } else {
|
---|
115 | var startY = container.getY()
|
---|
116 | var distance = Math.max(0, targetY) - startY
|
---|
117 | var startTime = new Date().getTime()
|
---|
118 | duration = duration || Math.min(Math.abs(distance), defaultDuration);
|
---|
119 | (function loopScroll() {
|
---|
120 | setScrollTimeoutId(setTimeout(function () {
|
---|
121 | // Calculate percentage:
|
---|
122 | var p = Math.min(1, (new Date().getTime() - startTime) / duration)
|
---|
123 | // Calculate the absolute vertical position:
|
---|
124 | var y = Math.max(0, Math.floor(startY + distance*(p < 0.5 ? 2*p*p : p*(4 - p*2)-1)))
|
---|
125 | container.toY(y)
|
---|
126 | if (p < 1 && (container.getHeight() + y) < container.body.scrollHeight) {
|
---|
127 | loopScroll()
|
---|
128 | } else {
|
---|
129 | setTimeout(stopScroll, 99) // with cooldown time
|
---|
130 | if (onDone) {
|
---|
131 | onDone()
|
---|
132 | }
|
---|
133 | }
|
---|
134 | }, 9))
|
---|
135 | })()
|
---|
136 | }
|
---|
137 | }
|
---|
138 |
|
---|
139 | /**
|
---|
140 | * Scrolls to the top of a specific element.
|
---|
141 | *
|
---|
142 | * @param {elem} The element to scroll to.
|
---|
143 | * @param {duration} Optionally the duration of the scroll operation.
|
---|
144 | * @param {onDone} An optional callback function to be invoked once the scroll finished.
|
---|
145 | */
|
---|
146 | var scrollToElem = function (elem, duration, onDone) {
|
---|
147 | scrollToY(getTopWithEdgeOffset(elem), duration, onDone)
|
---|
148 | }
|
---|
149 |
|
---|
150 | /**
|
---|
151 | * Scrolls an element into view if necessary.
|
---|
152 | *
|
---|
153 | * @param {elem} The element.
|
---|
154 | * @param {duration} Optionally the duration of the scroll operation.
|
---|
155 | * @param {onDone} An optional callback function to be invoked once the scroll finished.
|
---|
156 | */
|
---|
157 | var scrollIntoView = function (elem, duration, onDone) {
|
---|
158 | var elemHeight = elem.getBoundingClientRect().height
|
---|
159 | var elemBottom = container.getTopOf(elem) + elemHeight
|
---|
160 | var containerHeight = container.getHeight()
|
---|
161 | var y = container.getY()
|
---|
162 | var containerBottom = y + containerHeight
|
---|
163 | if (getTopWithEdgeOffset(elem) < y || (elemHeight + edgeOffset) > containerHeight) {
|
---|
164 | // Element is clipped at top or is higher than screen.
|
---|
165 | scrollToElem(elem, duration, onDone)
|
---|
166 | } else if ((elemBottom + edgeOffset) > containerBottom) {
|
---|
167 | // Element is clipped at the bottom.
|
---|
168 | scrollToY(elemBottom - containerHeight + edgeOffset, duration, onDone)
|
---|
169 | } else if (onDone) {
|
---|
170 | onDone()
|
---|
171 | }
|
---|
172 | }
|
---|
173 |
|
---|
174 | /**
|
---|
175 | * Scrolls to the center of an element.
|
---|
176 | *
|
---|
177 | * @param {elem} The element.
|
---|
178 | * @param {duration} Optionally the duration of the scroll operation.
|
---|
179 | * @param {offset} Optionally the offset of the top of the element from the center of the screen.
|
---|
180 | * A value of 0 is ignored.
|
---|
181 | * @param {onDone} An optional callback function to be invoked once the scroll finished.
|
---|
182 | */
|
---|
183 | var scrollToCenterOf = function (elem, duration, offset, onDone) {
|
---|
184 | scrollToY(Math.max(0, container.getTopOf(elem) - container.getHeight()/2 + (offset || elem.getBoundingClientRect().height/2)), duration, onDone)
|
---|
185 | }
|
---|
186 |
|
---|
187 | /**
|
---|
188 | * Changes default settings for this scroller.
|
---|
189 | *
|
---|
190 | * @param {newDefaultDuration} Optionally a new value for default duration, used for each scroll method by default.
|
---|
191 | * Ignored if null or undefined.
|
---|
192 | * @param {newEdgeOffset} Optionally a new value for the edge offset, used by each scroll method by default. Ignored if null or undefined.
|
---|
193 | * @returns An object with the current values.
|
---|
194 | */
|
---|
195 | var setup = function (newDefaultDuration, newEdgeOffset) {
|
---|
196 | if (newDefaultDuration === 0 || newDefaultDuration) {
|
---|
197 | defaultDuration = newDefaultDuration
|
---|
198 | }
|
---|
199 | if (newEdgeOffset === 0 || newEdgeOffset) {
|
---|
200 | edgeOffset = newEdgeOffset
|
---|
201 | }
|
---|
202 | return {
|
---|
203 | defaultDuration: defaultDuration,
|
---|
204 | edgeOffset: edgeOffset
|
---|
205 | }
|
---|
206 | }
|
---|
207 |
|
---|
208 | return {
|
---|
209 | setup: setup,
|
---|
210 | to: scrollToElem,
|
---|
211 | toY: scrollToY,
|
---|
212 | intoView: scrollIntoView,
|
---|
213 | center: scrollToCenterOf,
|
---|
214 | stop: stopScroll,
|
---|
215 | moving: function () { return !!scrollTimeoutId },
|
---|
216 | getY: container.getY,
|
---|
217 | getTopOf: container.getTopOf
|
---|
218 | }
|
---|
219 |
|
---|
220 | }
|
---|
221 |
|
---|
222 |
|
---|
223 | var docElem = document.documentElement
|
---|
224 | var getDocY = function () { return window.scrollY || docElem.scrollTop }
|
---|
225 |
|
---|
226 | // Create a scroller for the document:
|
---|
227 | var zenscroll = makeScroller({
|
---|
228 | body: document.scrollingElement || document.body,
|
---|
229 | toY: function (y) { window.scrollTo(0, y) },
|
---|
230 | getY: getDocY,
|
---|
231 | getHeight: function () { return window.innerHeight || docElem.clientHeight },
|
---|
232 | getTopOf: function (elem) { return elem.getBoundingClientRect().top + getDocY() - docElem.offsetTop }
|
---|
233 | })
|
---|
234 |
|
---|
235 |
|
---|
236 | /**
|
---|
237 | * Creates a scroller from the provided container element (e.g., a DIV)
|
---|
238 | *
|
---|
239 | * @param {scrollContainer} The vertical position within the document.
|
---|
240 | * @param {defaultDuration} Optionally a value for default duration, used for each scroll method by default.
|
---|
241 | * Ignored if 0 or null or undefined.
|
---|
242 | * @param {edgeOffset} Optionally a value for the edge offset, used by each scroll method by default.
|
---|
243 | * Ignored if null or undefined.
|
---|
244 | * @returns A scroller object, similar to `zenscroll` but controlling the provided element.
|
---|
245 | */
|
---|
246 | zenscroll.createScroller = function (scrollContainer, defaultDuration, edgeOffset) {
|
---|
247 | return makeScroller({
|
---|
248 | body: scrollContainer,
|
---|
249 | toY: function (y) { scrollContainer.scrollTop = y },
|
---|
250 | getY: function () { return scrollContainer.scrollTop },
|
---|
251 | getHeight: function () { return Math.min(scrollContainer.clientHeight, window.innerHeight || docElem.clientHeight) },
|
---|
252 | getTopOf: function (elem) { return elem.offsetTop }
|
---|
253 | }, defaultDuration, edgeOffset)
|
---|
254 | }
|
---|
255 |
|
---|
256 |
|
---|
257 | // Automatic link-smoothing on achors
|
---|
258 | // Exclude IE8- or when native is enabled or Zenscroll auto- is disabled
|
---|
259 | if ("addEventListener" in window && !window.noZensmooth && !isNativeSmoothScrollEnabledOn(document.body)) {
|
---|
260 |
|
---|
261 | var isHistorySupported = "history" in window && "pushState" in history
|
---|
262 | var isScrollRestorationSupported = isHistorySupported && "scrollRestoration" in history
|
---|
263 |
|
---|
264 | // On first load & refresh make sure the browser restores the position first
|
---|
265 | if (isScrollRestorationSupported) {
|
---|
266 | history.scrollRestoration = "auto"
|
---|
267 | }
|
---|
268 |
|
---|
269 | window.addEventListener("load", function () {
|
---|
270 |
|
---|
271 | if (isScrollRestorationSupported) {
|
---|
272 | // Set it to manual
|
---|
273 | setTimeout(function () { history.scrollRestoration = "manual" }, 9)
|
---|
274 | window.addEventListener("popstate", function (event) {
|
---|
275 | if (event.state && "zenscrollY" in event.state) {
|
---|
276 | zenscroll.toY(event.state.zenscrollY)
|
---|
277 | }
|
---|
278 | }, false)
|
---|
279 | }
|
---|
280 |
|
---|
281 | // Add edge offset on first load if necessary
|
---|
282 | // This may not work on IE (or older computer?) as it requires more timeout, around 100 ms
|
---|
283 | if (window.location.hash) {
|
---|
284 | setTimeout(function () {
|
---|
285 | // Adjustment is only needed if there is an edge offset:
|
---|
286 | var edgeOffset = zenscroll.setup().edgeOffset
|
---|
287 | if (edgeOffset) {
|
---|
288 | var targetElem = document.getElementById(window.location.href.split("#")[1])
|
---|
289 | if (targetElem) {
|
---|
290 | var targetY = Math.max(0, zenscroll.getTopOf(targetElem) - edgeOffset)
|
---|
291 | var diff = zenscroll.getY() - targetY
|
---|
292 | // Only do the adjustment if the browser is very close to the element:
|
---|
293 | if (0 <= diff && diff < 9 ) {
|
---|
294 | window.scrollTo(0, targetY)
|
---|
295 | }
|
---|
296 | }
|
---|
297 | }
|
---|
298 | }, 9)
|
---|
299 | }
|
---|
300 |
|
---|
301 | }, false)
|
---|
302 |
|
---|
303 | // Handling clicks on anchors
|
---|
304 | var RE_noZensmooth = new RegExp("(^|\\s)noZensmooth(\\s|$)")
|
---|
305 | window.addEventListener("click", function (event) {
|
---|
306 | var anchor = event.target
|
---|
307 | while (anchor && anchor.tagName !== "A") {
|
---|
308 | anchor = anchor.parentNode
|
---|
309 | }
|
---|
310 | // Let the browser handle the click if it wasn't with the primary button, or with some modifier keys:
|
---|
311 | if (!anchor || event.which !== 1 || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) {
|
---|
312 | return
|
---|
313 | }
|
---|
314 | // Save the current scrolling position so it can be used for scroll restoration:
|
---|
315 | if (isScrollRestorationSupported) {
|
---|
316 | var historyState = history.state && typeof history.state === "object" ? history.state : {}
|
---|
317 | historyState.zenscrollY = zenscroll.getY()
|
---|
318 | try {
|
---|
319 | history.replaceState(historyState, "")
|
---|
320 | } catch (e) {
|
---|
321 | // Avoid the Chrome Security exception on file protocol, e.g., file://index.html
|
---|
322 | }
|
---|
323 | }
|
---|
324 | // Find the referenced ID:
|
---|
325 | var href = anchor.getAttribute("href") || ""
|
---|
326 | if (href.indexOf("#") === 0 && !RE_noZensmooth.test(anchor.className)) {
|
---|
327 | var targetY = 0
|
---|
328 | var targetElem = document.getElementById(href.substring(1))
|
---|
329 | if (href !== "#") {
|
---|
330 | if (!targetElem) {
|
---|
331 | // Let the browser handle the click if the target ID is not found.
|
---|
332 | return
|
---|
333 | }
|
---|
334 | targetY = zenscroll.getTopOf(targetElem)
|
---|
335 | }
|
---|
336 | event.preventDefault()
|
---|
337 | // By default trigger the browser's `hashchange` event...
|
---|
338 | var onDone = function () { window.location = href }
|
---|
339 | // ...unless there is an edge offset specified
|
---|
340 | var edgeOffset = zenscroll.setup().edgeOffset
|
---|
341 | if (edgeOffset) {
|
---|
342 | targetY = Math.max(0, targetY - edgeOffset)
|
---|
343 | if (isHistorySupported) {
|
---|
344 | onDone = function () { history.pushState({}, "", href) }
|
---|
345 | }
|
---|
346 | }
|
---|
347 | zenscroll.toY(targetY, null, onDone)
|
---|
348 | }
|
---|
349 | }, false)
|
---|
350 |
|
---|
351 | }
|
---|
352 |
|
---|
353 |
|
---|
354 | return zenscroll
|
---|
355 |
|
---|
356 |
|
---|
357 | }));
|
---|