@furman1331/page-scroller
Version:
Amazing plugin for creating smooth scroll on your website
666 lines (641 loc) • 25.9 kB
JavaScript
class State {
container = null;
sections = null;
activeSlide = 0;
activeSection = 0;
scrollMode = 'automatic';
scrollingSpeed = 700;
transitionTimingFunction = 'ease';
isDebug = false;
isScrolling = false;
isResizing = false;
isInitialized = false;
isWheelEnabled = true;
isKeyboardEnabled = true;
isTouchEnabled = true;
slidesIdentifyAttribute = 'page-scroller-slide';
isAllowToScrollThroughSlides = true;
}
const state = new State();
class Callback {
onSectionChange;
onBeforeSectionChange;
}
const callback = new Callback();
var ClassName;
(function (ClassName) {
ClassName["html"] = "page-scroller-enabled";
ClassName["body"] = "page-scoller-body";
ClassName["container"] = "page-scroller-wrapper";
ClassName["section"] = "page-scroller-section";
ClassName["sectionWithSlides"] = "page-scroller-section-with-slides";
ClassName["activeSection"] = "page-scroller-section-active";
ClassName["slide"] = "page-scroller-slide";
})(ClassName || (ClassName = {}));
var SlideClassName;
(function (SlideClassName) {
SlideClassName["wrapper"] = "page-scroller-slide-wrapper";
SlideClassName["active"] = "page-scroller-slide-active";
})(SlideClassName || (SlideClassName = {}));
function initializeDOM() {
const htmlElement = document.querySelector('html');
htmlElement.classList.add(ClassName.html);
const bodyElement = document.querySelector('body');
bodyElement.classList.add(ClassName.body);
state.container.classList.add(ClassName.container);
const transition = `transform ${state.scrollingSpeed}ms ${state.transitionTimingFunction}`;
state.container.style.transition = transition;
prepareSections();
state.scrollMode === 'automatic' ? prepareScrollModeAutomaticDOM() : prepareScrollModeManualDOM();
}
function prepareSections() {
state.sections = Array.from(state.container.children).map((element) => {
const section = element;
const childrens = Array.from(section.children);
const foundSlides = childrens.filter((slide) => slide.hasAttribute(state.slidesIdentifyAttribute));
foundSlides.forEach((slide) => slide.classList.add(ClassName.slide));
if (!foundSlides.length)
return { element: section, slides: null };
const container = preapreSectionForSlides(section, foundSlides);
const slides = { container, elements: foundSlides };
return { element: section, slides };
});
state.sections.forEach((section) => section.element.classList.add(ClassName.section));
}
function preapreSectionForSlides(section, slides) {
const wrapperElement = document.createElement('div');
wrapperElement.classList.add(SlideClassName.wrapper);
const transition = `transform ${state.scrollingSpeed}ms ${state.transitionTimingFunction}`;
wrapperElement.style.transition = transition;
wrapperElement.style.width = `${slides.length * 100}%`;
slides.forEach((slide) => {
slide.style.width = `${100 / slides.length}%`;
wrapperElement.appendChild(slide);
});
const containerElement = document.createElement('div');
containerElement.classList.add(ClassName.sectionWithSlides);
containerElement.appendChild(wrapperElement);
section.appendChild(containerElement);
return wrapperElement;
}
function destroyDOM() {
const htmlElement = document.querySelector('html');
htmlElement.classList.remove(ClassName.html);
const bodyElement = document.querySelector('body');
bodyElement.classList.remove(ClassName.body);
state.container.classList.remove(ClassName.container);
state.container.style.transition = '';
state.container.style.transform = 'none';
state.container.style.webkitTransform = 'none';
state.sections.forEach((section) => section.element.classList.remove(ClassName.section));
}
function prepareScrollModeAutomaticDOM() {
const bodyElement = document.querySelector('body');
bodyElement.style.overflow = 'hidden';
bodyElement.style.height = '100%';
const htmlElement = document.querySelector('html');
htmlElement.style.overflow = 'hidden';
htmlElement.style.height = '100%';
const transition = `transform ${state.scrollingSpeed}ms ${state.transitionTimingFunction}`;
state.container.style.transition = transition;
}
function prepareScrollModeManualDOM() {
const bodyElement = document.querySelector('body');
bodyElement.style.overflow = 'auto';
bodyElement.style.height = 'initial';
const htmlElement = document.querySelector('html');
htmlElement.style.overflow = 'auto';
htmlElement.style.height = 'initial';
state.container.style.transition = '';
state.container.style.transform = 'none';
state.container.style.webkitTransform = 'none';
}
function useLogger() {
function info(message) {
if (state.isDebug)
console.log(createMessage(message, 'info'));
}
function error(message) {
if (state.isDebug)
console.error(createMessage(message, 'error'));
}
function warn(message) {
if (state.isDebug)
console.warn(createMessage(message, 'warn'));
}
function createMessage(message, type) {
return `[Page-Scroller]${type ? `[${type.toUpperCase()}]` : ''}: ${message}`;
}
return { info, error, warn, createMessage };
}
const focusableElementsString = `a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], summary:not([disabled]), [contenteditable]`;
function isUserUsingInput() {
const activeElement = document.activeElement;
const supportedElements = ['input', 'textarea'];
return supportedElements.includes(activeElement.tagName.toLowerCase());
}
function getAverageFromArray(array, number) {
const last = array.slice(Math.max(array.length - number, 0));
const sum = last.reduce((acc, curr) => acc + curr, 0);
return Math.ceil(sum / number);
}
function changeSectionOrSlideByDirection(direction) {
if (isAllowToChangeSlide(direction)) {
changeSlideByDirection(direction === 'down' ? 'right' : 'left');
return;
}
changeSectionByDirection(direction);
}
function changeSlideByDirection(direction) {
if (state.isScrolling)
return;
state.isScrolling = true;
const currentSlideIndex = state.activeSlide;
state.activeSlide = direction === 'right' ? currentSlideIndex + 1 : currentSlideIndex - 1;
changeSlide(currentSlideIndex, state.activeSlide);
}
function changeSectionByDirection(direction) {
if (state.isScrolling)
return;
state.isScrolling = true;
const currentSectionIndex = state.activeSection;
if (!isAllowToChangeSection(direction))
return (state.isScrolling = false);
state.activeSection = direction === 'down' ? currentSectionIndex + 1 : currentSectionIndex - 1;
changeSection(currentSectionIndex, state.activeSection);
}
function changeSectionBySpecificIndex(index) {
if (state.isScrolling)
return;
state.isScrolling = true;
const currentSectionIndex = state.activeSection;
if (!isAllowToChangeByIndex(index))
return (state.isScrolling = false);
state.sections[currentSectionIndex].element.classList.remove(ClassName.activeSection);
state.activeSection = index;
changeSection(currentSectionIndex, state.activeSection);
}
function reAdjustCurrentSection() {
if (state.scrollMode === 'manual')
return;
const sectionOffset = state.sections[state.activeSection].element.offsetTop;
const transform = `translate3d(0px, -${sectionOffset}px, 0px)`;
state.container.style.transform = transform;
state.container.style.webkitTransform = transform;
}
function changeSection(previousIndex, nextIndex) {
emitter.emit(EmitterEvents.onBeforeSectionChange, { beforeIndex: previousIndex, afterIndex: nextIndex });
state.sections[previousIndex].element.classList.remove(ClassName.activeSection);
const sectionOffset = state.sections[nextIndex].element.offsetTop;
const transform = `translate3d(0px, -${sectionOffset}px, 0px)`;
state.container.style.transform = transform;
state.container.style.webkitTransform = transform;
state.sections[nextIndex].element.classList.add(ClassName.activeSection);
setTimeout(() => {
state.isScrolling = false;
emitter.emit(EmitterEvents.onSectionChange, { beforeIndex: previousIndex, afterIndex: nextIndex });
}, 700);
}
function changeSlide(previousIndex, nextIndex) {
emitter.emit(EmitterEvents.onBeforeSlideChange, { beforeIndex: previousIndex, afterIndex: nextIndex });
const activeSection = state.sections[state.activeSection];
activeSection.slides.elements[previousIndex]?.classList.remove(SlideClassName.active);
const slideOffset = activeSection.slides.elements[nextIndex].offsetLeft;
const transform = `translate3d(-${slideOffset}px, 0px, 0px)`;
activeSection.slides.container.style.transform = transform;
activeSection.slides.container.style.webkitTransform = transform;
state.sections[state.activeSection].slides.elements[nextIndex].classList.add(SlideClassName.active);
setTimeout(() => {
state.isScrolling = false;
emitter.emit(EmitterEvents.onSlideChange, { beforeIndex: previousIndex, afterIndex: nextIndex });
}, state.scrollingSpeed);
}
function isAllowToChangeSection(direction) {
return direction === 'down' ? state.sections.length != state.activeSection + 1 : state.activeSection - 1 !== -1;
}
function isAllowToChangeSlide(direction) {
if (!state.isAllowToScrollThroughSlides)
return false;
const isCurrentSectionHasSlides = state.sections[state.activeSection].slides?.elements.length > 0;
if (!isCurrentSectionHasSlides)
return false;
const slides = state.sections[state.activeSection].slides;
const isEdgeSlide = direction === 'down' ? state.activeSlide + 1 === slides.elements.length : state.activeSlide - 1 === -1;
if (isEdgeSlide)
return false;
return true;
}
function isAllowToChangeByIndex(index) {
return index >= 0 && index < state.sections.length;
}
const defaultState = {
scrollMode: 'automatic',
scrollingSpeed: 700,
transitionTimingFunction: 'ease',
slidesIdentifyAttribute: 'page-scroller-slide',
isAllowToScrollThroughSlides: false,
isDebug: false,
isWheelEnabled: true,
isKeyboardEnabled: true,
isTouchEnabled: true,
};
function initializeState(options) {
state.scrollMode = options.scrollMode ?? defaultState.scrollMode;
state.scrollingSpeed = options.scrollingSpeed ?? defaultState.scrollingSpeed;
state.transitionTimingFunction = options.transitionTimingFunction ?? defaultState.transitionTimingFunction;
state.isDebug = options.isDebug ?? defaultState.isDebug;
state.isWheelEnabled = options.isWheelEnabled ?? defaultState.isWheelEnabled;
state.isKeyboardEnabled = options.isKeyboardEnabled ?? defaultState.isKeyboardEnabled;
state.isTouchEnabled = options.isTouchEnabled ?? defaultState.isTouchEnabled;
state.slidesIdentifyAttribute = options.slidesIdentifyAttribute ?? defaultState.slidesIdentifyAttribute;
state.isAllowToScrollThroughSlides = options.isAllowToScrollThroughSlides ?? defaultState.isAllowToScrollThroughSlides;
}
function destroyState() {
state.container = null;
state.sections = null;
state.activeSlide = 0;
state.activeSection = 0;
state.transitionTimingFunction = defaultState.transitionTimingFunction;
state.scrollingSpeed = defaultState.scrollingSpeed;
state.slidesIdentifyAttribute = defaultState.slidesIdentifyAttribute;
state.isAllowToScrollThroughSlides = defaultState.isAllowToScrollThroughSlides;
state.isDebug = defaultState.isDebug;
state.isScrolling = false;
state.isInitialized = false;
state.isWheelEnabled = defaultState.isWheelEnabled;
state.isKeyboardEnabled = defaultState.isKeyboardEnabled;
state.isTouchEnabled = defaultState.isTouchEnabled;
}
function initializeCallbacks(options) {
if (options.onSectionChange) {
callback.onSectionChange = options.onSectionChange;
emitter.on(EmitterEvents.onSectionChange, (event) => callback.onSectionChange(event));
}
if (options.onBeforeSectionChange) {
callback.onBeforeSectionChange = options.onBeforeSectionChange;
emitter.on(EmitterEvents.onBeforeSectionChange, (event) => callback.onBeforeSectionChange(event));
}
}
function destroyCallbacks() {
callback.onSectionChange = () => { };
emitter.off(EmitterEvents.onSectionChange);
callback.onBeforeSectionChange = () => { };
emitter.off(EmitterEvents.onBeforeSectionChange);
}
const logger$5 = useLogger();
function onInitialize(options) {
logger$5.info('Initializing Page Scroller...');
if (options) {
initializeState(options);
initializeCallbacks(options);
}
initializeDOM();
registerEvents();
registerEmitterEvents();
state.isInitialized = true;
logger$5.info('Initialized Page Scroller.');
}
function onDestroy() {
logger$5.warn('Destroying Page Scroller...');
destroyDOM();
destroyEvents();
destroyEmitterEvents();
destroyState();
destroyCallbacks();
state.isInitialized = false;
logger$5.warn('Destroyed Page Scroller.');
}
let scrollingTimeout;
let scrollings = [];
const logger$4 = useLogger();
/**
* Registers the wheel event listener on the document body.
*/
function registerWheelEvent() {
logger$4.info('Wheel event registered');
document.body.addEventListener('wheel', wheelEventHandler);
}
/**
* Removes the wheel event listener from the document body.
*/
function destroyWheelEvent() {
logger$4.info('Wheel event registered');
document.body.removeEventListener('wheel', wheelEventHandler);
}
function wheelEventHandler(event) {
logger$4.info('Wheel event detected');
clearTimeout(scrollingTimeout);
scrollingTimeout = setTimeout(() => {
scrollings = [];
}, 200);
const scrollValue = -event.deltaY || event.detail;
const direction = getScrollDirection(scrollValue);
if (scrollings.length > 100) {
scrollings.shift();
}
scrollings.push(Math.abs(scrollValue));
if (!checkIsAccelerating())
return;
return changeSectionOrSlideByDirection(direction);
}
function checkIsAccelerating() {
const avarageFromEnd = getAverageFromArray(scrollings, 5);
const avarageFromMid = getAverageFromArray(scrollings, 50);
return avarageFromEnd >= avarageFromMid;
}
/**
* Determines the scroll direction based on the WheelEvent.
* @param event - The WheelEvent object.
* @returns The scroll direction, either "up" or "down".
*/
function getScrollDirection(value) {
const delta = Math.max(-1, Math.min(1, value));
return delta < 0 ? 'down' : 'up';
}
const logger$3 = useLogger();
let focusElementCollation = null;
/**
* Registers the keyboard event listeners for keyup and keydown events.
*/
function registerKeyboardEvents() {
document.addEventListener('keydown', keyDownEventHandler);
emitter.on(EmitterEvents.onSectionChange, onSectionChangeHandler);
}
/**
* Removes the keyboard event listeners for keyup and keydown events.
*/
function destroyKeyboardEvents() {
document.removeEventListener('keydown', keyDownEventHandler);
emitter.off(EmitterEvents.onSectionChange);
}
/**
* Hanldes the keydown event and changes the section based on the key pressed.
* @param event KeyboardEvent - The keyboard event.
* @returns void
*/
function keyDownEventHandler(event) {
logger$3.info('Keydown event detected');
const key = event.key;
if (isUserUsingInput())
return;
switch (key) {
case ' ':
case 'ArrowDown':
case 'PageDown':
changeSectionOrSlideByDirection('down');
break;
case 'ArrowUp':
case 'PageUp':
changeSectionOrSlideByDirection('up');
break;
case 'ArrowRight':
changeSlideByDirection('right');
case 'ArrowLeft':
changeSlideByDirection('left');
case 'End':
changeSectionBySpecificIndex(state.sections.length - 1);
break;
case 'Home':
changeSectionBySpecificIndex(0);
break;
case 'Tab':
onTabPress(event);
break;
}
}
/**
* Make sure that the tab key will only focus elements within the current section.
* Prevent page break when the tab key is pressed.
* @param event - The keyboard event.
*/
function onTabPress(event) {
const isShiftPressed = event.shiftKey;
const activeElement = document.activeElement;
const focusableElements = getFocusableElements(state.sections[state.activeSection].element);
const isFirstFocusableInSection = activeElement === focusableElements[0];
const isLastFocusableInSection = activeElement === focusableElements[focusableElements.length - 1];
const shouldChangeSection = (isShiftPressed && isFirstFocusableInSection) || (!isShiftPressed && isLastFocusableInSection);
if (shouldChangeSection) {
event.preventDefault();
const direction = isShiftPressed && isFirstFocusableInSection ? 'up' : 'down';
focusElementCollation = direction === 'up' ? 'last' : 'first';
changeSectionOrSlideByDirection(direction);
}
}
/**
* Focuses the first or last focusable element within the current section while changing the section by tag key
*/
function onSectionChangeHandler() {
if (!focusElementCollation)
return;
const focusableElements = getFocusableElements(state.sections[state.activeSection].element);
focusableElements[focusElementCollation === 'first' ? 0 : focusableElements.length - 1].focus();
focusElementCollation = null;
}
function getFocusableElements(parent) {
return [].slice
.call(parent.querySelectorAll(focusableElementsString))
.filter((element) => element.getAttribute('tabindex') !== '-1' && element.offsetParent !== null);
}
const logger$2 = useLogger();
function registerTouchEvents() {
document.addEventListener('touchstart', onTouchStartHandler);
state.container.addEventListener('touchmove', onTouchMoveHandler, { passive: false });
}
function destroyTouchEvents() {
document.removeEventListener('touchstart', onTouchStartHandler);
state.container.removeEventListener('touchmove', onTouchMoveHandler);
}
let touchStartCoordinates = {};
function onTouchStartHandler(event) {
const coordinates = getEventCoordinated(event);
touchStartCoordinates = {
x: coordinates.x,
y: coordinates.y,
};
}
function onTouchMoveHandler(event) {
logger$2.info('Touch move event detected');
const coordinates = getEventCoordinated(event);
const isVerticalMovementEnought = Math.abs(coordinates.y - touchStartCoordinates.y) > (window.innerHeight / 100) * 5;
const direction = touchStartCoordinates.y > coordinates.y ? 'down' : 'up';
if (isVerticalMovementEnought)
changeSectionOrSlideByDirection(direction);
}
function getEventCoordinated(event) {
return {
x: event.touches[0].pageX,
y: event.touches[0].pageY,
};
}
const logger$1 = useLogger();
let timeout;
let isResizing = false;
function registerResizeEvents() {
onResizeHandler();
window.addEventListener('resize', onResizeHandler);
}
function destroyResizeEvents() {
resizeHandler();
clearTimeout(timeout);
window.removeEventListener('resize', onResizeHandler);
}
function onResizeHandler() {
logger$1.info('Resize event has been triggered.');
if (!isResizing) {
resizeHandler();
}
isResizing = true;
clearTimeout(timeout);
timeout = setTimeout(() => {
resizeAction();
isResizing = false;
}, 400);
}
function resizeAction() {
state.isResizing = true;
resizeHandler();
reAdjustCurrentSection();
}
function resizeHandler() {
const height = window ? window.innerHeight : document.documentElement.offsetHeight;
setSectionsSize(height);
}
function setSectionsSize(height) {
state.sections.forEach((section) => (section.element.style.height = `${height}px`));
}
/**
* Registers the events for the page scroller.
*/
function registerEvents() {
if (state.scrollMode === "manual")
return;
state.isWheelEnabled && registerWheelEvent();
state.isKeyboardEnabled && registerKeyboardEvents();
state.isTouchEnabled && registerTouchEvents();
registerResizeEvents();
}
/**
* Destroys the events for the page scroller.
*/
function destroyEvents() {
destroyKeyboardEvents();
destroyWheelEvent();
destroyTouchEvents();
destroyResizeEvents();
}
var EmitterEvents;
(function (EmitterEvents) {
EmitterEvents["onSectionChange"] = "onSectionChange";
EmitterEvents["onBeforeSectionChange"] = "onBeforeSectionChange";
EmitterEvents["onSlideChange"] = "onSlideChange";
EmitterEvents["onBeforeSlideChange"] = "onBeforeSlideChange";
EmitterEvents["onPageScrollStatusChanged"] = "onPageScrollStatusChanged";
EmitterEvents["onPageScrollModeAutomatic"] = "onPageScrollModeAutomatic";
EmitterEvents["onPageScrollModeManual"] = "onPageScrollModeManual";
})(EmitterEvents || (EmitterEvents = {}));
function mitt(all) {
all = all || new Map();
return {
all,
on(type, handler) {
const handlers = all.get(type);
if (handlers) {
handlers.push(handler);
}
else {
all.set(type, [handler]);
}
},
off(type, handler) {
const handlers = all.get(type);
if (handlers) {
if (handler) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
}
else {
all.set(type, []);
}
}
},
emit(type, evt) {
let handlers = all.get(type);
if (handlers) {
handlers.slice().map((handler) => {
handler(evt);
});
}
handlers = all.get('*');
if (handlers) {
handlers.slice().map((handler) => {
handler(type, evt);
});
}
},
};
}
const emitter = mitt();
function registerEmitterEvents() {
emitter.on(EmitterEvents.onPageScrollModeAutomatic, () => {
prepareScrollModeAutomaticDOM();
registerEvents();
});
emitter.on(EmitterEvents.onPageScrollModeManual, () => {
prepareScrollModeManualDOM();
destroyEvents();
});
}
function destroyEmitterEvents() {
emitter.off(EmitterEvents.onPageScrollModeManual);
emitter.off(EmitterEvents.onPageScrollModeAutomatic);
}
const isManualScrollingMode = () => state.scrollMode === 'manual';
const isAutomaticScrollingMode = () => state.scrollMode === 'automatic';
function changeScrollingMode(mode) {
if (state.scrollMode === mode)
return;
state.scrollMode = mode;
emitter.emit(mode === 'automatic' ? EmitterEvents.onPageScrollModeAutomatic : EmitterEvents.onPageScrollModeManual);
}
const getActiveSection = () => state.activeSection;
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = "html.page-scroller-enabled, .page-scroller-enabled body {\n margin: 0;\n padding: 0;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\n.page-scoller-body {\n height: 100%;\n position: relative;\n}\n\n.page-scroller-wrapper {\n height: 100%;\n width: 100%;\n position: relative;\n}\n\n.page-scroller-section {\n height: 100%;\n display: block;\n position: relative;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n -webkit-box-sizing: border-box;\n}\n\n.page-scroller-section-with-slides {\n z-index: 1;\n height: 100%;\n overflow: hidden;\n position: relative;\n -webkit-transition: all .3s ease-out;\n transition: all .3s ease-out;\n}\n\n.page-scroller-slide-wrapper {\n height: 100%;\n display: flex;\n float: left;\n position: relative;\n}\n";
styleInject(css_248z);
const logger = useLogger();
function usePageScroller(options) {
function initPageScroller(selector) {
logger.info('Initializing page scroller...');
if (state.isInitialized)
throw new Error(logger.createMessage('Page scroller is already initialized.'));
if (selector === undefined)
throw new Error(logger.createMessage('Please provide a valid selector.'));
state.container = document.querySelector(selector);
if (!state.container)
throw new Error(logger.createMessage('Container not found. Please provide a valid selector.'));
onInitialize(options);
}
return { initPageScroller, changeSectionByDirection, changeSectionBySpecificIndex };
}
export { changeScrollingMode, changeSectionByDirection, changeSectionBySpecificIndex, changeSectionOrSlideByDirection, changeSlideByDirection, getActiveSection, isAutomaticScrollingMode, isManualScrollingMode, onDestroy, usePageScroller };
//# sourceMappingURL=index.esm.mjs.map