277 lines
8.0 KiB
JavaScript
Executable File
277 lines
8.0 KiB
JavaScript
Executable File
var COMPLETE = 'complete',
|
|
CANCELED = 'canceled';
|
|
|
|
function raf(task){
|
|
if('requestAnimationFrame' in window){
|
|
return window.requestAnimationFrame(task);
|
|
}
|
|
|
|
setTimeout(task, 16);
|
|
}
|
|
|
|
function setElementScroll(element, x, y){
|
|
Math.max(0, x);
|
|
Math.max(0, y);
|
|
|
|
if(element.self === element){
|
|
element.scrollTo(x, y);
|
|
}else{
|
|
element.scrollLeft = x;
|
|
element.scrollTop = y;
|
|
}
|
|
}
|
|
|
|
function getTargetScrollLocation(scrollSettings, parent){
|
|
var align = scrollSettings.align,
|
|
target = scrollSettings.target,
|
|
targetPosition = target.getBoundingClientRect(),
|
|
parentPosition,
|
|
x,
|
|
y,
|
|
differenceX,
|
|
differenceY,
|
|
targetWidth,
|
|
targetHeight,
|
|
leftAlign = align && align.left != null ? align.left : 0.5,
|
|
topAlign = align && align.top != null ? align.top : 0.5,
|
|
leftOffset = align && align.leftOffset != null ? align.leftOffset : 0,
|
|
topOffset = align && align.topOffset != null ? align.topOffset : 0,
|
|
leftScalar = leftAlign,
|
|
topScalar = topAlign;
|
|
|
|
if(scrollSettings.isWindow(parent)){
|
|
targetWidth = Math.min(targetPosition.width, parent.innerWidth);
|
|
targetHeight = Math.min(targetPosition.height, parent.innerHeight);
|
|
x = targetPosition.left + parent.pageXOffset - parent.innerWidth * leftScalar + targetWidth * leftScalar;
|
|
y = targetPosition.top + parent.pageYOffset - parent.innerHeight * topScalar + targetHeight * topScalar;
|
|
x -= leftOffset;
|
|
y -= topOffset;
|
|
x = scrollSettings.align.lockX ? parent.pageXOffset : x;
|
|
y = scrollSettings.align.lockY ? parent.pageYOffset : y;
|
|
differenceX = x - parent.pageXOffset;
|
|
differenceY = y - parent.pageYOffset;
|
|
}else{
|
|
targetWidth = targetPosition.width;
|
|
targetHeight = targetPosition.height;
|
|
parentPosition = parent.getBoundingClientRect();
|
|
var offsetLeft = targetPosition.left - (parentPosition.left - parent.scrollLeft);
|
|
var offsetTop = targetPosition.top - (parentPosition.top - parent.scrollTop);
|
|
x = offsetLeft + (targetWidth * leftScalar) - parent.clientWidth * leftScalar;
|
|
y = offsetTop + (targetHeight * topScalar) - parent.clientHeight * topScalar;
|
|
x -= leftOffset;
|
|
y -= topOffset;
|
|
x = Math.max(Math.min(x, parent.scrollWidth - parent.clientWidth), 0);
|
|
y = Math.max(Math.min(y, parent.scrollHeight - parent.clientHeight), 0);
|
|
x = scrollSettings.align.lockX ? parent.scrollLeft : x;
|
|
y = scrollSettings.align.lockY ? parent.scrollTop : y;
|
|
differenceX = x - parent.scrollLeft;
|
|
differenceY = y - parent.scrollTop;
|
|
}
|
|
|
|
return {
|
|
x: x,
|
|
y: y,
|
|
differenceX: differenceX,
|
|
differenceY: differenceY
|
|
};
|
|
}
|
|
|
|
function animate(parent){
|
|
var scrollSettings = parent._scrollSettings;
|
|
|
|
if(!scrollSettings){
|
|
return;
|
|
}
|
|
|
|
var maxSynchronousAlignments = scrollSettings.maxSynchronousAlignments;
|
|
|
|
var location = getTargetScrollLocation(scrollSettings, parent),
|
|
time = Date.now() - scrollSettings.startTime,
|
|
timeValue = Math.min(1 / scrollSettings.time * time, 1);
|
|
|
|
if(scrollSettings.endIterations >= maxSynchronousAlignments){
|
|
setElementScroll(parent, location.x, location.y);
|
|
parent._scrollSettings = null;
|
|
return scrollSettings.end(COMPLETE);
|
|
}
|
|
|
|
var easeValue = 1 - scrollSettings.ease(timeValue);
|
|
|
|
setElementScroll(parent,
|
|
location.x - location.differenceX * easeValue,
|
|
location.y - location.differenceY * easeValue
|
|
);
|
|
|
|
if(time >= scrollSettings.time){
|
|
scrollSettings.endIterations++;
|
|
// Align ancestor synchronously
|
|
scrollSettings.scrollAncestor && animate(scrollSettings.scrollAncestor);
|
|
animate(parent);
|
|
return;
|
|
}
|
|
|
|
raf(animate.bind(null, parent));
|
|
}
|
|
|
|
function defaultIsWindow(target){
|
|
return target.self === target
|
|
}
|
|
|
|
function transitionScrollTo(target, parent, settings, scrollAncestor, callback){
|
|
var idle = !parent._scrollSettings,
|
|
lastSettings = parent._scrollSettings,
|
|
now = Date.now(),
|
|
cancelHandler,
|
|
passiveOptions = { passive: true };
|
|
|
|
if(lastSettings){
|
|
lastSettings.end(CANCELED);
|
|
}
|
|
|
|
function end(endType){
|
|
parent._scrollSettings = null;
|
|
|
|
if(parent.parentElement && parent.parentElement._scrollSettings){
|
|
parent.parentElement._scrollSettings.end(endType);
|
|
}
|
|
|
|
if(settings.debug){
|
|
console.log('Scrolling ended with type', endType, 'for', parent)
|
|
}
|
|
|
|
callback(endType);
|
|
if(cancelHandler){
|
|
parent.removeEventListener('touchstart', cancelHandler, passiveOptions);
|
|
parent.removeEventListener('wheel', cancelHandler, passiveOptions);
|
|
}
|
|
}
|
|
|
|
var maxSynchronousAlignments = settings.maxSynchronousAlignments;
|
|
|
|
if(maxSynchronousAlignments == null){
|
|
maxSynchronousAlignments = 3;
|
|
}
|
|
|
|
parent._scrollSettings = {
|
|
startTime: now,
|
|
endIterations: 0,
|
|
target: target,
|
|
time: settings.time,
|
|
ease: settings.ease,
|
|
align: settings.align,
|
|
isWindow: settings.isWindow || defaultIsWindow,
|
|
maxSynchronousAlignments: maxSynchronousAlignments,
|
|
end: end,
|
|
scrollAncestor
|
|
};
|
|
|
|
if(!('cancellable' in settings) || settings.cancellable){
|
|
cancelHandler = end.bind(null, CANCELED);
|
|
parent.addEventListener('touchstart', cancelHandler, passiveOptions);
|
|
parent.addEventListener('wheel', cancelHandler, passiveOptions);
|
|
}
|
|
|
|
if(idle){
|
|
animate(parent);
|
|
}
|
|
|
|
return cancelHandler
|
|
}
|
|
|
|
function defaultIsScrollable(element){
|
|
return (
|
|
'pageXOffset' in element ||
|
|
(
|
|
element.scrollHeight !== element.clientHeight ||
|
|
element.scrollWidth !== element.clientWidth
|
|
) &&
|
|
getComputedStyle(element).overflow !== 'hidden'
|
|
);
|
|
}
|
|
|
|
function defaultValidTarget(){
|
|
return true;
|
|
}
|
|
|
|
function findParentElement(el){
|
|
if (el.assignedSlot) {
|
|
return findParentElement(el.assignedSlot);
|
|
}
|
|
|
|
if (el.parentElement) {
|
|
if(el.parentElement.tagName.toLowerCase() === 'body'){
|
|
return el.parentElement.ownerDocument.defaultView || el.parentElement.ownerDocument.ownerWindow;
|
|
}
|
|
return el.parentElement;
|
|
}
|
|
|
|
if (el.getRootNode){
|
|
var parent = el.getRootNode()
|
|
if(parent.nodeType === 11) {
|
|
return parent.host;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = function(target, settings, callback){
|
|
if(!target){
|
|
return;
|
|
}
|
|
|
|
if(typeof settings === 'function'){
|
|
callback = settings;
|
|
settings = null;
|
|
}
|
|
|
|
if(!settings){
|
|
settings = {};
|
|
}
|
|
|
|
settings.time = isNaN(settings.time) ? 1000 : settings.time;
|
|
settings.ease = settings.ease || function(v){return 1 - Math.pow(1 - v, v / 2);};
|
|
settings.align = settings.align || {};
|
|
|
|
var parent = findParentElement(target),
|
|
parents = 1;
|
|
|
|
function done(endType){
|
|
parents--;
|
|
if(!parents){
|
|
callback && callback(endType);
|
|
}
|
|
}
|
|
|
|
var validTarget = settings.validTarget || defaultValidTarget;
|
|
var isScrollable = settings.isScrollable;
|
|
|
|
if(settings.debug){
|
|
console.log('About to scroll to', target)
|
|
|
|
if(!parent){
|
|
console.error('Target did not have a parent, is it mounted in the DOM?')
|
|
}
|
|
}
|
|
|
|
var scrollingElements = [];
|
|
|
|
while(parent){
|
|
if(settings.debug){
|
|
console.log('Scrolling parent node', parent)
|
|
}
|
|
|
|
if(validTarget(parent, parents) && (isScrollable ? isScrollable(parent, defaultIsScrollable) : defaultIsScrollable(parent))){
|
|
parents++;
|
|
scrollingElements.push(parent);
|
|
}
|
|
|
|
parent = findParentElement(parent);
|
|
|
|
if(!parent){
|
|
done(COMPLETE)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return scrollingElements.reduce((cancel, parent, index) => transitionScrollTo(target, parent, settings, scrollingElements[index + 1], done), null);
|
|
};
|