Skip to content

Commit

Permalink
Merge pull request #12 from jonaskuske/v.1.1.0
Browse files Browse the repository at this point in the history
v.1.1.0
  • Loading branch information
jonaskuske authored Dec 9, 2018
2 parents 058cf85 + c4e9ae4 commit ba49c8f
Show file tree
Hide file tree
Showing 9 changed files with 476 additions and 279 deletions.
7 changes: 6 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
language: node_js
node_js:
- "node"
- "10"
- "8"
install:
- npm install
script:
- npm test
- npm test
cache:
directories:
- node_modules
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2018-12-10
### Added
- `destroy()` and `polyfill()` now return the polyfill instance so you can chain them
- Tests for Node environment (→ SSR), `destroy()`, `polyfill()` and `{ force }` override
- Improved JSDoc typing for better IntelliSense completion
- Entry `"unpkg"` in `package.json`, points at minified version so CDN serves smaller file
### Changed
- You can now override `window.__forceSmoothscrollAnchorPolyfill__` with the `{ force: boolean }` argument of `polyfill()`
- Package entry (`"main"`) now points to unminified file so typing hints are kept
- Explain usage of `{ behavior: 'instant' }` (not in spec anymore) + outline alternative
### Fixed
- (Regression) Prevent 'window is not defined' error in Node environments

## [1.0.1] - 2018-12-08
### Added
- Added feature overview to README
Expand Down
12 changes: 7 additions & 5 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
<script>
// Some Polyfills for Internet Explorer 9, needed for this page
// Super cheap requestAnimationFrame polyfill
'requestAnimationFrame' in window || (window.requestAnimationFrame = function (a) { setTimeout(a, 0) });
'requestAnimationFrame' in window || (window.requestAnimationFrame = function(a) { setTimeout(a, 0) });
// Element.classList polyfill
; (function () { function a(e) { this.element = e } var b = function (e) { return e.replace(/^\s+|\s+$/g, '') }, c = function (e) { return new RegExp('(^|\\s+)' + e + '(\\s+|$)') }, d = function (e, f, g) { for (var h = 0; h < e.length; h++)f.call(g, e[h]) }; a.prototype = { add: function add() { d(arguments, function (e) { this.contains(e) || (this.element.className = b(this.element.className + ' ' + e)) }, this) }, remove: function remove() { d(arguments, function (e) { this.element.className = b(this.element.className.replace(c(e), ' ')) }, this) }, toggle: function toggle(e) { return this.contains(e) ? (this.remove(e), !1) : (this.add(e), !0) }, contains: function contains(e) { return c(e).test(this.element.className) }, item: function item(e) { return this.element.className.split(/\s+/)[e] || null }, replace: function replace(e, f) { this.remove(e), this.add(f) } }, 'classList' in Element.prototype || Object.defineProperty(Element.prototype, 'classList', { get: function get() { return new a(this) } }), window.DOMTokenList && !DOMTokenList.prototype.replace && (DOMTokenList.prototype.replace = a.prototype.replace) })();
!(function() { function a(e) { this.element = e } var b = function(e) { return e.replace(/^\s+|\s+$/g, '') }, c = function(e) { return new RegExp('(^|\\s+)' + e + '(\\s+|$)') }, d = function(e, f, g) { for (var h = 0; h < e.length; h++)f.call(g, e[h]) }; a.prototype = { add: function add() { d(arguments, function(e) { this.contains(e) || (this.element.className = b(this.element.className + ' ' + e)) }, this) }, remove: function remove() { d(arguments, function(e) { this.element.className = b(this.element.className.replace(c(e), ' ')) }, this) }, toggle: function toggle(e) { return this.contains(e) ? (this.remove(e), !1) : (this.add(e), !0) }, contains: function contains(e) { return c(e).test(this.element.className) }, item: function item(e) { return this.element.className.split(/\s+/)[e] || null }, replace: function replace(e, f) { this.remove(e), this.add(f) } }, 'classList' in Element.prototype || Object.defineProperty(Element.prototype, 'classList', { get: function get() { return new a(this) } }), window.DOMTokenList && !DOMTokenList.prototype.replace && (DOMTokenList.prototype.replace = a.prototype.replace) })();
</script>
<!-- Use any polyfill for the smoothscroll JavaScript API -->
<script src="https://unpkg.com/smoothscroll-polyfill/dist/smoothscroll.min.js"></script>
Expand All @@ -25,7 +25,7 @@

<script>
// Hide header after scroll (mobile only)
document.addEventListener('DOMContentLoaded', function () { var a = document, b = a.body, c = b.classList, e = a.documentElement.scrollTop || b.scrollTop; a.addEventListener('scroll', function () { var f = a.documentElement.scrollTop || b.scrollTop, g = f - e; 25 > Math.abs(g) || (0 < g ? c.add('hide-header') : c.remove('hide-header'), e = f) }, !1) });
document.addEventListener('DOMContentLoaded', function() { var a = document, b = a.body, c = b.classList, e = a.documentElement.scrollTop || b.scrollTop; a.addEventListener('scroll', function() { var f = a.documentElement.scrollTop || b.scrollTop, g = f - e; 25 > Math.abs(g) || (0 < g ? c.add('hide-header') : c.remove('hide-header'), e = f) }, !1) });
</script>
</head>

Expand Down Expand Up @@ -247,6 +247,8 @@ <h3 id="methods"><span>Methods: <code>destroy</code> and <code>polyfill</code></
<p>This package exports two methods, <code>destroy</code> and <code>polyfill</code>.
If loaded through a script tag, these methods are exposed on
<span class="allow-overflow"><code>window.smoothscrollAnchorPolyfill</code></span>.</p>
<p>Both methods return the polyfill instance so you can chain them
(e.g. <code>.destroy().polyfill()</code> to restart the script).</p>
</div>
<div class="row row-wrap">
<div class="column column-33" style="flex-grow:1;max-width:100%;">
Expand Down Expand Up @@ -571,9 +573,9 @@ <h3>Privacy</h3>

<script>
// Update URL when clicking on title without triggering navigation
Array.prototype.slice.call(document.querySelectorAll('h1[id],h2[id],h3[id],h4[id]')).forEach(function (a) { a.addEventListener('click', function () { var b = a.id; if (null !== b && b !== void 0) { var c = location.href; 0 < c.indexOf('#') ? c = c.replace(/#.*$/, '#' + b) : c += '#' + b, history.replaceState(null, document.title, c) } }) });
Array.prototype.slice.call(document.querySelectorAll('h1[id],h2[id],h3[id],h4[id]')).forEach(function(a) { a.addEventListener('click', function() { var b = a.id; if (null !== b && b !== void 0) { var c = location.href; 0 < c.indexOf('#') ? c = c.replace(/#.*$/, '#' + b) : c += '#' + b, history.replaceState(null, document.title, c) } }) });
// Flexbox feature detect & Button for toggling smoothscroll
!(function (d) { ('alignItems' in d.documentElement.style) && (d.documentElement.className += " flex-center"); d.querySelector('#scroll-btn button').addEventListener('click', function () { d.documentElement.classList.toggle('smooth-scroll') }) })(document)
!(function(d) { ('alignItems' in d.documentElement.style) && (d.documentElement.className += " flex-center"); d.querySelector('#scroll-btn button').addEventListener('click', function() { d.documentElement.classList.toggle('smooth-scroll') }) })(document)
</script>
</body>

Expand Down
133 changes: 79 additions & 54 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,76 @@
// @ts-check

/**
* @license
* smoothscroll-anchor-polyfill __VERSION__
* (c) 2018 Jonas Kuske
* Released under the MIT License.
*/

(function(global, factory) {
var SmoothscrollAnchorPolyfill = factory();
(function(global, Factory) {
var SmoothscrollAnchorPolyfill = new Factory();
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = SmoothscrollAnchorPolyfill;
} else {
global.SmoothscrollAnchorPolyfill = SmoothscrollAnchorPolyfill;
}
SmoothscrollAnchorPolyfill.polyfill();
})(this, function SmoothscrollAnchorPolyfill() {

// In test environment: abort, else run polyfill immediately
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') return;
else SmoothscrollAnchorPolyfill.polyfill();
})(this, function() {
/**
* Workaround for type check :(
* @typedef {{__forceSmoothscrollAnchorPolyfill__: [boolean]}} GlobalFlag
* @typedef {Window & GlobalFlag} ExtendedWindow
*/

/* */
var instance = this, isBrowser = typeof window !== 'undefined';

// Abort if outside browser (window is undefined in Node etc.)
var isBrowser = typeof window !== 'undefined';
if (!isBrowser) return;
if (isBrowser) {
/** @type {ExtendedWindow} */
var w = (window), d = document, docEl = d.documentElement;
}

/**
* Starts the polyfill by attaching the neccessary EventListeners
*
* Aborts, if ('scrollBehavior' in documentElement.style) and the force flag
* isn't set on the options parameter Object or globally on window
* @param {PolyfillOptions} [opts] Options for invoking
* @returns {SmoothscrollAnchorPolyfill} Polyfill Instance, allows for chaining
*
* @typedef {Object} PolyfillOptions
* @prop {boolean} [force] Enable despite native support, overrides global flag
*/
this.polyfill = function(opts) {
opts = opts || {};
if (isBrowser) {
var globalFlag = w.__forceSmoothscrollAnchorPolyfill__;
var force = typeof opts.force === 'boolean' ? opts.force : globalFlag;

// Abort if smoothscroll has native support and force flag isn't set
if ('scrollBehavior' in docEl.style && !force) return instance;

d.addEventListener('click', handleClick, false);
d.addEventListener('scroll', trackScrollPositions);
w.addEventListener('hashchange', handleHashChange);
}
return instance;
};

/**
* Stops the polyfill by removing all EventListeners
* @returns {SmoothscrollAnchorPolyfill} Polyfill Instance, allows for chaining
*/
this.destroy = function() {
if (isBrowser) {
d.removeEventListener('click', handleClick, false);
d.removeEventListener('scroll', trackScrollPositions);
w.removeEventListener('hashchange', handleHashChange);
}
return instance;
};

var w = window, d = document, docEl = d.documentElement;
if (!isBrowser) return;

// Check if browser supports focus without automatic scrolling (preventScroll)
var supportsPreventScroll = false;
Expand All @@ -50,7 +96,7 @@
function shouldSmoothscroll() {
// Values to check for set scroll-behavior in order of priority/specificity
var valuesToCheck = [
// Priority 1: behavior specified as inline property
// Priority 1: behavior assigned to style property
// Allows toggling smoothscroll from JS (docEl.style.scrollBehavior = ...)
docEl.style.scrollBehavior,
// Priority 2: behavior specified inline in style attribute
Expand Down Expand Up @@ -83,21 +129,23 @@

/**
* Get the target element of an event
* @param {event} evt
* @param {Event} evt
* @returns {HTMLElement}
*/
function getEventTarget(evt) {
evt = evt || w.event;
return evt.target || evt.srcElement;
return /** @type {HTMLElement} */ (evt.target || evt.srcElement);
}

/**
* Check if an element is an anchor pointing to a target on the current page
* @param {HTMLElement} el
* @param {HTMLAnchorElement} el
* @returns {boolean}
*/
function isAnchorToLocalElement(el) {
return (
// Is an anchor with a fragment in the url
el.tagName && el.tagName.toLowerCase() === 'a' && /#/.test(el.href) &&
el.tagName.toLowerCase() === 'a' && /#/.test(el.href) &&
// Target is on current page
el.hostname === location.hostname && el.pathname === location.pathname
);
Expand Down Expand Up @@ -135,15 +183,15 @@
}

/**
* Walks up the DOM starting from a given element until an element satisfies the validate function
* Walks up the DOM starting from "element" until an element satisfies "validate()"
* @param {HTMLElement} element The element from where to start validating
* @param {Function} validate The validation function
* @returns {HTMLElement|boolean}
* @param {function} validate Validation function, receives current element as arg
* @returns {?HTMLElement} The found element or null
*/
function findInParents(element, validate) {
if (validate(element)) return element;
if (element.parentNode) return findInParents(element.parentNode, validate);
return false;
if (element.parentElement) return findInParents(element.parentElement, validate);
return null;
}

// Stores the setTimeout id of pending focus changes, allows aborting them
Expand Down Expand Up @@ -174,7 +222,7 @@
* Check if the clicked target is an anchor pointing to a local element,
* if so prevent the default behavior and handle the scrolling using the
* native JavaScript scroll APIs so smoothscroll polyfills apply
* @param {event} evt
* @param {MouseEvent} evt
*/
function handleClick(evt) {
// Abort if shift/ctrl-click or not primary click (button !== 0)
Expand All @@ -183,7 +231,9 @@
if (!shouldSmoothscroll()) return;

// Check the DOM from the click target upwards if a local anchor was clicked
var anchor = findInParents(getEventTarget(evt), isAnchorToLocalElement);
var anchor = /** @type {?HTMLAnchorElement} */ (
findInParents(getEventTarget(evt), isAnchorToLocalElement)
);
if (!anchor) return;

// Find the element targeted by the hash
Expand All @@ -200,7 +250,6 @@
// Append the hash to the URL
if (history.pushState) history.pushState(null, d.title, (hash || '#'));
}

}

// To enable smooth scrolling on hashchange, we need to immediately restore
Expand All @@ -218,8 +267,8 @@
* and instead scrolls smoothly to the new hash target
*/
function handleHashChange() {
// scroll-behavior not set to smooth or body nor parsed yet? Abort
if (!shouldSmoothscroll() || !d.body) return;
// scroll-behavior not set to smooth or body not parsed yet? Abort
if (!d.body || !shouldSmoothscroll()) return;

var target = getScrollTarget(location.hash);
if (!target) return;
Expand All @@ -229,7 +278,11 @@
var currentPos = getScrollTop();
var top = lastTwoScrollPos[lastTwoScrollPos[1] === currentPos ? 0 : 1];

// @ts-ignore
// Undo the scroll caused by the hashchange...
// Using {behavior: 'instant'} even though it's not in the spec anymore as
// Blink & Gecko support it – once an engine with native support doesn't,
// we need to disable scroll-behavior during scroll reset, then restore
w.scroll({ top: top, behavior: 'instant' });
// ...and instead smoothscroll to the target
triggerSmoothscroll(target);
Expand All @@ -250,32 +303,4 @@
lastTwoScrollPos[0] = lastTwoScrollPos[1];
lastTwoScrollPos[1] = getScrollTop();
}

return {
/**
* Starts the polyfill by attaching the neccessary EventListeners
*
* Aborts, if ('scrollBehavior' in documentElement.style) and the force flag
* isn't set on the options parameter Object or globally on window
* @param {{force: boolean}} opts Enable polyfill despite native support
*/
polyfill: function(opts) {
opts = opts || {};
// Abort if smoothscroll is natively supported and force flag is not set
var forcePolyfill = opts.force || w.__forceSmoothscrollAnchorPolyfill__;
if (!forcePolyfill && 'scrollBehavior' in docEl.style) return;

d.addEventListener('click', handleClick, false);
d.addEventListener('scroll', trackScrollPositions);
w.addEventListener('hashchange', handleHashChange);
},
/**
* Stops the polyfill by removing all EventListeners
*/
destroy: function() {
d.removeEventListener('click', handleClick, false);
d.removeEventListener('scroll', trackScrollPositions);
w.removeEventListener('hashchange', handleHashChange);
}
};
});
Loading

0 comments on commit ba49c8f

Please sign in to comment.