Skip to content

Instantly share code, notes, and snippets.

@ivansky
Last active February 5, 2019 08:04
Show Gist options
  • Save ivansky/d8a51e0b2294f5a7939f44cd201de4b9 to your computer and use it in GitHub Desktop.
Save ivansky/d8a51e0b2294f5a7939f44cd201de4b9 to your computer and use it in GitHub Desktop.
type ScrollDirection = 'vertical' | 'horizontal' | 'both';
/**
* It prevents default behaviour above the scrollable areas
* until it's in available scrollable direction.
*
* Example of checkScrollableElement:
*
* var cleanBlocking = (
* blockOverScroll(
* mostParentElement,
* (element) => {
* return element.attributes && el.attributes['data-scroll']
* }
* )
* );
*
* unsubscribeStep() {
* cleanBlocking(); // remove subscribes to prevent memory leak
* }
*/
export function blockOverScroll(
element: Element,
getScrollDirection: (element: Element) => ScrollDirection | false,
): () => void {
let clientX = null; // remember X position on touch start
let clientY = null; // remember Y position on touch start
function touchStart(event) {
if (event.targetTouches.length === 1) {
clientX = event.targetTouches[0].clientX;
clientY = event.targetTouches[0].clientY;
}
}
function touchMove(event) {
if (event.targetTouches.length === 1 && event.cancelable !== false) {
return disableRubberBand(event);
}
}
function disableRubberBand(event: TouchEvent) {
const scrollableElement = findClosestParent(
event.target as any,
getScrollDirection as any,
);
if (!scrollableElement) {
event.preventDefault();
return false;
}
const direction = getScrollDirection(scrollableElement) as ScrollDirection;
// @todo Make another logic for `both` direction
if (direction === 'vertical' || direction === 'both') {
const nextY = event.targetTouches[0].clientY - clientY;
if (scrollableElement.scrollTop === 0 && nextY > 0) {
// element is at the top of its scroll
event.preventDefault();
} else if (isElementVerticalScrolled(scrollableElement) && nextY < 0) {
event.preventDefault();
}
} else if (direction === 'horizontal') {
const nextX = event.targetTouches[0].clientX - clientX;
if (scrollableElement.scrollLeft === 0 && nextX > 0) {
event.preventDefault();
} else if (isElementHorizontalScrolled(scrollableElement) && nextX < 0) {
event.preventDefault();
}
}
}
element.addEventListener('touchstart', touchStart, false);
element.addEventListener('touchmove', touchMove, false);
return function cleanupBlockOverScroll() {
element.removeEventListener('touchstart', touchStart);
element.removeEventListener('touchmove', touchMove);
}
}
/**
* Find closest parent element or return itself
* which is suitable for condition function.
*
* @param element Lowest element from which we will search.
* @param checkParent Condition function to check element are suitable.
*/
function findClosestParent(element: Element, checkParent: (parent: Element) => boolean) {
if (checkParent(element)) {
return element;
}
let current: Element = element.parentNode as any;
while (current && !checkParent(current as any)) {
current = current.parentNode as any;
}
return current;
}
/**
* Check if element is scrolled to the end.
*
* @param element Element.
*/
function isElementVerticalScrolled(element: Element) {
const {
scrollHeight,
scrollTop,
clientHeight,
} = element;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
return scrollHeight - scrollTop <= clientHeight;
}
function isElementHorizontalScrolled(element: Element) {
const {
scrollWidth,
scrollLeft,
clientWidth,
} = element;
return scrollWidth - scrollLeft <= clientWidth;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment