From 7d2188cb938ce7b9cff79f837ac03644891eb2b7 Mon Sep 17 00:00:00 2001 From: Jonas Kuske <30421456+jonaskuske@users.noreply.github.com> Date: Fri, 7 Dec 2018 21:40:34 +0100 Subject: [PATCH 1/8] feat: export polyfill and destroy methods The polyfill now exports a method 'destroy' as part of it's public API (previously it was returned, only for testing) so you can stop it. To restart later, an additional method 'polyfill' is exposed. --- index.js | 69 ++++++++++++++++++++++++++++++-------------------------- test.js | 37 +++++++++++------------------- 2 files changed, 50 insertions(+), 56 deletions(-) diff --git a/index.js b/index.js index 7e59a05..abf7ee1 100755 --- a/index.js +++ b/index.js @@ -4,31 +4,28 @@ * (c) 2018 Jonas Kuske * Released under the MIT License. */ -(function (indexFn) { - if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { - // Test env: export function so it can run multiple times - exports.polyfill = indexFn; + +(function (global, factory) { + var SmoothscrollAnchorPolyfill = factory(); + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = SmoothscrollAnchorPolyfill; } else { - // Else run immediately, no time to waste! - indexFn(); + global.SmoothscrollAnchorPolyfill = SmoothscrollAnchorPolyfill; } -})(function () { - /* */ + // In test environment: abort, else run polyfill immediately + if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') return; + else SmoothscrollAnchorPolyfill.polyfill(); +})(this, function () { - // Noop to be returned if polyfill returns early. Will be assigned later - var destroy = function () { }; + /* */ // Abort if outside browser (window is undefined in Node etc.) var isBrowser = typeof window !== 'undefined'; - if (!isBrowser) return destroy; + if (!isBrowser) return; var w = window, d = document, docEl = d.documentElement; - var forcePolyfill = w.__forceSmoothscrollAnchorPolyfill__ === true; - // Abort if smoothscroll is natively supported and force flag is not set - if (!forcePolyfill && 'scrollBehavior' in docEl.style) return destroy; - // Check if browser supports focus without automatic scrolling (preventScroll) var supportsPreventScroll = false; try { @@ -75,7 +72,7 @@ * If a valid CSS property value for scroll-behavior is passed, returns * whether it specifies smooth scroll behavior or not, else returns null * @param {any} value The value to check - * @returns {boolean|undefined} + * @returns {?boolean} The specified scroll behavior or null */ function getScrollBehavior(value) { var status = null; @@ -99,10 +96,8 @@ */ function isAnchorToLocalElement(el) { return ( - // Is an anchor - el.tagName && el.tagName.toLowerCase() === 'a' && - // Targets an element - el.href.indexOf('#') > -1 && + // Is an anchor with a fragment in the url + el.tagName && el.tagName.toLowerCase() === 'a' && /#/.test(el.href) && // Target is on current page el.hostname === location.hostname && el.pathname === location.pathname ); @@ -256,16 +251,26 @@ lastTwoScrollPos[1] = getScrollTop(); } - // Register all event handlers - d.addEventListener('click', handleClick, false); - d.addEventListener('scroll', trackScrollPositions); - w.addEventListener("hashchange", handleHashChange); - - // Assign destroy function that unregisters event listeners. Used for testing - destroy = function () { - d.removeEventListener('click', handleClick, false); - d.removeEventListener('scroll', trackScrollPositions); - w.removeEventListener('hashchange', handleHashChange); - } - return destroy; + return { + /** + * Starts the polyfill by attaching the neccessary EventListeners + */ + polyfill: function () { + // Abort if smoothscroll is natively supported and force flag is not set + var forcePolyfill = w.__forceSmoothscrollAnchorPolyfill__ === true; + 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); + } + }; }); diff --git a/test.js b/test.js index b83e55d..0a6cb7b 100755 --- a/test.js +++ b/test.js @@ -1,4 +1,4 @@ -const { polyfill } = require('./index') +const { polyfill, destroy } = require('./index') const insertElement = (type, attrs) => { const el = document.createElement(type) @@ -22,7 +22,7 @@ afterEach(() => { document.documentElement.removeAttribute('style') document.body.innerHTML = '' - document.head.innerHTML = '' + destroy() }) describe('General', () => { @@ -33,11 +33,10 @@ describe('General', () => { document.documentElement.style.scrollBehavior = 'smooth' const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() anchor.click() expect(spy).not.toHaveBeenCalled() - destroy() }) @@ -50,11 +49,10 @@ describe('General', () => { window.__forceSmoothscrollAnchorPolyfill__ = true const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() anchor.click() expect(spy).toHaveBeenCalled() - destroy() }) }) @@ -66,12 +64,11 @@ describe('Scroll targeting', () => { const anchorTwo = insertElement('a', { href: '#' }) const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() anchor.click() anchorTwo.click() expect(spy).toHaveBeenCalledTimes(2) - destroy() }) it('Scrolls to targeted element if anchor targets its ID', () => { @@ -82,12 +79,11 @@ describe('Scroll targeting', () => { const windowSpy = jest.spyOn(window, 'scroll') const spy = jest.spyOn(target, 'scrollIntoView') - const destroy = polyfill() + polyfill() anchor.click() expect(windowSpy).not.toHaveBeenCalled() expect(spy).toHaveBeenCalled() - destroy() }) it('Scrolls to element instead of top if hash "#top" targets an ID', () => { @@ -98,12 +94,11 @@ describe('Scroll targeting', () => { const windowSpy = jest.spyOn(window, 'scroll') const spy = jest.spyOn(target, 'scrollIntoView') - const destroy = polyfill() + polyfill() anchor.click() expect(spy).toHaveBeenCalled() expect(windowSpy).not.toHaveBeenCalled() - destroy() }) }) @@ -115,11 +110,10 @@ describe('Enabling & Toggling the polyfill', () => { const anchor = insertElement('a', { href: '#' }) const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() anchor.click() expect(spy).toHaveBeenCalled() - destroy() }) it('Can be enabled through the style attribute on ', () => { @@ -128,18 +122,17 @@ describe('Enabling & Toggling the polyfill', () => { const anchor = insertElement('a', { href: '#' }) const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() anchor.click() expect(spy).toHaveBeenCalled() - destroy() }) it('Can be enabled by documentElement.style.scrollBehavior', () => { const anchor = insertElement('a', { href: '#' }) const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() // Only set scrollBehavior after the polyfill ran, so it doesn't bail // due to checking for native support ('scrollBehavior' in docEl.style) @@ -147,7 +140,6 @@ describe('Enabling & Toggling the polyfill', () => { anchor.click() expect(spy).toHaveBeenCalled() - destroy() }) it('Can be disabled through method with higher specificity', () => { @@ -157,7 +149,7 @@ describe('Enabling & Toggling the polyfill', () => { const anchor = insertElement('a', { href: '#' }) const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() anchor.click() expect(spy).toHaveBeenCalledTimes(1) @@ -169,7 +161,6 @@ describe('Enabling & Toggling the polyfill', () => { document.documentElement.style.scrollBehavior = 'smooth' anchor.click() expect(spy).toHaveBeenCalledTimes(2) - destroy() }) }) @@ -180,21 +171,20 @@ describe('Enabling & Toggling the polyfill', () => { const anchor = insertElement('a', { href: '#top' }) const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() anchor.click() expect(spy).toHaveBeenCalledTimes(1) document.documentElement.style.scrollBehavior = 'auto' anchor.click() expect(spy).toHaveBeenCalledTimes(1) - destroy() }) it('Responds to computedStyle of font-family', () => { const anchor = insertElement('a', { href: '#top' }) const spy = jest.spyOn(window, 'scroll') - const destroy = polyfill() + polyfill() anchor.click() expect(spy).toHaveBeenCalledTimes(0) @@ -204,7 +194,6 @@ describe('Enabling & Toggling the polyfill', () => { document.documentElement.style.font = '100 1rem "scroll-behavior:inherit"' anchor.click() expect(spy).toHaveBeenCalledTimes(1) - destroy() }) }) }) \ No newline at end of file From 58be9645760f10460a06a539e34c9ff7febe16b6 Mon Sep 17 00:00:00 2001 From: Jonas Kuske <30421456+jonaskuske@users.noreply.github.com> Date: Fri, 7 Dec 2018 21:49:25 +0100 Subject: [PATCH 2/8] feat: accept force flag as parameter when invoking polyfill method --- index.js | 523 ++++++++++++++++++++++++++++--------------------------- test.js | 16 +- 2 files changed, 279 insertions(+), 260 deletions(-) diff --git a/index.js b/index.js index abf7ee1..7f3c96e 100755 --- a/index.js +++ b/index.js @@ -5,272 +5,277 @@ * Released under the MIT License. */ -(function (global, factory) { - var SmoothscrollAnchorPolyfill = factory(); - if (typeof exports === 'object' && typeof module !== 'undefined') { - module.exports = SmoothscrollAnchorPolyfill; - } else { - global.SmoothscrollAnchorPolyfill = SmoothscrollAnchorPolyfill; - } - - // In test environment: abort, else run polyfill immediately - if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') return; - else SmoothscrollAnchorPolyfill.polyfill(); -})(this, function () { - - /* */ - - // Abort if outside browser (window is undefined in Node etc.) - var isBrowser = typeof window !== 'undefined'; - if (!isBrowser) return; - - var w = window, d = document, docEl = d.documentElement; - - // Check if browser supports focus without automatic scrolling (preventScroll) - var supportsPreventScroll = false; - try { - var el = d.createElement('a'); - // Define getter for preventScroll to find out if the browser accesses it - var preppedFocusOption = Object.defineProperty({}, 'preventScroll', { - get: function () { - supportsPreventScroll = true; - } - }); - // Trigger focus – if browser uses preventScroll the var will be set to true - el.focus(preppedFocusOption); - } catch (e) { } - - // Regex to extract the value following the scroll-behavior property name - var extractValue = /scroll-behavior:[\s]*([^;"`'\s]+)/; - - /** - * Returns true if scroll-behavior: smooth is set and not overwritten - * by a higher-specifity declaration, else returns false - */ - function shouldSmoothscroll() { - // Values to check for set scroll-behavior in order of priority/specificity - var valuesToCheck = [ - // Priority 1: behavior specified as inline property - // Allows toggling smoothscroll from JS (docEl.style.scrollBehavior = ...) - docEl.style.scrollBehavior, - // Priority 2: behavior specified inline in style attribute - (extractValue.exec(docEl.getAttribute('style')) || [])[1], - // Priority 3: behavior specified in fontFamily - // Behaves like regular CSS, e.g. allows using media queries - (extractValue.exec(getComputedStyle(docEl).fontFamily) || [])[1] - ]; - // Loop over values in specified order, return once a valid value is found - for (var i = 0; i < valuesToCheck.length; i++) { - var specifiedBehavior = getScrollBehavior(valuesToCheck[i]); - if (specifiedBehavior !== null) return specifiedBehavior; +(function(global, factory) { + var SmoothscrollAnchorPolyfill = factory(); + if (typeof exports === 'object' && typeof module !== 'undefined') { + module.exports = SmoothscrollAnchorPolyfill; + } else { + global.SmoothscrollAnchorPolyfill = SmoothscrollAnchorPolyfill; } - // No value found? Return false, no set value = no smoothscroll :( - return false; - } - - /** - * If a valid CSS property value for scroll-behavior is passed, returns - * whether it specifies smooth scroll behavior or not, else returns null - * @param {any} value The value to check - * @returns {?boolean} The specified scroll behavior or null - */ - function getScrollBehavior(value) { - var status = null; - if (/^smooth$/.test(value)) status = true; - if (/^(initial|inherit|auto|unset)$/.test(value)) status = false; - return status; - } - - /** - * Get the target element of an event - * @param {event} evt - */ - function getEventTarget(evt) { - evt = evt || w.event; - return evt.target || evt.srcElement; - } - - /** - * Check if an element is an anchor pointing to a target on the current page - * @param {HTMLElement} el - */ - function isAnchorToLocalElement(el) { - return ( - // Is an anchor with a fragment in the url - el.tagName && el.tagName.toLowerCase() === 'a' && /#/.test(el.href) && - // Target is on current page - el.hostname === location.hostname && el.pathname === location.pathname - ); - } - - /** - * Focuses an element, if it's not focused after the first try, - * allow focusing by adjusting tabIndex and retry - * @param {HTMLElement} el - */ - function focusElement(el) { - el.focus({ preventScroll: true }); - if (d.activeElement !== el) { - el.setAttribute('tabindex', '-1'); - // TODO: Only remove outline if it comes from the UA, not the user CSS - el.style.outline = 'none'; - el.focus({ preventScroll: true }); + + // In test environment: abort, else run polyfill immediately + if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') return; + else SmoothscrollAnchorPolyfill.polyfill(); +})(this, function() { + + /* */ + + // Abort if outside browser (window is undefined in Node etc.) + var isBrowser = typeof window !== 'undefined'; + if (!isBrowser) return; + + var w = window, d = document, docEl = d.documentElement; + + // Check if browser supports focus without automatic scrolling (preventScroll) + var supportsPreventScroll = false; + try { + var el = d.createElement('a'); + // Define getter for preventScroll to find out if the browser accesses it + var preppedFocusOption = Object.defineProperty({}, 'preventScroll', { + get: function() { + supportsPreventScroll = true; + } + }); + // Trigger focus – if browser uses preventScroll the var will be set to true + el.focus(preppedFocusOption); + } catch (e) { } + + // Regex to extract the value following the scroll-behavior property name + var extractValue = /scroll-behavior:[\s]*([^;"`'\s]+)/; + + /** + * Returns true if scroll-behavior: smooth is set and not overwritten + * by a higher-specifity declaration, else returns false + */ + function shouldSmoothscroll() { + // Values to check for set scroll-behavior in order of priority/specificity + var valuesToCheck = [ + // Priority 1: behavior specified as inline property + // Allows toggling smoothscroll from JS (docEl.style.scrollBehavior = ...) + docEl.style.scrollBehavior, + // Priority 2: behavior specified inline in style attribute + (extractValue.exec(docEl.getAttribute('style')) || [])[1], + // Priority 3: behavior specified in fontFamily + // Behaves like regular CSS, e.g. allows using media queries + (extractValue.exec(getComputedStyle(docEl).fontFamily) || [])[1] + ]; + // Loop over values in specified order, return once a valid value is found + for (var i = 0; i < valuesToCheck.length; i++) { + var specifiedBehavior = getScrollBehavior(valuesToCheck[i]); + if (specifiedBehavior !== null) return specifiedBehavior; + } + // No value found? Return false, no set value = no smoothscroll :( + return false; + } + + /** + * If a valid CSS property value for scroll-behavior is passed, returns + * whether it specifies smooth scroll behavior or not, else returns null + * @param {any} value The value to check + * @returns {?boolean} The specified scroll behavior or null + */ + function getScrollBehavior(value) { + var status = null; + if (/^smooth$/.test(value)) status = true; + if (/^(initial|inherit|auto|unset)$/.test(value)) status = false; + return status; + } + + /** + * Get the target element of an event + * @param {event} evt + */ + function getEventTarget(evt) { + evt = evt || w.event; + return evt.target || evt.srcElement; + } + + /** + * Check if an element is an anchor pointing to a target on the current page + * @param {HTMLElement} el + */ + function isAnchorToLocalElement(el) { + return ( + // Is an anchor with a fragment in the url + el.tagName && el.tagName.toLowerCase() === 'a' && /#/.test(el.href) && + // Target is on current page + el.hostname === location.hostname && el.pathname === location.pathname + ); + } + + /** + * Focuses an element, if it's not focused after the first try, + * allow focusing by adjusting tabIndex and retry + * @param {HTMLElement} el + */ + function focusElement(el) { + el.focus({ preventScroll: true }); + if (d.activeElement !== el) { + el.setAttribute('tabindex', '-1'); + // TODO: Only remove outline if it comes from the UA, not the user CSS + el.style.outline = 'none'; + el.focus({ preventScroll: true }); + } + } + + /** + * Returns the element whose id matches the hash or + * document.body if the hash is "#top" or "" (empty string) + * @param {string} hash + */ + function getScrollTarget(hash) { + if (typeof hash !== 'string') return null; + + // Retrieve target if an id is specified in the hash, otherwise use body. + // If hash is "#top" and no target with id "top" was found, also use body + // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href + var target = hash ? d.getElementById(hash.slice(1)) : d.body; + if (hash === '#top' && !target) target = d.body; + return target; } - } - - /** - * Returns the element whose id matches the hash or - * document.body if the hash is "#top" or "" (empty string) - * @param {string} hash - */ - function getScrollTarget(hash) { - if (typeof hash !== 'string') return null; - - // Retrieve target if an id is specified in the hash, otherwise use body. - // If hash is "#top" and no target with id "top" was found, also use body - // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-href - var target = hash ? d.getElementById(hash.slice(1)) : d.body; - if (hash === '#top' && !target) target = d.body; - return target; - } - - /** - * Walks up the DOM starting from a given element until an element satisfies the validate function - * @param {HTMLElement} element The element from where to start validating - * @param {Function} validate The validation function - * @returns {HTMLElement|boolean} - */ - function findInParents(element, validate) { - if (validate(element)) return element; - if (element.parentNode) return findInParents(element.parentNode, validate); - return false; - } - - // Stores the setTimeout id of pending focus changes, allows aborting them - var pendingFocusChange; - - /** - * Scrolls to a given element or to the top if the given element - * is document.body, then focuses the element - * @param {HTMLElement} target - */ - function triggerSmoothscroll(target) { - // Clear potential pending focus change triggered by a previous scroll - if (!supportsPreventScroll) clearTimeout(pendingFocusChange); - - // Use JS scroll APIs to scroll to top (if target is body) or to the element - // This allows polyfills for these APIs to do their smooth scrolling magic - var scrollTop = target === d.body; - if (scrollTop) w.scroll({ top: 0, left: 0, behavior: 'smooth' }); - else target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - - // If the browser supports preventScroll: immediately focus the target - // Otherwise schedule the focus so the smoothscroll isn't interrupted - if (supportsPreventScroll) focusElement(target); - else pendingFocusChange = setTimeout(focusElement.bind(null, target), 450); - } - - /** - * 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 - */ - function handleClick(evt) { - // Abort if shift/ctrl-click or not primary click (button !== 0) - if (evt.metaKey || evt.ctrlKey || evt.shiftKey || evt.button !== 0) return; - // scroll-behavior not set to smooth? Bail out, let browser handle it - if (!shouldSmoothscroll()) return; - - // Check the DOM from the click target upwards if a local anchor was clicked - var anchor = findInParents(getEventTarget(evt), isAnchorToLocalElement); - if (!anchor) return; - - // Find the element targeted by the hash - var hash = anchor.hash; - var target = getScrollTarget(hash); - - if (target) { - // Prevent default browser behavior to avoid a jump to the anchor target - evt.preventDefault(); - - // Trigger the smooth scroll - triggerSmoothscroll(target); - - // Append the hash to the URL - if (history.pushState) history.pushState(null, d.title, (hash || '#')); + + /** + * Walks up the DOM starting from a given element until an element satisfies the validate function + * @param {HTMLElement} element The element from where to start validating + * @param {Function} validate The validation function + * @returns {HTMLElement|boolean} + */ + function findInParents(element, validate) { + if (validate(element)) return element; + if (element.parentNode) return findInParents(element.parentNode, validate); + return false; } - } - - // To enable smooth scrolling on hashchange, we need to immediately restore - // the scroll pos after a hashchange changed it, so we track it constantly. - // Some browsers don't trigger a scroll event before the hashchange, - // so to undo, the position from last scroll is the one we need to go back to. - // In others (e.g. IE) the scroll listener is triggered again before the - // hashchange occurs and the last reported position is already the new one - // updated by the hashchange – we need the second last to undo there. - // Because of this we don't track just the last, but the last two positions. - var lastTwoScrollPos = []; - - /** - * Tries to undo the automatic, instant scroll caused by a hashchange - * 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; - - var target = getScrollTarget(location.hash); - if (!target) return; - - // If the position last reported by the scroll listener is the same as the - // current one caused by a hashchange, go back to second last – else last - var currentPos = getScrollTop(); - var top = lastTwoScrollPos[lastTwoScrollPos[1] === currentPos ? 0 : 1]; - - // Undo the scroll caused by the hashchange... - w.scroll({ top: top, behavior: 'instant' }); - // ...and instead smoothscroll to the target - triggerSmoothscroll(target); - } - - /** - * Returns the scroll offset towards the top - */ - function getScrollTop() { - return docEl.scrollTop || d.body.scrollTop; - } - - /** - * Update the last two scroll positions - */ - function trackScrollPositions() { - if (!d.body) return; // Body not parsed yet? Abort - lastTwoScrollPos[0] = lastTwoScrollPos[1]; - lastTwoScrollPos[1] = getScrollTop(); - } - - return { + // Stores the setTimeout id of pending focus changes, allows aborting them + var pendingFocusChange; + /** - * Starts the polyfill by attaching the neccessary EventListeners + * Scrolls to a given element or to the top if the given element + * is document.body, then focuses the element + * @param {HTMLElement} target */ - polyfill: function () { - // Abort if smoothscroll is natively supported and force flag is not set - var forcePolyfill = w.__forceSmoothscrollAnchorPolyfill__ === true; - if (!forcePolyfill && 'scrollBehavior' in docEl.style) return; - - d.addEventListener('click', handleClick, false); - d.addEventListener('scroll', trackScrollPositions); - w.addEventListener('hashchange', handleHashChange); - }, + function triggerSmoothscroll(target) { + // Clear potential pending focus change triggered by a previous scroll + if (!supportsPreventScroll) clearTimeout(pendingFocusChange); + + // Use JS scroll APIs to scroll to top (if target is body) or to the element + // This allows polyfills for these APIs to do their smooth scrolling magic + var scrollTop = target === d.body; + if (scrollTop) w.scroll({ top: 0, left: 0, behavior: 'smooth' }); + else target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + // If the browser supports preventScroll: immediately focus the target + // Otherwise schedule the focus so the smoothscroll isn't interrupted + if (supportsPreventScroll) focusElement(target); + else pendingFocusChange = setTimeout(focusElement.bind(null, target), 450); + } + /** - * Stops the polyfill by removing all EventListeners + * 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 */ - destroy: function () { - d.removeEventListener('click', handleClick, false); - d.removeEventListener('scroll', trackScrollPositions); - w.removeEventListener('hashchange', handleHashChange); + function handleClick(evt) { + // Abort if shift/ctrl-click or not primary click (button !== 0) + if (evt.metaKey || evt.ctrlKey || evt.shiftKey || evt.button !== 0) return; + // scroll-behavior not set to smooth? Bail out, let browser handle it + if (!shouldSmoothscroll()) return; + + // Check the DOM from the click target upwards if a local anchor was clicked + var anchor = findInParents(getEventTarget(evt), isAnchorToLocalElement); + if (!anchor) return; + + // Find the element targeted by the hash + var hash = anchor.hash; + var target = getScrollTarget(hash); + + if (target) { + // Prevent default browser behavior to avoid a jump to the anchor target + evt.preventDefault(); + + // Trigger the smooth scroll + triggerSmoothscroll(target); + + // 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 + // the scroll pos after a hashchange changed it, so we track it constantly. + // Some browsers don't trigger a scroll event before the hashchange, + // so to undo, the position from last scroll is the one we need to go back to. + // In others (e.g. IE) the scroll listener is triggered again before the + // hashchange occurs and the last reported position is already the new one + // updated by the hashchange – we need the second last to undo there. + // Because of this we don't track just the last, but the last two positions. + var lastTwoScrollPos = []; + + /** + * Tries to undo the automatic, instant scroll caused by a hashchange + * 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; + + var target = getScrollTarget(location.hash); + if (!target) return; + + // If the position last reported by the scroll listener is the same as the + // current one caused by a hashchange, go back to second last – else last + var currentPos = getScrollTop(); + var top = lastTwoScrollPos[lastTwoScrollPos[1] === currentPos ? 0 : 1]; + + // Undo the scroll caused by the hashchange... + w.scroll({ top: top, behavior: 'instant' }); + // ...and instead smoothscroll to the target + triggerSmoothscroll(target); + } + + /** + * Returns the scroll offset towards the top + */ + function getScrollTop() { + return docEl.scrollTop || d.body.scrollTop; + } + + /** + * Update the last two scroll positions + */ + function trackScrollPositions() { + if (!d.body) return; // Body not parsed yet? Abort + 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); + } + }; }); diff --git a/test.js b/test.js index 0a6cb7b..2b19f90 100755 --- a/test.js +++ b/test.js @@ -40,7 +40,7 @@ describe('General', () => { }) - it('Runs even in browsers with native support if force flag is set', () => { + it('Runs even with native support if force flag is set on window', () => { const anchor = insertElement('a', { href: '#' }) // Mock native support @@ -51,6 +51,20 @@ describe('General', () => { const spy = jest.spyOn(window, 'scroll') polyfill() + anchor.click() + expect(spy).toHaveBeenCalled() + }) + + it('Runs even with native support if force flag is passed as arg', () => { + const anchor = insertElement('a', { href: '#' }) + + // Mock native support + document.documentElement.style.scrollBehavior = 'smooth' + + const spy = jest.spyOn(window, 'scroll') + // Pass force flag in options object + polyfill({ force: true }) + anchor.click() expect(spy).toHaveBeenCalled() }) From 0f4248bd4cc6b540547efa3a6a8f31f2c5fef76e Mon Sep 17 00:00:00 2001 From: Jonas Kuske <30421456+jonaskuske@users.noreply.github.com> Date: Sat, 8 Dec 2018 02:09:06 +0100 Subject: [PATCH 3/8] docs: rewrite docs for v1.0.0, move to docs/ fix #9, fix #10 --- .gitattributes | 2 - CHANGELOG.md | 16 +- favicon.ico => docs/favicon.ico | Bin index.css => docs/index.css | 46 +++++- index.html => docs/index.html | 267 ++++++++++++++++++++++++-------- 5 files changed, 257 insertions(+), 74 deletions(-) delete mode 100755 .gitattributes rename favicon.ico => docs/favicon.ico (100%) rename index.css => docs/index.css (82%) rename index.html => docs/index.html (58%) diff --git a/.gitattributes b/.gitattributes deleted file mode 100755 index b383ed1..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -index.html linguist-documentation -index.css linguist-documentation \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e7e44..9a3889a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,23 @@ 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.0.0] - 2018-12-07 +### Added + - The methods 'destroy' and 'polyfill' are now exported (CommonJS) or exposed on window.SmoothscrollAnchorPolyfill. The polyfill still runs automatically on load so embedding it is enough, but now you can destroy it if you want (EventListeners are removed) and start it again, later. + - In addition to `window.__forceSmoothscrollAnchorPolyfill__`, you can now pass `{ force: true }` when invoking `polyfill()` to force-enable the package even in browsers with native support +### Changed + - Updated the documentation website to reflect the new API + - Moved the documentation in a separate docs/ folder to clean up the repo + - Small fixes for formatting and typos in the README + ## [1.0.0-beta] - 2018-12-05 ### Changed - The README.md file has been updated to match the API of v1.0.0 - - Fixed 'window is not defined' error in Node environments, important for usage with SSR - BREAKING: Polyfill now only handles smooth scroll if scroll-behavior is set to 'smooth' via <html style="">, documentElement.style.scrollBehavior or a custom font-family (more information will be added to the documentation) ### Added - Tests for smooth scrolling when clicking anchors have been implemented +### Fixed + - Fixed 'window is not defined' error in Node environments, important for usage with SSR ## [0.12.0] - 2018-11-15 ### Added @@ -20,12 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - When navigating to an anchor, the anchor is now focused. - In browsers supporting the optional `preventScroll` argument, the anchor is focused immediately and the focus scroll is prevented by passing this argument. - If the browser doesn't support `preventScroll` (e.g. Internet Explorer), the focus is scheduled to happen 450ms after the smooth scroll started so it does not interfere with the smooth scrolling (which caused flickering). - - ### Changed - The flag to enforce the polyfill (even if the browser has native support) is now called (`window.__forceSmoothscrollAnchorPolyfill__`). The docs have been updated to reflect this. - - ### Fixed - The polyfill now properly handles Shift/Meta keys and allows for opening links in new windows by shift-clicking instead of preventing it with `event.preventDefault()` - The docs website now works in Internet Explorer 9, polyfills for `Element.classList`, `requestAnimationFrame` + an alternative for flexbox layouts have been added diff --git a/favicon.ico b/docs/favicon.ico similarity index 100% rename from favicon.ico rename to docs/favicon.ico diff --git a/index.css b/docs/index.css similarity index 82% rename from index.css rename to docs/index.css index 248409d..12834ca 100755 --- a/index.css +++ b/docs/index.css @@ -1,9 +1,3 @@ -html { - scroll-behavior: smooth; - /* Additionally specified in custom font-family so polyfill can parse it */ - font-family: 'scroll-behavior:smooth'; -} - body { margin: 0; font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif; @@ -66,6 +60,42 @@ section, .fullscreen { padding-top: 11rem; } +.smooth-scroll { + scroll-behavior: smooth; + /* Additionally specified in custom font-family so polyfill can parse it */ + font-family: 'scroll-behavior:smooth'; +} + +.fullscreen h1 { + margin-top: 7rem; +} + +.scroll-btn { + margin-top: 7rem; +} + +@media screen and (min-height: 660px) { + .fullscreen h1 { + margin-top: 12rem; + } + .scroll-btn { + margin-top: 12rem; + } +} + +@media screen and (min-height: 900px) { + .fullscreen h1 { + margin-top: 24rem; + } + .scroll-btn { + margin-top: 24rem; + } +} + +html:not(.smooth-scroll) .scroll-btn { + font-weight: lighter; +} + .max-width { max-width: 100%; } @@ -192,4 +222,8 @@ h1[id]:hover, h2[id]:hover, h3[id]:hover, h4[id]:hover, h1[data-to]:hover, h2[da h1[id]:hover::before, h2[id]:hover::before, h3[id]:hover::before, h4[id]:hover::before, h1[data-to]:hover::before, h2[data-to]:hover::before, h3[data-to]:hover::before, h4[data-to]:hover::before { opacity: 0.5; +} + +.allow-overflow { + overflow: auto; } \ No newline at end of file diff --git a/index.html b/docs/index.html similarity index 58% rename from index.html rename to docs/index.html index b884fb9..5a23173 100755 --- a/index.html +++ b/docs/index.html @@ -1,5 +1,5 @@ - + @@ -13,10 +13,10 @@ // 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) })(); - - + + - + @@ -49,18 +49,21 @@

smoothscroll-anchor-polyfill

⚓ Apply smooth scroll to anchor links to replicate CSS scroll-behavior

- GitHub +
GitHub
+

Start

-

The Scroll Behavior specification allows for native smooth +

The Scroll + Behavior specification allows for + native smooth scrolling in browsers – both by using JavaScript scroll APIs like window.scrollTo and - element.scrollIntoView or by simply setting the + Element.scrollIntoView or by simply setting the property scroll-behavior to smooth in CSS, which will then make any scrolling smooth by default. This includes @@ -87,57 +90,99 @@

Start

Usage

-

Requirements

-

Since this script uses the JavaScript scroll APIs and relies on +

⚠ Since this script uses the JavaScript scroll APIs and relies on their smooth scroll functionality to operate, you'll need a polyfill for the Scroll Behavior spec in order for this script to make a difference. smoothscroll-polyfill is used as example throughout this site, but you may just as well use another polyfill – or write your own implementation.

-

⚠ Furthermore, this script assumes that scroll-behavior - is set globally to smooth in your CSS. If you don't do that, - scroll behavior will differ between browsers with and without - native support for Scroll Behavior. Set it as follows:

-
<style>
-html {
-  scroll-behavior: smooth;
-}
-</style>
+        

1. Setting scroll-behavior in CSS

+

Because browsers don't parse CSS properties they don't recognize. + For this reason, reading the scroll-behavior property + from your regular stylesheets is unfortunately not possible (without + a performance hit). Instead, specify scroll-behavior + using one of these options:

+

Option 1: Using the inline style attribute

+

Simply define scroll-behaviour as an inline style on + the html element:

+
<html style="scroll-behavior: smooth;">
+...
+</html>
 
+

This way, the polyfill can read the + property using getAttribute('style') even if the browser + doesn't parse it.

+ +

Option 2: Using font-family as + workaround

+

Alternatively, you can specify the property as the name of a custom + font family: +

<style>
+  html {
+    /* Normal CSS properties for browsers with native support */
+    scroll-behavior: smooth;
+
+    /* Additionally defined as the name of a font, so the polyfill can read it */
+    font-family: "scroll-behavior: smooth", sans-serif;
+  }
+<style>
+ Your actual fonts will still work the way they should – plus, you can + simply declare actual fonts on body { } and use font + styles on html { } exclusively for the means of this + polyfill, which is prefered. Unlike inline styles, this allows you to + use normal CSS features like media queries or classes. The following + only enables smooth scroll on desktop devices, for example:

+
<style>
+  html {
+    scroll-behavior: auto;
+    font-family: "scroll-behavior: auto";
+  }
+  @media screen and (min-width: 1150px) {
+    html {
+      scroll-behavior: smooth;
+      font-family: "scroll-behavior: smooth";
+    }
+  }
+<style>
+

💡 Redeclaring your scroll-behavior properties as font + names can be automated using a PostCSS + plugin, so you can write regular CSS and don't have to + bother with font-families. It just works™

-

Basic usage

+

2. Installing the polyfill

+

Option 1: Using <script>

Simply drop in <script> tags linking to the - polyfills and and you're good to go.

+ polyfills and you're good to go.

-
<!-- A polyfill to enable smoothscroll for the JavaScript APIs -->
-<script src="https://unpkg.com/smoothscroll-polyfill"></script>
+        
<!-- Any polyfill to enable smoothscroll for the JavaScript APIs -->
+<script src="https://unpkg.com/smoothscroll-polyfill/dist/smoothscroll.min.js"></script>
 
 <!-- This package, to apply the smoothscroll to anchor links -->
 <script src="https://unpkg.com/smoothscroll-anchor-polyfill"></script>
 
-

With npm

-

Alternatively, if you're using npm, you can - install using - npm install smoothscroll-anchor-polyfill and then use - the polyfill by - requiring/importing it in your JS.

- -
// Import any polyfill to enable smoothscroll for JS APIs
+        

Option 2: With npm

+

Alternatively, if you're using npm, you can + install using npm install smoothscroll-anchor-polyfill + and then use the polyfill by requiring/importing it in your JS.

+ +
// Import any polyfill to enable smoothscroll for JS APIs
 import smoothscrollPolyfill from 'smoothscroll-polyfill';
+
 // Import this package to apply the smoothscroll to anchor links
-import 'smoothscroll-anchor-polyfill';
+import smoothscrollAnchorPolyfill from 'smoothscroll-anchor-polyfill';
 
-// Enable the main smoothscroll polyfill (might not be neccessary if you use an alternative one)
+// (Unlike this package, smoothscroll-polyfill needs to be actively invoked: )
 smoothscrollPolyfill.polyfill();
 
-

Advanced (with Code Splitting)

-

If you're using a build system with support for code splitting like - Parcel or Webpack, - you can use dynamic imports to load the polyfills – this way, - browsers won't even download the polyfill code if they already have - support for the Scroll Behavior spec natively:

- -
// Only continue if polyfills are actually needed
+          

Advanced installation (with Code Splitting)

+

If you're using a build system with support for code splitting + like + Parcel or Webpack, + you can use dynamic imports to load the polyfills – this way, + browsers won't even download the polyfill code if they already have + support for the Scroll Behavior spec natively:

+ +
// Only continue if polyfills are actually needed
 if (!('scrollBehavior') in document.documentElement.style) {
 
   // Wait until the Polyfills are loaded
@@ -147,7 +192,7 @@ 

Advanced (with Code Splitting)

]) // then use the modules however you want .then(([smoothscrollPolyfill, smoothscrollAnchorPolyfill]) => { - // e.g. enable the main smoothscroll polyfill (might not be neccessary if you use an alternative one) + // (Unlike this package, smoothscroll-polyfill needs to be actively invoked: ) smoothscroll.polyfill(); }); } @@ -156,15 +201,31 @@

Advanced (with Code Splitting)

Docs

-

There's not much more to it than loading this polyfill – it will +

For 90% of use cases, there should not be much more to it than + loading this polyfill – it will execute immediately no matter if loaded through a script tag or in a CommonJS environment. If the Scroll Behavior spec is supported natively, the code won't do anything.

-

⚠ Remember to also set the scroll-behavior property - globally (on html) - in your CSS, otherwise your site will behave differently in browsers - with and without support for the Scroll Behavior spec! ⚠

-

Use the Polyfill even if there is native +

Changing the scroll behavior

+

The prefered way to dynamically adjust the scroll behavior is the font-family workaround. This way you can + simply toggle a CSS class on <html> depending on the + behavior you want. Valid property values are smooth for + enabling smooth scroll and auto, initial, + inherit or unset for enabling instant, + jumping scroll.

+
+

You can also assign these values directly to document.documentElement.style.scrollBehavior, + it will have precedence over both the inline style attribute and + the + property set using the font-family workaround.

+
+

+ ⚠ Assigning to .scrollBehavior is not recommened + however as this property is used for feature detection. Assigning to + it before a polyfill was loaded will break this one and most other + polyfills related to smooth scrolling. ⚠

+

Using the polyfill even if there is native support

@@ -176,21 +237,104 @@

Use the Polyfill even if there is native native smooth scroll. Not recommended.

-

Polyfill anchors dynamically inserted later

+

Methods: destroy and polyfill

+
+

This package exports two methods, destroy and polyfill. + If loaded through a script tag, these methods are exposed on + window.smoothscrollAnchorPolyfill.

+
+
+
+ destroy(): +
+
+

The polyfill runs automatically when it's loaded, setting up + the EventListeners it needs. This method disables the + polyfill and removes all EventListeners.

+
+
+
+
+ polyfill({ force: boolean }): +
+
+

If you used destroy() to disable the polyfill, + you can re-enable it with this method. It takes an (optional) + Object as argument, the property force behaves + the same way as the global force flag + on window.

+
+
+

⚠ Note that both the global force flag + and the + check for native + support ('scrollBehavior' in document.documentElement.style) + will be re-evaluated when polyfill() runs. If you + assigned to .scrollBehavior in the + meantime, this check will evaluate to true and the + polyfill won't enable itself. + Use the force flag or run delete document.documentElement.style.scrollBehavior; + if you encounter this problem.

+

Limitations

+

scroll-behavior is not + detected in regular + stylesheets

+

As already explained in the Usage section, + scroll-behavior + can not be set in regular CSS, as accessing the property there + from JavaScript is not possible without a performance hit. This + is caused by browsers not parsing a CSS property if it isn't + recognized as valid. If you need the flexiblity of CSS, consider + the font-family + workaround.

+

scroll-behavior is only supported + as global setting

+

In browsers with native support, you can define scroll-behavior + at multiple points in your document, e.g. auto on + the document itself, but smooth on a slideshow + container that has separate scrolling. This polyfill does not + allow for that, either all anchors on the page scroll smoothly by + setting scroll-behavior + at document level, or none.

+ +

This actually doesn't have anything to do with this polyfill – + it's + a limitation of Blink's native implementation (so it affects + other Blink-based browsers like Opera, too). While 'normal' + scrolling is smooth, if you click a couple of links and then + navigate back and forth using the browser's forwards/backwards + buttons (which triggers a hashchange + everytime), it jumps from anchor to anchor instead of scrolling + smoothly. If this is important to you, you can fix it by + detecting the Blink engine and force-enabling this polyfill. Load + browsengine.js, + then do (before the polyfill runs):

+
if (window.webpage.engine.blink) {
+  window.__forceSmoothscrollAnchorPolyfill__ = true;
+}
+

FAQ

+

Will this break Server Side Rendering?

+

No.

+

Polyfill anchors dynamically inserted + later

The polyfill uses Event Delegation to detect clicks, so even if an anchor is added to the page after the polyfill was loaded, everything should work.

-

Disable the polyfill

-

There currently is no way to disable the polyfill once it was - loaded. I considered adding this functionality but couldn't think of - a good use case – if you have one, let me know!

-

💡 Note that even if global scroll-behavior is set and - this polyfill is loaded, you can still perform instant, jumping - navigation by using one of the JavaScript scroll APIs and specifying - { behavior: 'instant' } in the options object.

-

Will this break Server Side Rendering?

-

No.

+

Does this support prefers-reduced-motion?

+

prefers-reduced-motion is a relatively new CSS media + query that hints at whether a client prefers less motion, which can + be important for people with certain illnesses. But Safari is the + only browser that has implemented it yet, and it doesn't support the + Scroll Behavior spec, so there is no reference for the interplay of + prefers-reduced-motion and Scroll Behavior yet. For this + reason, it is not implemented (yet) in this polyfill.
+ However, it is relatively safe to assume that prefers-reduced-motion + will disable scroll-behavior: smooth so this can + absolutely be discussed – please file an issue on GitHub if it + affects your project.

@@ -333,7 +477,8 @@

Legal

Note: the legalities discussed here concern this website itself, not the software package "smoothscroll-anchor-polyfill", and are required for compliance with German & European law. "smoothscroll-anchor-polyfill" - itself is licensed under MIT license, for more information + itself is licensed under a plain MIT license, for more + information check out the respective GitHub repository.

Imprint

@@ -418,9 +563,9 @@

Privacy

From ba7fe10a2a0ba7c0e9d16ed05297a924dd49b52b Mon Sep 17 00:00:00 2001 From: Jonas Kuske <30421456+jonaskuske@users.noreply.github.com> Date: Sat, 8 Dec 2018 02:51:36 +0100 Subject: [PATCH 4/8] docs: prevent elements from disappearing behind header when scrolled to added an invisible padding to targetable elements [id], so they are pushed down and don't disappear behind the header when scrolled to --- docs/index.css | 57 ++++++++++++++++++++++++--------- docs/index.html | 83 +++++++++++++++++++++++++------------------------ 2 files changed, 85 insertions(+), 55 deletions(-) diff --git a/docs/index.css b/docs/index.css index 12834ca..62223d1 100755 --- a/docs/index.css +++ b/docs/index.css @@ -57,7 +57,7 @@ header, section, .fullscreen { } section, .fullscreen { - padding-top: 11rem; + padding-top: 5rem; } .smooth-scroll { @@ -67,7 +67,7 @@ section, .fullscreen { } .fullscreen h1 { - margin-top: 7rem; + margin-top: 13rem; } .scroll-btn { @@ -76,7 +76,7 @@ section, .fullscreen { @media screen and (min-height: 660px) { .fullscreen h1 { - margin-top: 12rem; + margin-top: 18rem; } .scroll-btn { margin-top: 12rem; @@ -85,7 +85,7 @@ section, .fullscreen { @media screen and (min-height: 900px) { .fullscreen h1 { - margin-top: 24rem; + margin-top: 30rem; } .scroll-btn { margin-top: 24rem; @@ -130,19 +130,19 @@ html:not(.flex-center) .fullscreen>div { margin-bottom: 0; } -#start { +section:nth-child(2) { background: #faf7fc; } -#usage { +section:nth-child(3) { background: #f6effa; } -#docs { +section:nth-child(4) { background: #f1e7f7; } -#legal, footer>div { +section:nth-child(5), footer>div { background: #eddff5; } @@ -200,30 +200,59 @@ footer>div.row p { } } -h1[id], h2[id], h3[id], h4[id], h1[data-to], h2[data-to], h3[data-to], h4[data-to] { +h1[id] span, h2[id] span, h3[id] span, h4[id] span { position: relative; transition: padding 200ms ease-out; cursor: pointer; } -h1[id]::before, h2[id]::before, h3[id]::before, h4[id]::before, h1[data-to]::before, h2[data-to]::before, h3[data-to]::before, h4[data-to]::before { +h1[id] span::before, h2[id] span::before, h3[id] span::before, h4[id] span::before { content: "#"; position: absolute; left: 0; - top: 50%; - transform: translate(-50%, -50%); + bottom: 0%; + transform: translateX(-50%); opacity: 0; transition: opacity 0.3s ease-out; } -h1[id]:hover, h2[id]:hover, h3[id]:hover, h4[id]:hover, h1[data-to]:hover, h2[data-to]:hover, h3[data-to]:hover, h4[data-to]:hover { +h1[id] span:hover, h2[id] span:hover, h3[id] span:hover, h4[id] span:hover { padding-left: 2rem; } -h1[id]:hover::before, h2[id]:hover::before, h3[id]:hover::before, h4[id]:hover::before, h1[data-to]:hover::before, h2[data-to]:hover::before, h3[data-to]:hover::before, h4[data-to]:hover::before { +h1[id] span:hover::before, h2[id] span:hover::before, h3[id] span:hover::before, h4[id] span:hover::before { opacity: 0.5; } .allow-overflow { overflow: auto; +} + +/* Add invisible padding to top of targetable elements, +so they're not hidden under the header when scrolled to top */ + +[id] { + padding-top: 90px; + margin-top: -90px; +} + +@media screen and (max-width: 785px) { + [id] { + padding-top: 110px; + margin-top: -110px; + } +} + +@media screen and (max-width: 545px) { + [id] { + padding-top: 145px; + margin-top: -145px; + } +} + +@media screen and (max-width: 425px) { + [id] { + padding-top: 175px; + margin-top: -175px; + } } \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 5a23173..e0bea64 100755 --- a/docs/index.html +++ b/docs/index.html @@ -46,7 +46,7 @@
-

smoothscroll-anchor-polyfill

+

smoothscroll-anchor-polyfill

⚓ Apply smooth scroll to anchor links to replicate CSS scroll-behavior

@@ -54,8 +54,8 @@

smoothscroll-anchor-polyfill

-
-

Start

+
+

Start

The Scroll Behavior specification allows for native smooth @@ -88,21 +88,21 @@

Start

-
-

Usage

+
+

Usage

⚠ Since this script uses the JavaScript scroll APIs and relies on their smooth scroll functionality to operate, you'll need a polyfill for the Scroll Behavior spec in order for this script to make a difference. smoothscroll-polyfill is used as example throughout this site, but you may just as well use another polyfill – or write your own implementation.

-

1. Setting scroll-behavior in CSS

+

1. Setting scroll-behavior in CSS

Because browsers don't parse CSS properties they don't recognize. For this reason, reading the scroll-behavior property from your regular stylesheets is unfortunately not possible (without a performance hit). Instead, specify scroll-behavior using one of these options:

-

Option 1: Using the inline style attribute

+

Option 1: Using the inline style attribute

Simply define scroll-behaviour as an inline style on the html element:

<html style="scroll-behavior: smooth;">
@@ -113,8 +113,8 @@ 

Option 1: Using the inline style attribute

property using getAttribute('style') even if the browser doesn't parse it.

-

Option 2: Using font-family as - workaround

+

Option 2: Using font-family as + workaround

Alternatively, you can specify the property as the name of a custom font family:

<style>
@@ -149,8 +149,8 @@ 

Option 2: Using font-family as plugin, so you can write regular CSS and don't have to bother with font-families. It just works™

-

2. Installing the polyfill

-

Option 1: Using <script>

+

2. Installing the polyfill

+

Option 1: Using <script>

Simply drop in <script> tags linking to the polyfills and you're good to go.

@@ -160,7 +160,7 @@

Option 1: Using <script>

<!-- This package, to apply the smoothscroll to anchor links --> <script src="https://unpkg.com/smoothscroll-anchor-polyfill"></script>
-

Option 2: With npm

+

Option 2: With npm

Alternatively, if you're using npm, you can install using npm install smoothscroll-anchor-polyfill and then use the polyfill by requiring/importing it in your JS.

@@ -174,7 +174,8 @@

Option 2: With npm

// (Unlike this package, smoothscroll-polyfill needs to be actively invoked: ) smoothscrollPolyfill.polyfill();
-

Advanced installation (with Code Splitting)

+

Advanced installation (with Code + Splitting)

If you're using a build system with support for code splitting like Parcel or Webpack, @@ -199,14 +200,14 @@

Advanced installation (with Code Splitting)

-
-

Docs

+
+

Docs

For 90% of use cases, there should not be much more to it than loading this polyfill – it will execute immediately no matter if loaded through a script tag or in a CommonJS environment. If the Scroll Behavior spec is supported natively, the code won't do anything.

-

Changing the scroll behavior

+

Changing the scroll behavior

The prefered way to dynamically adjust the scroll behavior is the font-family workaround. This way you can simply toggle a CSS class on <html> depending on the @@ -225,8 +226,8 @@

Changing the scroll behavior

however as this property is used for feature detection. Assigning to it before a polyfill was loaded will break this one and most other polyfills related to smooth scrolling. ⚠

-

Using the polyfill even if there is native - support

+

Using the polyfill even if there is native + support

window.__forceSmoothscrollAnchorPolyfill__: @@ -237,7 +238,7 @@

Using the polyfill even if there is native native smooth scroll. Not recommended.

-

Methods: destroy and polyfill

+

Methods: destroy and polyfill

This package exports two methods, destroy and polyfill. If loaded through a script tag, these methods are exposed on @@ -275,10 +276,11 @@

Methods: destroy and polyfill

polyfill won't enable itself. Use the force flag or run delete document.documentElement.style.scrollBehavior; if you encounter this problem.

-

Limitations

-

scroll-behavior is not - detected in regular - stylesheets

+

Limitations

+

scroll-behavior is + not + detected in regular + stylesheets

As already explained in the Usage section, scroll-behavior can not be set in regular CSS, as accessing the property there @@ -287,8 +289,9 @@

scroll-behavior is not recognized as valid. If you need the flexiblity of CSS, consider the font-family workaround.

-

scroll-behavior is only supported - as global setting

+

scroll-behavior is only + supported + as global setting

In browsers with native support, you can define scroll-behavior at multiple points in your document, e.g. auto on the document itself, but smooth on a slideshow @@ -296,12 +299,10 @@

scroll-behavior is only supported allow for that, either all anchors on the page scroll smoothly by setting scroll-behavior at document level, or none.

-

+

This actually doesn't have anything to do with this polyfill – - it's - a limitation of Blink's native implementation (so it affects + it's a limitation of Blink's native implementation (so it affects other Blink-based browsers like Opera, too). While 'normal' scrolling is smooth, if you click a couple of links and then navigate back and forth using the browser's forwards/backwards @@ -314,16 +315,16 @@

FAQ

-

Will this break Server Side Rendering?

+

FAQ

+

Will this break Server Side Rendering?

No.

-

Polyfill anchors dynamically inserted - later

+

Polyfill anchors dynamically inserted + later

The polyfill uses Event Delegation to detect clicks, so even if an anchor is added to the page after the polyfill was loaded, everything should work.

-

Does this support prefers-reduced-motion?

+

Does this support prefers-reduced-motion?

prefers-reduced-motion is a relatively new CSS media query that hints at whether a client prefers less motion, which can be important for people with certain illnesses. But Safari is the @@ -338,11 +339,11 @@

Does this support prefers-reduced-motion? -