vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
321 lines • 11.1 kB
JavaScript
import { Module } from '../../base';
import { initVevet } from '../../global/initVevet';
import { addEventListener, clamp, onResize, toPixels } from '../../utils';
import { createScrollbarStyles } from './styles';
import { Swipe } from '../Swipe';
export * from './types';
/**
* A custom scrollbar component. Supports both `window` and `HTMLElement` containers.
*
* [Documentation](https://antonbobrov.github.io/vevet/docs/components/Scrollbar)
*
* @group Components
*/
export class Scrollbar extends Module {
/** Get default static properties. */
_getStatic() {
return Object.assign(Object.assign({}, super._getStatic()), { container: window, parent: false, class: false, axis: 'y', draggable: true, autoHide: true, resizeDebounce: 0 });
}
/** Get default mutable properties. */
_getMutable() {
return Object.assign(Object.assign({}, super._getMutable()), { minSize: 50, autoSize: true });
}
get prefix() {
return `${initVevet().prefix}scrollbar`;
}
/**
* The element to which the scrollbar is applied.
*/
get container() {
return this._props.container;
}
/**
* Scrollbar outer element
*/
get outer() {
return this._outer;
}
/**
* Scrollbar track element (the container of the thumb).
*/
get track() {
return this._track;
}
/**
* Scrollbar thumb element (draggable handle).
*/
get thumb() {
return this._thumb;
}
constructor(props) {
super(props);
/** Save scroll value on swipe start */
this._valueOnSwipeStart = 0;
/** Previous scroll value */
this._prevScrollValue = 0;
// No need to remove styles on destroy
createScrollbarStyles(this.prefix);
// Create elements
this._create();
// Set events
this._setResize();
this._setOnscroll();
this._setSwipe();
}
/** Handles property mutations */
_handleProps() {
super._handleProps();
this.resize();
}
/** Scroll axis */
get axis() {
return this.props.axis;
}
/**
* The element where the scrollbar is appended.
* If `parent` is not set, it defaults to `container` or `document.body` (if applied to `window`).
*/
get parent() {
const { parent, container } = this.props;
return (parent || (container instanceof Window ? initVevet().body : container));
}
/**
* The actual scrollable element.
* Returns `document.documentElement` for `window`, otherwise the `container` itself.
*/
get scrollElement() {
return this.container instanceof Window ? initVevet().html : this.container;
}
/**
* Returns the total scroll width/height of the content.
*/
get scrollSize() {
const { scrollElement } = this;
return this.axis === 'x'
? scrollElement.scrollWidth
: scrollElement.scrollHeight;
}
/**
* Returns the total scrollable distance.
*/
get scrollableSize() {
const { scrollElement } = this;
return this.axis === 'x'
? this.scrollSize - scrollElement.clientWidth
: this.scrollSize - scrollElement.clientHeight;
}
/**
* Returns scrollTop or scrollLeft of the scrollable element.
*/
get scrollValue() {
const { axis } = this;
if (this.container instanceof Window) {
return axis === 'x' ? window.scrollX : window.scrollY;
}
return axis === 'x' ? this.container.scrollLeft : this.container.scrollTop;
}
/** Returns the current track size. */
get trackSize() {
return this.axis === 'x'
? this._track.offsetWidth
: this._track.offsetHeight;
}
/** Returns the current thumb size. */
get thumbSize() {
return this.axis === 'x'
? this._thumb.offsetWidth
: this._thumb.offsetHeight;
}
/** Create elements */
_create() {
const app = initVevet();
const { parent, scrollElement } = this;
const isInWindow = this.container instanceof Window;
this._outer = this._createOuter();
parent.appendChild(this._outer);
this._track = this._createTrack();
this._outer.appendChild(this._track);
this._thumb = this._createThumb();
this._track.appendChild(this._thumb);
// Apply global styles
if (isInWindow) {
this._addTempClassName(app.html, this._cn('-scrollable'));
this._addTempClassName(app.body, this._cn('-scrollable'));
}
else {
this._addTempClassName(scrollElement, this._cn('-scrollable'));
}
this.onDestroy(() => this._outer.remove());
}
/** Create outer element */
_createOuter() {
const cn = this._cn.bind(this);
const { props, axis } = this;
const element = document.createElement('div');
element.classList.add(cn(''));
element.classList.add(cn(`_${axis}`));
if (props.class) {
element.classList.add(props.class);
}
if (this.container instanceof Window) {
this._addTempClassName(element, this._cn('_in-window'));
}
if (props.autoHide) {
this._addTempClassName(element, this._cn('_auto-hide'));
}
return element;
}
/** Create track element */
_createTrack() {
const cn = this._cn.bind(this);
const { axis } = this;
const element = document.createElement('div');
element.classList.add(cn('__track'));
element.classList.add(cn(`__track_${axis}`));
return element;
}
/** Create thumb element */
_createThumb() {
const cn = this._cn.bind(this);
const element = document.createElement('div');
element.classList.add(cn('__thumb'));
element.classList.add(cn(`__thumb_${this.axis}`));
return element;
}
/** Set resize events */
_setResize() {
const handler = onResize({
element: [this.track, this.parent, this.scrollElement],
resizeDebounce: this.props.resizeDebounce,
callback: () => this.resize(),
});
handler.resize();
this.onDestroy(() => handler.remove());
}
/** Set scroll events */
_setOnscroll() {
const handler = addEventListener(this.container, 'scroll', () => this._onScroll(), {
passive: true,
});
this.onDestroy(() => handler());
}
/** Set swipe events */
_setSwipe() {
if (!this.props.draggable) {
return;
}
const swipe = new Swipe({ container: this.thumb, grabCursor: true });
swipe.on('start', (coords) => {
this._valueOnSwipeStart = this.scrollValue;
this.callbacks.emit('swipeStart', coords);
});
swipe.on('move', (coords) => {
this._onSwipeMove(coords);
this.callbacks.emit('swipe', coords);
});
swipe.on('end', (coords) => {
this.callbacks.emit('swipeEnd', coords);
});
swipe.on('touchmove', (event) => {
event.stopPropagation();
event.stopImmediatePropagation();
});
swipe.on('mousemove', (event) => {
event.stopPropagation();
event.stopImmediatePropagation();
});
this.onDestroy(() => swipe.destroy());
}
/** Resize the scrollbar. */
resize() {
const { scrollableSize, scrollSize, outer, track, thumb, props, axis } = this;
const { autoSize: shouldAutoSize } = props;
const isHorizontal = axis === 'x';
// Define if the scrollbar is empty
outer.classList.toggle(this._cn('_empty'), scrollableSize === 0);
// Save sizes
const trackSize = isHorizontal ? track.offsetWidth : track.offsetHeight;
// Calculate minimum thumb size
const minThumbSize = toPixels(props.minSize);
let newThumbSize = minThumbSize;
// Calculate thumb sizes if auto-size is enabled
if (shouldAutoSize) {
newThumbSize = clamp(trackSize / (scrollSize / trackSize), minThumbSize, Infinity);
}
// Apply sizes
if (isHorizontal) {
thumb.style.width = `${newThumbSize}px`;
}
else {
thumb.style.height = `${newThumbSize}px`;
}
// Reset timeouts
if (this._addInActionTimeout) {
clearTimeout(this._addInActionTimeout);
}
// Render the scrollbar
this._render();
// Emit callbacks
this.callbacks.emit('resize', undefined);
}
/** Render the scrollbar. */
_render() {
const { scrollValue, scrollableSize, axis, thumbSize, trackSize } = this;
const scrollProgress = clamp(scrollValue / scrollableSize);
const translate = (trackSize - thumbSize) * scrollProgress;
const x = axis === 'x' ? translate : 0;
const y = axis === 'y' ? translate : 0;
this._thumb.style.transform = `translate(${x}px, ${y}px)`;
// Emit callbacks
this.callbacks.emit('update', undefined);
}
/** Handle scroll update */
_onScroll() {
const { scrollValue, outer } = this;
const inActionClass = this._cn('_in-action');
if (scrollValue !== this._prevScrollValue) {
this._addInActionTimeout = setTimeout(() => {
if (!outer.classList.contains(inActionClass)) {
outer.classList.add(inActionClass);
this.callbacks.emit('show', undefined);
}
}, 50);
}
else {
this._prevScrollValue = scrollValue;
}
this._render();
if (this._removeInActionTimeout) {
clearTimeout(this._removeInActionTimeout);
}
this._removeInActionTimeout = setTimeout(() => {
outer.classList.remove(inActionClass);
this.callbacks.emit('hide', undefined);
}, 500);
}
/** Handle swipe move */
_onSwipeMove({ diff }) {
const { scrollElement, axis, trackSize, thumbSize, scrollableSize } = this;
const diffCoord = axis === 'x' ? diff.x : diff.y;
const iterator = (diffCoord / (trackSize - thumbSize)) * scrollableSize;
const target = this._valueOnSwipeStart + iterator;
scrollElement.scrollTo({
top: axis === 'y' ? target : undefined,
left: axis === 'x' ? target : undefined,
behavior: 'instant',
});
}
/**
* Destroys the component and cleans up resources.
*/
_destroy() {
super._destroy();
if (this._addInActionTimeout) {
clearTimeout(this._addInActionTimeout);
}
if (this._removeInActionTimeout) {
clearTimeout(this._removeInActionTimeout);
}
}
}
//# sourceMappingURL=index.js.map