From fdada4ccea747a89ac9a4d990215ccddd463596c Mon Sep 17 00:00:00 2001 From: Gabor Lenard Date: Sat, 18 Mar 2017 07:33:18 +0100 Subject: [PATCH] v4.0.0 --- README.md | 138 +++++++++++++++++----- package.json | 4 +- zenscroll-min.js | 2 +- zenscroll.js | 299 +++++++++++++++++++++++++++-------------------- 4 files changed, 284 insertions(+), 159 deletions(-) diff --git a/README.md b/README.md index 95b75ae..29e058d 100644 --- a/README.md +++ b/README.md @@ -12,27 +12,26 @@ Smooth animated scrolling. Move elements into view, or scroll to any vertical position. -1.2 kilobyte of vanilla JavaScript. No dependencies. +1.4 kilobyte of vanilla JavaScript. No dependencies. ## About -Zenscroll is a vanilla JavaScript module that enables animated vertical scrolling to any element or any position within your document or within a DIV or other scrollable container. +Zenscroll is a vanilla JavaScript library that enables animated vertical scrolling to an element or position within your document or within a scrollable element (DIV, etc.). It can also automatically smooth all scrolling within the same page. Features: -- Smooth animated scrolling. -- Automatic smooth-scolling on links within the same page. -- Works even if the Back and Forward buttons are used (on compatible browsers). +- Smooth animated vertical scrolling. +- Automatic smooth-scrolling on links within the same page, including back and forward navigation (Chrome, Firefox, Opera, Safari Technology Preview). - Scroll to the top of a specific element. - Scrolling an element into view, making sure both top & bottom are visible, if possible. - Scroll to an element and center it on the screen. -- Customize the duration of the individual scroll operations. -- If you provide a callback function it will be executed when the scrolling is done. - Specify the spacing between the element and the edge of the screen (e.g., for fixed navigation bars and footers). -- Uses the browser’s built-in smooth-behavior if it’s enabled. -- Just 1.2 kilobyte minimized & gzipped. +- Customize the duration of the individual scroll operations. +- Callback when the scrolling is done. +- Can use new browser’s built-in smooth-behavior if it’s enabled. +- Just 1.4 kilobyte minimized & gzipped. - No dependencies. Full support tested and works under: @@ -68,12 +67,15 @@ Limited support (programmatic animated scroll in document) tested and works unde ```` -You can also use npm to get Zenscroll: +Zenscroll will also work if you reference it in the `` but it is not recommended for performance reasons. Important: You can only call Zenscroll when `document.body` is already available, so don’t try to call Zenscroll functions from the ``. + +You can also [get Zenscroll via npm](https://www.npmjs.com/package/zenscroll): ```` npm install zenscroll ```` + ### Enabling native smooth-scrolling in the browser If you want to leverage the native smooth-scrolling by the browser (currently available in Firefox 36+ and Chrome 49+) then set the [`scroll-behavior` CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior) to `smooth` on the body and on the elements you want to scroll. E.g., @@ -82,11 +84,14 @@ If you want to leverage the native smooth-scrolling by the browser (currently av body, .smooth-container { scroll-behavior: smooth } ```` -If this is set and the browser supports it, Zenscroll will use the browser’s built-in support for smooth-scrolling. However, note that if you use the native smooth-scrolling then you loose the finer control options that Zenscroll offers: the speed settings of the animation, and the edge offset for links within the page. Only set this CSS property on the `body` or on the elements if you don’t need this level of control. +If this is set, Zenscroll will use built-in smooth-scrolling in capable browsers while still animating the scroll in older browsers. However, note that if you use the native smooth-scrolling then you loose the finer control options that Zenscroll offers: the speed settings of the animation, and the edge offset for links within the page. Only set this CSS property on the `body` or on the elements if you don’t need this level of control. + ### Disabling automatic smoothing on local links -If you want to use Zenscroll programmatically but you don’t need the automatic smoothing on local links then set `window.noZensmooth` to a non-falsy value. In this case the event handler for automatic smoothing is not installed but you can still use everything, like `zenscroll.intoView()`, etc. It’s important to set this value before Zenscroll is executed, otherwise the handler will be installed. So make sure that setting the variable comes before the loading of the script. For example: +If you want to use Zenscroll programmatically but you don’t need the automatic smoothing on local links then set `window.noZensmooth` to a non-falsy value. In this case the event handler for automatic smoothing is not installed but you can still use everything, like `zenscroll.intoView()`, etc. + +It’s important to set this value before Zenscroll is executed, otherwise the handlers will be installed. So make sure the setting comes before the loading of the script. For example: ````html ... @@ -95,26 +100,62 @@ If you want to use Zenscroll programmatically but you don’t need the automatic ```` -(I consider this a rare scenario that’s why I keep the default behavior of installing the event handler.) +(I consider the disabling of the automatic smooth-scrolling a rather rare scenario that’s why I install the event handlers by default.) ## How to use -### 1. Smooth scroll within your page +### 1. Automatic smooth-scroll to links within the page + +If Zenscroll is included in your page it will automatically animate the vertical scrolling to anchors on the same page. No further setting is required. (Note that it can be disabled, see [1.5](#1.5.disabletheautomaticsmooth-scrolling).) + +Automatic smooth-scrolling works also with content you dynamically load via AJAX, as Zenscroll uses a generic click handler. Since the automatic smooth-scrolling is implemented a progressive enhancement, internal links work in older browsers as well. + +#### 1.1. Automatic edge offset adjustment on load + +Zenscroll automatically adds the configured edge offset when the page is loaded with a hash suffix. For example, if you navigate to `"http://yoursite.com/#about"` then the scroll position will be not cut sharply at the top edge of the element with `id="above"`. Instead, Zenscroll automatically adds the configured edge offset (which is 9 pixels by default, unless you [changed it](#9.changesettings)). + +No automatic adjustment happens in the following cases: + +- If automatic smooth-scroll is disabled via `noZensmooth` or the native smooth-scrolling (see [1.5](#1.5.disabletheautomaticsmooth-scrolling)) +- If edge offset was set to zero (e.g., `zenscroll.setup(null, 0)`). + +#### 1.2. Limited support for smooth back & forward navigation + +The scroll is also animated when the browser’s back and forward buttons or the relevant key combinations are used (or even if the navigation is invoked from JavaScript). Note that although Zenscroll remembers the vertical scroll position, it cannot calculate changes caused by browser window resizing or collapsing/expanding elements, etc. + +This functionality requires browser support for `history.scrollRestoration` which is available for example in Chrome 46+, Firefox 46+, and Safari Technology Preview. + +#### 1.3. Limited support for the `hashchange` event and the CSS pseudo `:target` + +The automatic smooth-scroll on local links can also trigger the standard `hashchange` event and the CSS pseudo-class `:target` but only if you set the edge offset to 0. I had to introduce this limitation because otherwise the scrolling isn’t smooth. + +So if you need `hashchange` or `:target` then make sure you execute `zenscroll.setup(null, 0)`. + +#### 1.4. No support for automatic scroll to elements inside scrollable elements + +Zenscroll wants to be a lightweight solution for animated vertical scrolling, that works in most browsers. The automatic link-scrolling is even more focused on anchors that are directly inside the page body. Unfortunately it won’t scroll accurately to elements that are inside other scrollable elements (e.g., `overflow: auto; max-height: 100px`). Don’t expect Zenscroll to re-implement the full functionality of the browser, with the recursive logic of traversing the nested scrollable elements. + +So, how to deal with a situation like this? Try one of the following methods: + +- Exclude the problematic links from the automatic smooth-scrolling (see [1.6.](#1.6.excludealinkfromtheautomaticsmooth-scrolling)). +- Or implement the special logic programatically (see [7\. Scroll inside a scrollable DIV](#7.scrollinsideascrollablediv)). +- Or don’t use Zenscroll at all. Instead, rely on the browser’s built-in scrolling, and enable the native smooth-scrolling (via `body { scroll-behavior: smooth }`) which works in new browsers. -If Zenscroll is included in your page it will automatically animate the scrolling to anchors on the same page. +#### 1.5. Disable the automatic smooth-scrolling -However, automatic smooth scrolling within the same page is not enabled in these two cases: +The automatic smooth-scrolling is completely disabled in the following cases: 1. If you set `window.noZensmooth` to a non-falsy value (see [above](#disablingautomaticsmoothingonlocallinks)). -2. If the `scroll-behavior` CSS property is set to `smooth` on the `body` (see [above](#enablingnativesmooth-scrollinginthebrowser)). In this case the browser is already smooth-scrolling within the same page. +2. In new browsers if the `scroll-behavior` CSS property is set to `smooth` on the `body` (see [above](#enablingnativesmooth-scrollinginthebrowser)). In this case Zenscroll will only enable automatic smooth-scrolling in browsers which don’t support this feature yet (e.g., Internet Explorer). -If you want only some of the links to be excluded from the automatic smoothing then start with the path of the page. E.g., instead of writing `` use ``. - -The scroll is also animated when the Back and Forward buttons are used. (Note that it remembers the vertical scroll position but it doesn’t calculate changes caused by browser window resizing or collapsing/expanding elements, etc.) This functionality requires browser support for `history.scrollRestoration` which is available in Chrome 46+ and Firefox 46+. WebKit already has a [ticket](https://bugs.webkit.org/show_bug.cgi?id=147782) for it, in the meantime you can use a polyfill for Safari, like [scroll-restoration-polyfill](https://github.com/bfred-it/scroll-restoration-polyfill). +#### 1.6. Exclude a link from the automatic smooth-scrolling -Automatic smooth-scrolling works also with content you dynamically load via AJAX, as Zenscroll uses a generic click handler. Since the automatic smooth-scrolling is implemented a progressive enhancement, internal links work in older browsers as well. +If you want only some of the links to be excluded from the automatic smoothing then do one of the following: + +1. Add the class `noZensmooth` to the anchor element, for example `` +1. Alternatively, start with the path of the page. E.g., instead of writing `` use `` or ``. ### 2. Scroll to the top of an element @@ -124,7 +165,7 @@ var about = document.getElementById("about") zenscroll.to(about) ```` -Note that Zenscroll intentionally leaves a few pixels (by default 9px) from the edges of the screen or scolling container. If you have a fixed navigation bar or footer bar then you probably need more than that. Or you may want to set it to zero. You can globally override the default value by calling `zenscroll.setup()` (see below), or by providing the `edgeOffset` parameter when you create a scroller for a DIV, e.g., `zenscroll.createScroller(myDiv, null, 0)` +Note that Zenscroll intentionally leaves a few pixels (by default 9px) from the edges of the screen or scrolling container. If you have a fixed navigation bar or footer bar then you probably need more than that. Or you may want to set it to zero. You can globally override the default value by calling `zenscroll.setup()` (see [below](#9.changesettings)), or by providing the `edgeOffset` parameter when you create a scroller for a DIV, e.g., `zenscroll.createScroller(myDiv, null, 20)` ### 3. Scroll to a specific vertical position @@ -136,7 +177,7 @@ zenscroll.toY(50) ### 4. Scroll an element into view -If the element is already fully visible then no scroll is performed. Otherwise Zenscroll will try to make both top & bottom of element visible, if possible. If the element is higher than the visible viewport then it will simply scroll to the top of the element. +If the element is already fully visible, with the edge offset at the top and bottom, then no scroll is performed. Otherwise Zenscroll will try to make both top & bottom of element visible, if possible. If the element is higher than the visible viewport then it will simply scroll to the top of the element, including the edge offset. ````js zenscroll.intoView(image1) @@ -161,7 +202,7 @@ zenscroll.center(image2) If you want you can also define an offset. The top of the element will be upwards from the center of the screen by this amount of pixels. (By default offset is the half of the element’s height.) ````js -var duration = 500 // miliseconds +var duration = 500 // milliseconds var offset = 200 // pixels zenscroll.center(image2, duration, offset) ```` @@ -171,7 +212,7 @@ Note that a zero value for offset is ignored. You can work around this by using ### 6. Set the duration of the scroll -The default duration is 999 which is ~1 second. The duration is automatically reduced for elements that are very close. You can specifically set the duration for each scroll function via an optional second parameter. (Note that a value of zero for duration is ignored.) +The default duration is 999 which is ~1 second. The duration is automatically reduced for elements that are very close. You can specifically set the duration for each scroll function via an optional second parameter. If you pass a value of zero then the scroll happends immediately, without smoothing. Examples: @@ -187,10 +228,14 @@ zenscroll.to(about, 500) // 500ms == half a second zenscroll.center(image2, 2000) // 2 seconds ```` +````js +zenscroll.to(about, 0) // 0 milliseconds == no smoothing +```` + ### 7. Scroll inside a scrollable DIV -Anything you can do within the document you can also do inside a scrollable element. You just need to instantiate a new scoller for that element. I will also fall back by default to the native browser smooth-scrolling if available (which can be overridden via `setup()`). +Anything you can do within the document you can also do inside a scrollable DIV or other element. You just need to instantiate a new scroller for that element. I will also fall back by default to the native browser smooth-scrolling if available (which can be overridden via `setup()`). **Important:** the container DIV must have its `position` CSS property set to `relative`, `absolute` or `fixed`. If you want to keep it in the normal document flow, just assign `position: relative` to it via a class or a `style` attribute, like in the example below: @@ -218,7 +263,7 @@ Anything you can do within the document you can also do inside a scrollable elem Obviously you can use all other scroll functions and parameters with the scrollable container. Two more examples: ````js -myScroller.toY(35) +myScroller.toY(500) ```` ````js @@ -230,7 +275,9 @@ myScroller.intoView(target) You can provide a callback function to all four scroll functions, which is executed when the scroll operation is finished. For example, you change some UI elements but first you want to make sure that the relevant elements are visible. -If you look at the code examples above under the previous point, [Scroll inside a scrollable DIV](#7.scrollinsideascrollablediv) they are actually implemented like this: +Note that the callback is immediately invoked if the native scroll-smoothing is enabled (see [above](#enablingnativesmooth-scrollinginthebrowser)). + +If you look at the source code of the examples under [Scroll inside a scrollable DIV](#7.scrollinsideascrollablediv) they are actually implemented like this: ````js // Last line of example 1: @@ -275,19 +322,28 @@ myScroller.setup(500, 10) If you don’t want to change a value just omit the parameter or pass `null`. For example, the line below sets the default duration, while leaving other settings unchanged: ````js -zenscroll.setup(777) +zenscroll.setup(777) // only updates defaultDuration to 777 ```` Sets the the spacing between the edge of the screen (or a DIV) and the target element you are scrolling to, while leaving the default duration unchanged: ````js -zenscroll.setup(null, 42) +zenscroll.setup(null, 42) // only updates edgeOffset to 42 ```` +The function always returns the current values in an object, so even if no parameters are passed you can obtain the current settings: + +````js +var currentSettings = zenscroll.setup() +var dd = currentSettings.defaultDuration +var eo = currentSettings.edgeOffset +```` + + -### 10. Controlling the smooth operation +### 10. Additional functions -To check whether a scoll is being performed right now: +To check whether a scroll is being performed right now: ````js var isScrolling = zenscroll.moving() @@ -299,6 +355,24 @@ To stop the current scrolling: zenscroll.stop() ```` +To get the current vertical scrolling position: + +````js +var bodyY = zenscroll.getY() + +var myDivY = myDivScroller.getY() +```` + +To get the top position of an element within the body or a scroller: + +````js +var myElemY = zenscroll.getTopOf(myElem) + +var relativeTopOfElem = myDivScroller.getTopOf(anElemInMyDiv) +```` + +Impotant: the returned value is without edge offset, and without recurcursively calculating nested scrollable containers. + ## License diff --git a/package.json b/package.json index bea0c53..8bbab59 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zenscroll", - "version": "3.3.0", - "description": "A module to smooth-scroll web pages and containers (DIVs)", + "version": "4.0.0", + "description": "A module to smooth-scroll web pages and scrollable elements (like DIVs)", "main": "zenscroll.js", "files": ["zenscroll.js", "zenscroll-min.js"], "homepage": "https://zengabor.github.io/zenscroll/", diff --git a/zenscroll-min.js b/zenscroll-min.js index 6f4895a..0c4aa20 100644 --- a/zenscroll-min.js +++ b/zenscroll-min.js @@ -1 +1 @@ -!function(t,e){"function"==typeof define&&define.amd?define([],e()):"object"==typeof module&&module.exports?module.exports=e():t.zenscroll=e()}(this,function(){"use strict";var t=function(t){return"getComputedStyle"in window&&"smooth"===window.getComputedStyle(t)["scroll-behavior"]};if("undefined"==typeof window||!("document"in window))return{};var e=function(e,n,o){n=n||999,o||0===o||(o=9);var i,r=function(t){i=t},c=document.documentElement,u=function(){return e?e.scrollTop:window.scrollY||c.scrollTop},l=function(){return e?Math.min(e.offsetHeight,window.innerHeight):window.innerHeight||c.clientHeight},a=function(t){return e?t.offsetTop:t.getBoundingClientRect().top+u()-c.offsetTop},s=function(){clearTimeout(i),r(0)},f=function(o,i,a){if(s(),t(e?e:document.body))(e||window).scrollTo(0,o),a&&a();else{var f=u(),d=Math.max(o,0)-f;i=i||Math.min(Math.abs(d),n);var h=(new Date).getTime();!function t(){r(setTimeout(function(){var n=Math.min(((new Date).getTime()-h)/i,1),o=Math.max(Math.floor(f+d*(n<.5?2*n*n:n*(4-2*n)-1)),0);e?e.scrollTop=o:window.scrollTo(0,o),n<1&&l()+o<(e||c).scrollHeight?t():(setTimeout(s,99),a&&a())},9))}()}},d=function(t,e,n){var i=a(t)-o;return f(i,e,n),i},h=function(t,e,n){var i=t.getBoundingClientRect().height,r=a(t),c=r+i,s=l(),h=u(),w=h+s;r-os?d(t,e,n):c+o>w?f(c-s+o,e,n):n&&n()},w=function(t,e,n,o){f(Math.max(a(t)-l()/2+(n||t.getBoundingClientRect().height/2),0),e,o)},m=function(t,e){t&&(n=t),(0===e||e)&&(o=e)};return{setup:m,to:d,toY:f,intoView:h,center:w,stop:s,moving:function(){return!!i},getY:u}},n=e();if("addEventListener"in window&&!t(document.body)&&!window.noZensmooth){"scrollRestoration"in history&&(history.scrollRestoration="manual",window.addEventListener("popstate",function(t){t.state&&t.state.scrollY&&n.toY(t.state.scrollY)},!1));var o=function(t,e){try{history.replaceState({scrollY:n.getY()},""),history.pushState({scrollY:e},"",t)}catch(t){}};window.addEventListener("click",function(t){for(var e=t.target;e&&"A"!==e.tagName;)e=e.parentNode;if(!(!e||1!==t.which||t.shiftKey||t.metaKey||t.ctrlKey||t.altKey)){var i=e.getAttribute("href")||"";if(0===i.indexOf("#"))if("#"===i)t.preventDefault(),n.toY(0),o(window.location.href.split("#")[0],0);else{var r=e.hash.substring(1),c=document.getElementById(r);c&&(t.preventDefault(),o("#"+r,n.to(c)))}}},!1)}return{createScroller:e,setup:n.setup,to:n.to,toY:n.toY,intoView:n.intoView,center:n.center,stop:n.stop,moving:n.moving}}); \ No newline at end of file +!function(t,e){"function"==typeof define&&define.amd?define([],e()):"object"==typeof module&&module.exports?module.exports=e():function n(){document&&document.body?t.zenscroll=e():setTimeout(n,9)}()}(this,function(){"use strict";var t=function(t){return"getComputedStyle"in window&&"smooth"===window.getComputedStyle(t)["scroll-behavior"]};if("undefined"==typeof window||!("document"in window))return{};var e=function(e,n,o){n=n||999,o||0===o||(o=9);var i,r=function(t){i=t},u=function(){clearTimeout(i),r(0)},c=function(t){return Math.max(0,e.getTopOf(t)-o)},f=function(o,i,c){if(u(),0===i||i&&i<0||t(e.body))e.toY(o),c&&c();else{var f=e.getY(),a=Math.max(0,o)-f,l=(new Date).getTime();i=i||Math.min(Math.abs(a),n),function t(){r(setTimeout(function(){var n=Math.min(1,((new Date).getTime()-l)/i),o=Math.max(0,Math.floor(f+a*(n<.5?2*n*n:n*(4-2*n)-1)));e.toY(o),n<1&&e.getHeight()+ol?a(t,n,i):u+o>d?f(u-l+o,n,i):i&&i()},s=function(t,n,o,i){f(Math.max(0,e.getTopOf(t)-e.getHeight()/2+(o||t.getBoundingClientRect().height/2)),n,i)};return{setup:function(t,e){return(0===t||t)&&(n=t),(0===e||e)&&(o=e),{defaultDuration:n,edgeOffset:o}},to:a,toY:f,intoView:l,center:s,stop:u,moving:function(){return!!i},getY:e.getY,getTopOf:e.getTopOf}},n=document.documentElement,o=function(){return window.scrollY||n.scrollTop},i=e({body:document.scrollingElement||document.body,toY:function(t){window.scrollTo(0,t)},getY:o,getHeight:function(){return window.innerHeight||n.clientHeight},getTopOf:function(t){return t.getBoundingClientRect().top+o()-n.offsetTop}});if(i.createScroller=function(t,o,i){return e({body:t,toY:function(e){t.scrollTop=e},getY:function(){return t.scrollTop},getHeight:function(){return Math.min(t.clientHeight,window.innerHeight||n.clientHeight)},getTopOf:function(t){return t.offsetTop}},o,i)},"addEventListener"in window&&!window.noZensmooth&&!t(document.body)){var r="scrollRestoration"in history;r&&(history.scrollRestoration="auto"),window.addEventListener("load",function(){r&&(setTimeout(function(){history.scrollRestoration="manual"},9),window.addEventListener("popstate",function(t){t.state&&"zenscrollY"in t.state&&i.toY(t.state.zenscrollY)},!1)),window.location.hash&&setTimeout(function(){var t=i.setup().edgeOffset;if(t){var e=document.getElementById(window.location.href.split("#")[1]);if(e){var n=Math.max(0,i.getTopOf(e)-t),o=i.getY()-n;0<=o&&o<9&&window.scrollTo(0,n)}}},9)},!1);var u=new RegExp("(^|\\s)noZensmooth(\\s|$)");window.addEventListener("click",function(t){for(var e=t.target;e&&"A"!==e.tagName;)e=e.parentNode;if(!(!e||1!==t.which||t.shiftKey||t.metaKey||t.ctrlKey||t.altKey)){if(r)try{history.replaceState({zenscrollY:i.getY()},"")}catch(t){}var n=e.getAttribute("href")||"";if(0===n.indexOf("#")&&!u.test(e.className)){var o=0,c=document.getElementById(n.substring(1));if("#"!==n){if(!c)return;o=i.getTopOf(c)}t.preventDefault();var f=function(){window.location=n},a=i.setup().edgeOffset;a&&(o=Math.max(0,o-a),f=function(){history.pushState(null,"",n)}),i.toY(o,null,f)}}},!1)}return i}); \ No newline at end of file diff --git a/zenscroll.js b/zenscroll.js index aaf7a46..12531ca 100644 --- a/zenscroll.js +++ b/zenscroll.js @@ -1,8 +1,8 @@ /** - * Zenscroll 3.3.0 + * Zenscroll 4.0.0 * https://github.com/zengabor/zenscroll/ * - * Copyright 2015–2016 Gabor Lenard + * Copyright 2015–2017 Gabor Lenard * * This is free and unencumbered software released into the public domain. * @@ -28,7 +28,7 @@ * OTHER DEALINGS IN THE SOFTWARE. * * For more information, please refer to - * + * */ /*jshint devel:true, asi:true */ @@ -42,99 +42,88 @@ } else if (typeof module === "object" && module.exports) { module.exports = factory() } else { - root.zenscroll = factory() + (function install() { + // To make sure Zenscroll can be referenced from the header, before `body` is available + if (document && document.body) { + root.zenscroll = factory() + } else { + // retry 9ms later + setTimeout(install, 9) + } + })() } }(this, function () { "use strict" - + + // Detect if the browser already supports native smooth scrolling (e.g., Firefox 36+ and Chrome 49+) and it is enabled: var isNativeSmoothScrollEnabledOn = function (elem) { return ("getComputedStyle" in window) && window.getComputedStyle(elem)["scroll-behavior"] === "smooth" } + // Exit if it’s not a browser environment: if (typeof window === "undefined" || !("document" in window)) { return {} } - var createScroller = function (scrollContainer, defaultDuration, edgeOffset) { + var makeScroller = function (container, defaultDuration, edgeOffset) { + + // Use defaults if not provided defaultDuration = defaultDuration || 999 //ms if (!edgeOffset && edgeOffset !== 0) { - // When scrolling, this amount of distance is kept from the edges of the scrollContainer: + // When scrolling, this amount of distance is kept from the edges of the container: edgeOffset = 9 //px } + // Handling the life-cycle of the scroller var scrollTimeoutId var setScrollTimeoutId = function (newValue) { scrollTimeoutId = newValue } - var docElem = document.documentElement - - var getScrollTop = function () { - if (scrollContainer) { - return scrollContainer.scrollTop - } else { - return window.scrollY || docElem.scrollTop - } - } - - var getViewHeight = function () { - if (scrollContainer) { - return Math.min(scrollContainer.offsetHeight, window.innerHeight) - } else { - return window.innerHeight || docElem.clientHeight - } - } - - var getRelativeTopOf = function (elem) { - if (scrollContainer) { - return elem.offsetTop - } else { - return elem.getBoundingClientRect().top + getScrollTop() - docElem.offsetTop - } - } /** - * Immediately stops the current smooth scroll operation + * Stop the current smooth scroll operation immediately */ var stopScroll = function () { clearTimeout(scrollTimeoutId) setScrollTimeoutId(0) } + var getTopWithEdgeOffset = function (elem) { + return Math.max(0, container.getTopOf(elem) - edgeOffset) + } + /** * Scrolls to a specific vertical position in the document. * - * @param {endY} The vertical position within the document. + * @param {targetY} The vertical position within the document. * @param {duration} Optionally the duration of the scroll operation. - * If 0 or not provided it is automatically calculated based on the - * distance and the default duration. - * @param {onDone} Callback function to be invoken once the scroll finishes. + * If not provided the default duration is used. + * @param {onDone} An optional callback function to be invoked once the scroll finished. */ - var scrollToY = function (endY, duration, onDone) { + var scrollToY = function (targetY, duration, onDone) { stopScroll() - if (isNativeSmoothScrollEnabledOn(scrollContainer ? scrollContainer : document.body)) { - (scrollContainer || window).scrollTo(0, endY) + if (duration === 0 || (duration && duration < 0) || isNativeSmoothScrollEnabledOn(container.body)) { + container.toY(targetY) if (onDone) { onDone() } } else { - var startY = getScrollTop() - var distance = Math.max(endY,0) - startY - duration = duration || Math.min(Math.abs(distance), defaultDuration) - var startTime = new Date().getTime(); + var startY = container.getY() + var distance = Math.max(0, targetY) - startY + var startTime = new Date().getTime() + duration = duration || Math.min(Math.abs(distance), defaultDuration); (function loopScroll() { setScrollTimeoutId(setTimeout(function () { - var p = Math.min((new Date().getTime() - startTime) / duration, 1) // percentage - var y = Math.max(Math.floor(startY + distance*(p < 0.5 ? 2*p*p : p*(4 - p*2)-1)), 0) - if (scrollContainer) { - scrollContainer.scrollTop = y - } else { - window.scrollTo(0, y) - } - if (p < 1 && (getViewHeight() + y) < (scrollContainer || docElem).scrollHeight) { + // Calculate percentage: + var p = Math.min(1, (new Date().getTime() - startTime) / duration) + // Calculate the absolute vertical position: + var y = Math.max(0, Math.floor(startY + distance*(p < 0.5 ? 2*p*p : p*(4 - p*2)-1))) + container.toY(y) + if (p < 1 && (container.getHeight() + y) < container.body.scrollHeight) { loopScroll() } else { setTimeout(stopScroll, 99) // with cooldown time @@ -150,16 +139,12 @@ /** * Scrolls to the top of a specific element. * - * @param {elem} The element. + * @param {elem} The element to scroll to. * @param {duration} Optionally the duration of the scroll operation. - * A value of 0 is ignored. - * @param {onDone} Callback function to be invoken once the scroll finishes. - * @returns {endY} The new vertical scoll position that will be valid once the scroll finishes. + * @param {onDone} An optional callback function to be invoked once the scroll finished. */ var scrollToElem = function (elem, duration, onDone) { - var endY = getRelativeTopOf(elem) - edgeOffset - scrollToY(endY, duration, onDone) - return endY + scrollToY(getTopWithEdgeOffset(elem), duration, onDone) } /** @@ -167,17 +152,15 @@ * * @param {elem} The element. * @param {duration} Optionally the duration of the scroll operation. - * A value of 0 is ignored. - * @param {onDone} Callback function to be invoken once the scroll finishes. + * @param {onDone} An optional callback function to be invoked once the scroll finished. */ var scrollIntoView = function (elem, duration, onDone) { var elemHeight = elem.getBoundingClientRect().height - var elemTop = getRelativeTopOf(elem) - var elemBottom = elemTop + elemHeight - var containerHeight = getViewHeight() - var containerTop = getScrollTop() - var containerBottom = containerTop + containerHeight - if ((elemTop - edgeOffset) < containerTop || (elemHeight + edgeOffset) > containerHeight) { + var elemBottom = container.getTopOf(elem) + elemHeight + var containerHeight = container.getHeight() + var y = container.getY() + var containerBottom = y + containerHeight + if (getTopWithEdgeOffset(elem) < y || (elemHeight + edgeOffset) > containerHeight) { // Element is clipped at top or is higher than screen. scrollToElem(elem, duration, onDone) } else if ((elemBottom + edgeOffset) > containerBottom) { @@ -194,34 +177,31 @@ * @param {elem} The element. * @param {duration} Optionally the duration of the scroll operation. * @param {offset} Optionally the offset of the top of the element from the center of the screen. - * A value of 0 is ignored. - * @param {onDone} Callback function to be invoken once the scroll finishes. + * @param {onDone} An optional callback function to be invoked once the scroll finished. */ var scrollToCenterOf = function (elem, duration, offset, onDone) { - scrollToY( - Math.max( - getRelativeTopOf(elem) - getViewHeight()/2 + (offset || elem.getBoundingClientRect().height/2), - 0 - ), - duration, - onDone - ) + scrollToY(Math.max(0, container.getTopOf(elem) - container.getHeight()/2 + (offset || elem.getBoundingClientRect().height/2)), duration, onDone) } /** * Changes default settings for this scroller. * - * @param {newDefaultDuration} New value for default duration, used for each scroll method by default. - * Ignored if 0 or falsy. - * @param {newEdgeOffset} New value for the edge offset, used by each scroll method by default. + * @param {newDefaultDuration} Optionally a new value for default duration, used for each scroll method by default. + * Ignored if null or undefined. + * @param {newEdgeOffset} Optionally a new value for the edge offset, used by each scroll method by default. Ignored if null or undefined. + * @returns An object with the current values. */ var setup = function (newDefaultDuration, newEdgeOffset) { - if (newDefaultDuration) { + if (newDefaultDuration === 0 || newDefaultDuration) { defaultDuration = newDefaultDuration } if (newEdgeOffset === 0 || newEdgeOffset) { edgeOffset = newEdgeOffset } + return { + defaultDuration: defaultDuration, + edgeOffset: edgeOffset + } } return { @@ -232,70 +212,141 @@ center: scrollToCenterOf, stop: stopScroll, moving: function () { return !!scrollTimeoutId }, - getY: getScrollTop + getY: container.getY, + getTopOf: container.getTopOf } } - // Create a scroller for the browser window, omitting parameters: - var defaultScroller = createScroller() - // Create listeners for the documentElement only & exclude IE8- - if ("addEventListener" in window && !(isNativeSmoothScrollEnabledOn(document.body) || window.noZensmooth)) { - if ("scrollRestoration" in history) { - history.scrollRestoration = "manual" - window.addEventListener("popstate", function (event) { - if (event.state && "scrollY" in event.state) { - defaultScroller.toY(event.state.scrollY) - } - }, false) + var docElem = document.documentElement + var getDocY = function () { return window.scrollY || docElem.scrollTop } + + // Create a scroller for the document: + var zenscroll = makeScroller({ + body: document.scrollingElement || document.body, + toY: function (y) { window.scrollTo(0, y) }, + getY: getDocY, + getHeight: function () { return window.innerHeight || docElem.clientHeight }, + getTopOf: function (elem) { return elem.getBoundingClientRect().top + getDocY() - docElem.offsetTop } + }) + + + /** + * Creates a scroller from the provided container element (e.g., a DIV) + * + * @param {scrollContainer} The vertical position within the document. + * @param {defaultDuration} Optionally a value for default duration, used for each scroll method by default. + * Ignored if 0 or null or undefined. + * @param {edgeOffset} Optionally a value for the edge offset, used by each scroll method by default. + * Ignored if null or undefined. + * @returns A scroller object, similar to `zenscroll` but controlling the provided element. + */ + zenscroll.createScroller = function (scrollContainer, defaultDuration, edgeOffset) { + return makeScroller({ + body: scrollContainer, + toY: function (y) { scrollContainer.scrollTop = y }, + getY: function () { return scrollContainer.scrollTop }, + getHeight: function () { return Math.min(scrollContainer.clientHeight, window.innerHeight || docElem.clientHeight) }, + getTopOf: function (elem) { return elem.offsetTop } + }, defaultDuration, edgeOffset) + } + + + // Automatic link-smoothing on achors + // Exclude IE8- or when native is enabled or Zenscroll auto- is disabled + if ("addEventListener" in window && !window.noZensmooth && !isNativeSmoothScrollEnabledOn(document.body)) { + + + var isScrollRestorationSupported = "scrollRestoration" in history + + // On first load & refresh make sure the browser restores the position first + if (isScrollRestorationSupported) { + history.scrollRestoration = "auto" } - var replaceUrl = function (hash, newY) { - try { - history.replaceState({scrollY:defaultScroller.getY()}, "") // remember the scroll position before scrolling - history.pushState({scrollY:newY}, "", window.location.href.split("#")[0] + hash) // remember the new scroll position (which will be after scrolling) - } catch (e) { - // To avoid the Security exception in Chrome when the page was opened via the file protocol, e.g., file://index.html + + window.addEventListener("load", function () { + + if (isScrollRestorationSupported) { + // Set it to manual + setTimeout(function () { history.scrollRestoration = "manual" }, 9) + window.addEventListener("popstate", function (event) { + if (event.state && "zenscrollY" in event.state) { + zenscroll.toY(event.state.zenscrollY) + } + }, false) } - } + + // Add edge offset on first load if necessary + // This may not work on IE (or older computer?) as it requires more timeout, around 100 ms + if (window.location.hash) { + setTimeout(function () { + // Adjustment is only needed if there is an edge offset: + var edgeOffset = zenscroll.setup().edgeOffset + if (edgeOffset) { + var targetElem = document.getElementById(window.location.href.split("#")[1]) + if (targetElem) { + var targetY = Math.max(0, zenscroll.getTopOf(targetElem) - edgeOffset) + var diff = zenscroll.getY() - targetY + // Only do the adjustment if the browser is very close to the element: + if (0 <= diff && diff < 9 ) { + window.scrollTo(0, targetY) + } + } + } + }, 9) + } + + }, false) + + // Handling clicks on anchors + var RE_noZensmooth = new RegExp("(^|\\s)noZensmooth(\\s|$)") window.addEventListener("click", function (event) { var anchor = event.target while (anchor && anchor.tagName !== "A") { anchor = anchor.parentNode } - // Only handle links that were clicked with the primary button, without modifier keys: + // Let the browser handle the click if it wasn't with the primary button, or with some modifier keys: if (!anchor || event.which !== 1 || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) { return } + // Save the current scrolling position so it can be used for scroll restoration: + if (isScrollRestorationSupported) { + try { + history.replaceState({ zenscrollY: zenscroll.getY() }, "") + } catch (e) { + // Avoid the Chrome Security exception on file protocol, e.g., file://index.html + } + } + // Find the referenced ID: var href = anchor.getAttribute("href") || "" - if (href.indexOf("#") === 0) { - if (href === "#") { - event.preventDefault() - defaultScroller.toY(0) - replaceUrl("", 0) - } else { - var targetId = anchor.hash.substring(1) - var targetElem = document.getElementById(targetId) - if (targetElem) { - event.preventDefault() - replaceUrl("#" + targetId, defaultScroller.to(targetElem)) + if (href.indexOf("#") === 0 && !RE_noZensmooth.test(anchor.className)) { + var targetY = 0 + var targetElem = document.getElementById(href.substring(1)) + if (href !== "#") { + if (!targetElem) { + // Let the browser handle the click if the target ID is not found. + return } + targetY = zenscroll.getTopOf(targetElem) + } + event.preventDefault() + // By default trigger the browser's `hashchange` event... + var onDone = function () { window.location = href } + // ...unless there is an edge offset specified + var edgeOffset = zenscroll.setup().edgeOffset + if (edgeOffset) { + targetY = Math.max(0, targetY - edgeOffset) + onDone = function () { history.pushState(null, "", href) } } + zenscroll.toY(targetY, null, onDone) } }, false) - } - return { - // Expose the "constructor" that can create a new scroller: - createScroller: createScroller, - // Surface the methods of the default scroller: - setup: defaultScroller.setup, - to: defaultScroller.to, - toY: defaultScroller.toY, - intoView: defaultScroller.intoView, - center: defaultScroller.center, - stop: defaultScroller.stop, - moving: defaultScroller.moving } + + return zenscroll + + }));