bs_lib-8.x-1.0-alpha3/js/anchor-scroll.js
js/anchor-scroll.js
/**
* @file
*
* Custom scrolling to anchor target functionality.
*/
(function (drupalSettings, once) {
'use strict';
// Do not continue if browser do not support window.scrollY - IE family.
if (!window.hasOwnProperty('scrollY')) {
return;
}
window.BSLib = window.BSLib ? window.BSLib : {};
/**
* Calculate fixed elements offset.
*
* @return {integer}
* Offset of all fixed elements taking into consideration overlapping.
*/
window.BSLib.getFixedElementsOffset = function () {
var options = drupalSettings.bs_lib.anchor_scroll;
// We will calculate offset distance by taking all vertical points of fixed
// elements, sorting them and then summing offset of first point with
// distances of all points - this will also take into consideration any
// possible overlapping of fixed elements.
// Get all vertical points of fixed elements.
var points = [];
for (var i = 0; i < options.fixed_elements.length; ++i) {
// Lines can be empty which we need to skip.
if (options.fixed_elements[i].trim().length !== 0) {
var fixedElement = document.querySelector(options.fixed_elements[i]);
if (fixedElement) {
var boundingRect = fixedElement.getBoundingClientRect();
points.push(boundingRect.top, boundingRect.bottom);
}
}
}
// Final offset sum, we start by adding custom offset.
var fixedElementsOffset = options.offset;
// Add elements offsets.
if (points.length > 0) {
// Sort points from smallest to biggest.
points.sort(function (a, b) { return a - b; });
// Add offset of the most top element point.
fixedElementsOffset += points[0];
// Add distances between the rest of the points.
for (i = 1; i < points.length; ++i) {
fixedElementsOffset += points[i] - points[i - 1];
}
}
return fixedElementsOffset;
};
/**
* Process click on a link anchor element.
*
* @param event
* @param element
*/
window.BSLib.processClick = function(event, element) {
var targetElement = document.getElementById(element.href.split('#')[1]);
if (targetElement) {
// Prevent further processing to disable browser jumping to anchor.
event.preventDefault();
// Scroll to anchor.
scrollToElement(targetElement);
// Because we disabled browser jumping to anchor we need to manually
// update browser location with a new hash.
// We use replace instead of push because two phase scroll jumping is
// messing with history scrollRestoration - it will return only to the
// first fast scroll target.
// @todo using of pushState probably makes more sense, can we fix
// scrollRestoration problem somehow? Maybe we could use popstate event
// https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event.
history.replaceState(null, null, element.href);
}
};
/**
* Dispatch event helper.
*
* @param {string} name
* Event name.
*/
var dispatchEvent = function (name) {
const event = document.createEvent('Event');
event.initEvent('bslib.anchorscroll.' + name, true, true);
window.dispatchEvent(event);
}
/**
* Calculate element offset.
*
* @param element
* @return {number}
*/
var calculateScrollOffset = function (element) {
return element.getBoundingClientRect().top - window.BSLib.getFixedElementsOffset();
};
/**
* Focus element while preventing browser scrolling to that element.
*
* @param element
*/
var focusElement = function (element) {
// Focus the target element, important for accessibility.
element.focus({preventScroll: true});
if (document.activeElement !== element) {
// If element is not focusable set tabindex first so we can focus it.
element.setAttribute('tabindex','-1');
element.focus({preventScroll: true});
}
}
/**
* Called when first scroll part is finished.
*
* @param element
*/
var firstScrollEnded = function (element) {
dispatchEvent('firstscrollended');
var timer;
window.addEventListener('scroll', function(e) {
window.requestAnimationFrame(function () {
timer && clearTimeout(timer);
timer = setTimeout(secondScrollEnded.bind(null, element), 50);
});
}, {once: true});
window.scrollTo({
top: window.scrollY + calculateScrollOffset(element),
behavior: 'smooth'
});
}
/**
* Called when second scroll part is ended.
*
* @param element
*/
var secondScrollEnded = function (element) {
dispatchEvent('secondscrollended');
focusElement(element);
}
/**
* Scroll to element.
*
* @param element
* Element object to scroll.
*/
function scrollToElement(element) {
// We are doing scrolling in two phases first and second. The reason for
// this is because fixed sticky elements can change it dimension and
// position while scrolling. This is the reason we will do 2/3 of the
// needed scrolling in first phase, recalculate scroll positions, and then
// do the rest of the scrolling in second phase.
var offset = calculateScrollOffset(element);
// Only scroll if offset is big enough.
if (Math.abs(offset) > 1) {
// First scroll to 2/3 of the element fast.
// Wait a bit for any elements to change.
// Scroll last 1/3 to the element smooth.
var timer;
window.addEventListener('scroll', function(e) {
window.requestAnimationFrame(function () {
timer && clearTimeout(timer);
timer = setTimeout(firstScrollEnded.bind(null, element), 10);
});
}, {once: true});
// Scroll to element.
window.scrollTo({
top: window.scrollY + 2/3*offset,
});
}
}
/**
* Process in page anchor links and attach smooth scrolling to them.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behaviors for in page anchor links.
*/
Drupal.behaviors.BSLibAnchorScroll = {
attach: function (context) {
var excludeLinks = '';
for (var i = 0; i < drupalSettings.bs_lib.anchor_scroll.exclude_links.length; ++i) {
excludeLinks += ':not(' + drupalSettings.bs_lib.anchor_scroll.exclude_links[i] + ')';
}
// Apply to every link with a hash. Originally only for href^="#" and
// href^="/#" but that does not apply to link not on the frontpage, and
// on multilingual websites. There is a check for empty # links in
// processClick.
once('bs-lib-anchor-smooth-scroll', 'a[href*="#"]' + excludeLinks, context).forEach((element) => {
element.addEventListener('click', (e) => {
BSLib.processClick(e, element);
});
});
}
};
// If browser location has a hash and element with that id or name exist on
// content automatically scroll to that element.
if (location.hash) {
// Query for hash element when DOM is loaded.
window.addEventListener('DOMContentLoaded', function() {
var hash = location.hash;
var targetElement = document.getElementById(hash.slice(1));
// Element which offsetParent is null is not displayed or it is fixed - we
// do not scroll to that element.
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
if (targetElement && targetElement.offsetParent != null) {
// Reset hash to prevent native browser jumping to hash element.
location.hash = '';
// Trigger scroll on window load event - we need to wait native browser
// jumping and do custom scroll after browser jump and that will happen
// on load event, and not on DomContentLoaded.
window.addEventListener('load', function () {
// Restore hash part in URL without browser scrolling.
history.replaceState(null, null, location.href + hash.slice(1));
// Scroll to element.
scrollToElement(targetElement);
});
}
});
}
}(drupalSettings, once));
