react-scrollbars-custom
Version:
The best React custom scrollbars component
1,246 lines (1,238 loc) • 68.8 kB
JavaScript
import { cnb } from 'cnbuilder';
import * as React from 'react';
import { zoomLevel } from 'zoom-level';
import { DraggableCore } from 'react-draggable';
let doc = typeof document === 'object' ? document : null;
const isBrowser = typeof window !== 'undefined' &&
typeof navigator !== 'undefined' &&
typeof document !== 'undefined';
const isUndef = (v) => {
return typeof v === 'undefined';
};
const isFun = (v) => {
return typeof v === 'function';
};
const isNum = (v) => {
return typeof v === 'number';
};
/**
* @description Will return renderer result if presented, div element otherwise.
* If renderer is presented it'll receive `elementRef` function which should be used as HTMLElement's ref.
*
* @param props {ElementPropsWithElementRefAndRenderer}
* @param elementRef {ElementRef}
*/
const renderDivWithRenderer = (props, elementRef) => {
if (isFun(props.renderer)) {
props.elementRef = elementRef;
const { renderer } = props;
delete props.renderer;
return renderer(props);
}
delete props.elementRef;
return React.createElement("div", { ...props, ref: elementRef });
};
const getInnerSize = (el, dimension, padding1, padding2) => {
const styles = getComputedStyle(el);
if (styles.boxSizing === 'border-box') {
return Math.max(0, (Number.parseFloat(styles[dimension]) || 0) -
(Number.parseFloat(styles[padding1]) || 0) -
(Number.parseFloat(styles[padding2]) || 0));
}
return Number.parseFloat(styles[dimension]) || 0;
};
/**
* @description Return element's height without padding
*/
const getInnerHeight = (el) => {
return getInnerSize(el, 'height', 'paddingTop', 'paddingBottom');
};
/**
* @description Return element's width without padding
*/
const getInnerWidth = (el) => {
return getInnerSize(el, 'width', 'paddingLeft', 'paddingRight');
};
/**
* @description Return unique UUID v4
*/
const uuid = () => {
// eslint-disable-next-line @typescript-eslint/no-shadow
let uuid = '';
for (let i = 0; i < 32; i++) {
switch (i) {
case 8:
case 20: {
uuid += `-${Math.trunc(Math.random() * 16).toString(16)}`;
break;
}
case 12: {
uuid += '-4';
break;
}
case 16: {
uuid += `-${((Math.random() * 16) | (0 & 3) | 8).toString(16)}`;
break;
}
default: {
uuid += Math.trunc(Math.random() * 16).toString(16);
}
}
}
return uuid;
};
/**
* @description Calculate thumb size for given viewport and track parameters
*
* @param {number} contentSize - Scrollable content size
* @param {number} viewportSize - Viewport size
* @param {number} trackSize - Track size thumb can move
* @param {number} minimalSize - Minimal thumb's size
* @param {number} maximalSize - Maximal thumb's size
*/
const calcThumbSize = (contentSize, viewportSize, trackSize, minimalSize, maximalSize) => {
if (viewportSize >= contentSize) {
return 0;
}
let thumbSize = (viewportSize / contentSize) * trackSize;
if (isNum(maximalSize)) {
thumbSize = Math.min(maximalSize, thumbSize);
}
if (isNum(minimalSize)) {
thumbSize = Math.max(minimalSize, thumbSize);
}
return thumbSize;
};
/**
* @description Calculate thumb offset for given viewport, track and thumb parameters
*
* @param {number} contentSize - Scrollable content size
* @param {number} viewportSize - Viewport size
* @param {number} trackSize - Track size thumb can move
* @param {number} thumbSize - Thumb size
* @param {number} scroll - Scroll value to represent
*/
const calcThumbOffset = (contentSize, viewportSize, trackSize, thumbSize, scroll) => {
if (!scroll || !thumbSize || viewportSize >= contentSize) {
return 0;
}
return ((trackSize - thumbSize) * scroll) / (contentSize - viewportSize);
};
/**
* @description Calculate scroll for given viewport, track and thumb parameters
*
* @param {number} contentSize - Scrollable content size
* @param {number} viewportSize - Viewport size
* @param {number} trackSize - Track size thumb can move
* @param {number} thumbSize - Thumb size
* @param {number} thumbOffset - Thumb's offset representing the scroll
*/
const calcScrollForThumbOffset = (contentSize, viewportSize, trackSize, thumbSize, thumbOffset) => {
if (!thumbOffset || !thumbSize || viewportSize >= contentSize) {
return 0;
}
return (thumbOffset * (contentSize - viewportSize)) / (trackSize - thumbSize);
};
/**
* @description Returns scrollbar width specific for current environment. Can return undefined if DOM is not ready yet.
*/
const getScrollbarWidth = (force = false) => {
if (!doc) {
getScrollbarWidth._cache = 0;
return getScrollbarWidth._cache;
}
if (!force && !isUndef(getScrollbarWidth._cache)) {
return getScrollbarWidth._cache;
}
const el = doc.createElement('div');
el.setAttribute('style', 'position:absolute;width:100px;height:100px;top:-999px;left:-999px;overflow:scroll;');
doc.body.append(el);
/* istanbul ignore next */
if (el.clientWidth === 0) {
// Do not even cache this value because there is no calculations. Issue https://github.com/xobotyi/react-scrollbars-custom/issues/123
el.remove();
return;
}
getScrollbarWidth._cache = 100 - el.clientWidth;
el.remove();
return getScrollbarWidth._cache;
};
/**
* @description Detect need of horizontal scroll reverse while RTL.
*/
const shouldReverseRtlScroll = (force = false) => {
if (!force && !isUndef(shouldReverseRtlScroll._cache)) {
return shouldReverseRtlScroll._cache;
}
if (!doc) {
shouldReverseRtlScroll._cache = false;
return shouldReverseRtlScroll._cache;
}
const el = doc.createElement('div');
const child = doc.createElement('div');
el.append(child);
el.setAttribute('style', 'position:absolute;width:100px;height:100px;top:-999px;left:-999px;overflow:scroll;direction:rtl');
child.setAttribute('style', 'width:1000px;height:1000px');
doc.body.append(el);
el.scrollLeft = -50;
shouldReverseRtlScroll._cache = el.scrollLeft === -50;
el.remove();
return shouldReverseRtlScroll._cache;
};
class Emittr {
constructor(maxHandlers = 10) {
this.setMaxHandlers(maxHandlers);
this._handlers = Object.create(null);
}
static _callEventHandlers(emitter, handlers, args) {
if (!handlers.length) {
return;
}
if (handlers.length === 1) {
Reflect.apply(handlers[0], emitter, args);
return;
}
handlers = [...handlers];
let idx;
for (idx = 0; idx < handlers.length; idx++) {
Reflect.apply(handlers[idx], emitter, args);
}
}
setMaxHandlers(count) {
if (!isNum(count) || count <= 0) {
throw new TypeError(`Expected maxHandlers to be a positive number, got '${count}' of type ${typeof count}`);
}
this._maxHandlers = count;
return this;
}
getMaxHandlers() {
return this._maxHandlers;
}
emit(name, ...args) {
if (typeof this._handlers[name] !== 'object' || !Array.isArray(this._handlers[name])) {
return false;
}
Emittr._callEventHandlers(this, this._handlers[name], args);
return true;
}
on(name, handler) {
Emittr._addHandler(this, name, handler);
return this;
}
prependOn(name, handler) {
Emittr._addHandler(this, name, handler, true);
return this;
}
once(name, handler) {
if (!isFun(handler)) {
throw new TypeError(`Expected event handler to be a function, got ${typeof handler}`);
}
Emittr._addHandler(this, name, this._wrapOnceHandler(name, handler));
return this;
}
prependOnce(name, handler) {
if (!isFun(handler)) {
throw new TypeError(`Expected event handler to be a function, got ${typeof handler}`);
}
Emittr._addHandler(this, name, this._wrapOnceHandler(name, handler), true);
return this;
}
off(name, handler) {
Emittr._removeHandler(this, name, handler);
return this;
}
removeAllHandlers() {
const handlers = this._handlers;
this._handlers = Object.create(null);
const removeHandlers = handlers.removeHandler;
delete handlers.removeHandler;
let idx;
let eventName;
// eslint-disable-next-line guard-for-in,no-restricted-syntax
for (eventName in handlers) {
for (idx = handlers[eventName].length - 1; idx >= 0; idx--) {
Emittr._callEventHandlers(this, removeHandlers, [
eventName,
handlers[eventName][idx].handler || handlers[eventName][idx],
]);
}
}
return this;
}
_wrapOnceHandler(name, handler) {
const onceState = {
fired: false,
handler,
wrappedHandler: undefined,
emitter: this,
event: name,
};
const wrappedHandler = Emittr._onceWrapper.bind(onceState);
onceState.wrappedHandler = wrappedHandler;
wrappedHandler.handler = handler;
wrappedHandler.event = name;
return wrappedHandler;
}
}
Emittr._addHandler = (emitter, name, handler, prepend = false) => {
if (!isFun(handler)) {
throw new TypeError(`Expected event handler to be a function, got ${typeof handler}`);
}
emitter._handlers[name] = emitter._handlers[name] || [];
emitter.emit('addHandler', name, handler);
if (prepend) {
emitter._handlers[name].unshift(handler);
}
else {
emitter._handlers[name].push(handler);
}
return emitter;
};
Emittr._onceWrapper = function _onceWrapper(...args) {
if (!this.fired) {
this.fired = true;
this.emitter.off(this.event, this.wrappedHandler);
Reflect.apply(this.handler, this.emitter, args);
}
};
Emittr._removeHandler = (emitter, name, handler) => {
if (!isFun(handler)) {
throw new TypeError(`Expected event handler to be a function, got ${typeof handler}`);
}
if (isUndef(emitter._handlers[name]) || !emitter._handlers[name].length) {
return emitter;
}
let idx = -1;
if (emitter._handlers[name].length === 1) {
if (emitter._handlers[name][0] === handler ||
emitter._handlers[name][0].handler === handler) {
idx = 0;
handler = emitter._handlers[name][0].handler || emitter._handlers[name][0];
}
}
else {
for (idx = emitter._handlers[name].length - 1; idx >= 0; idx--) {
if (emitter._handlers[name][idx] === handler ||
emitter._handlers[name][idx].handler === handler) {
handler =
emitter._handlers[name][idx].handler || emitter._handlers[name][idx];
break;
}
}
}
if (idx === -1) {
return emitter;
}
if (idx === 0) {
emitter._handlers[name].shift();
}
else {
emitter._handlers[name].splice(idx, 1);
}
emitter.emit('removeHandler', name, handler);
return emitter;
};
class RAFLoop {
constructor() {
/**
* @description List of targets to update
*/
this.targets = [];
/**
* @description ID of requested animation frame. Valuable only if loop is active and has items to iterate.
*/
this.animationFrameID = 0;
/**
* @description Loop's state.
*/
this._isActive = false;
/**
* @description Start the loop if it wasn't yet.
*/
this.start = () => {
if (!this._isActive && this.targets.length) {
this._isActive = true;
if (this.animationFrameID)
cancelAnimationFrame(this.animationFrameID);
this.animationFrameID = requestAnimationFrame(this.rafCallback);
}
return this;
};
/**
* @description Stop the loop if is was active.
*/
this.stop = () => {
if (this._isActive) {
this._isActive = false;
if (this.animationFrameID)
cancelAnimationFrame(this.animationFrameID);
this.animationFrameID = 0;
}
return this;
};
/**
* @description Add target to the iteration list if it's not there.
*/
this.addTarget = (target, silent = false) => {
if (!this.targets.includes(target)) {
this.targets.push(target);
if (this.targets.length === 1 && !silent)
this.start();
}
return this;
};
/**
* @description Remove target from iteration list if it was there.
*/
this.removeTarget = (target) => {
const idx = this.targets.indexOf(target);
if (idx !== -1) {
this.targets.splice(idx, 1);
if (this.targets.length === 0)
this.stop();
}
return this;
};
/**
* @description Callback that called each animation frame.
*/
this.rafCallback = () => {
if (!this._isActive) {
return 0;
}
for (let i = 0; i < this.targets.length; i++) {
if (!this.targets[i]._unmounted)
this.targets[i].update();
}
this.animationFrameID = requestAnimationFrame(this.rafCallback);
return this.animationFrameID;
};
}
/**
* @description Loop's state.
*/
get isActive() {
return this._isActive;
}
}
var Loop = new RAFLoop();
var AXIS_DIRECTION;
(function (AXIS_DIRECTION) {
AXIS_DIRECTION["X"] = "x";
AXIS_DIRECTION["Y"] = "y";
})(AXIS_DIRECTION || (AXIS_DIRECTION = {}));
var TRACK_CLICK_BEHAVIOR;
(function (TRACK_CLICK_BEHAVIOR) {
TRACK_CLICK_BEHAVIOR["JUMP"] = "jump";
TRACK_CLICK_BEHAVIOR["STEP"] = "step";
})(TRACK_CLICK_BEHAVIOR || (TRACK_CLICK_BEHAVIOR = {}));
class ScrollbarThumb extends React.Component {
constructor() {
super(...arguments);
this.element = null;
this.initialOffsetX = 0;
this.initialOffsetY = 0;
this.elementRefHack = React.createRef();
this.lastDragData = {
x: 0,
y: 0,
deltaX: 0,
deltaY: 0,
lastX: 0,
lastY: 0,
};
this.handleOnDragStart = (ev, data) => {
if (!this.element) {
this.handleOnDragStop(ev, data);
return;
}
if (isBrowser) {
this.prevUserSelect = document.body.style.userSelect;
document.body.style.userSelect = 'none';
this.prevOnSelectStart = document.onselectstart;
document.addEventListener('selectstart', ScrollbarThumb.selectStartReplacer);
}
if (this.props.onDragStart) {
this.props.onDragStart((this.lastDragData = {
x: data.x - this.initialOffsetX,
y: data.y - this.initialOffsetY,
lastX: data.lastX - this.initialOffsetX,
lastY: data.lastY - this.initialOffsetY,
deltaX: data.deltaX,
deltaY: data.deltaY,
}));
}
this.element.classList.add('dragging');
};
this.handleOnDrag = (ev, data) => {
if (!this.element) {
this.handleOnDragStop(ev, data);
return;
}
if (this.props.onDrag) {
this.props.onDrag((this.lastDragData = {
x: data.x - this.initialOffsetX,
y: data.y - this.initialOffsetY,
lastX: data.lastX - this.initialOffsetX,
lastY: data.lastY - this.initialOffsetY,
deltaX: data.deltaX,
deltaY: data.deltaY,
}));
}
};
this.handleOnDragStop = (ev, data) => {
const resultData = data
? {
x: data.x - this.initialOffsetX,
y: data.y - this.initialOffsetY,
lastX: data.lastX - this.initialOffsetX,
lastY: data.lastY - this.initialOffsetY,
deltaX: data.deltaX,
deltaY: data.deltaY,
}
: this.lastDragData;
if (this.props.onDragEnd)
this.props.onDragEnd(resultData);
if (this.element)
this.element.classList.remove('dragging');
if (isBrowser) {
document.body.style.userSelect = this.prevUserSelect;
if (this.prevOnSelectStart) {
document.addEventListener('selectstart', this.prevOnSelectStart);
}
this.prevOnSelectStart = null;
}
this.initialOffsetX = 0;
this.initialOffsetY = 0;
this.lastDragData = {
x: 0,
y: 0,
deltaX: 0,
deltaY: 0,
lastX: 0,
lastY: 0,
};
};
this.handleOnMouseDown = (ev) => {
if (!this.element) {
return;
}
ev.preventDefault();
ev.stopPropagation();
if (!isUndef(ev.offsetX)) {
/* istanbul ignore next */
this.initialOffsetX = ev.offsetX;
/* istanbul ignore next */
this.initialOffsetY = ev.offsetY;
}
else {
const rect = this.element.getBoundingClientRect();
this.initialOffsetX =
(ev.clientX || ev.touches[0].clientX) - rect.left;
this.initialOffsetY =
(ev.clientY || ev.touches[0].clientY) - rect.top;
}
};
this.elementRef = (ref) => {
if (isFun(this.props.elementRef))
this.props.elementRef(ref);
this.element = ref;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.elementRefHack.current = ref;
};
}
componentDidMount() {
if (!this.element) {
this.setState(() => {
throw new Error("<ScrollbarThumb> Element was not created. Possibly you haven't provided HTMLDivElement to renderer's `elementRef` function.");
});
}
}
componentWillUnmount() {
this.handleOnDragStop();
this.elementRef(null);
}
render() {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
elementRef, axis,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onDrag,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onDragEnd,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onDragStart, ...props } = this.props;
props.className = cnb('ScrollbarsCustom-Thumb', axis === AXIS_DIRECTION.X ? 'ScrollbarsCustom-ThumbX' : 'ScrollbarsCustom-ThumbY', props.className);
if (props.renderer) {
props.axis = axis;
}
return (React.createElement(DraggableCore, { allowAnyClick: false, enableUserSelectHack: false, onMouseDown: this.handleOnMouseDown, onDrag: this.handleOnDrag, onStart: this.handleOnDragStart, onStop: this.handleOnDragStop, nodeRef: this.elementRefHack }, renderDivWithRenderer(props, this.elementRef)));
}
}
ScrollbarThumb.selectStartReplacer = () => false;
class ScrollbarTrack extends React.Component {
constructor() {
super(...arguments);
this.element = null;
this.elementRef = (ref) => {
if (isFun(this.props.elementRef))
this.props.elementRef(ref);
this.element = ref;
};
this.handleClick = (ev) => {
if (!ev || !this.element || ev.button !== 0) {
return;
}
if (isFun(this.props.onClick) && ev.target === this.element) {
if (!isUndef(ev.offsetX)) {
this.props.onClick(ev, {
axis: this.props.axis,
offset: this.props.axis === AXIS_DIRECTION.X ? ev.offsetX : ev.offsetY,
});
}
else {
// support for old browsers
/* istanbul ignore next */
const rect = this.element.getBoundingClientRect();
/* istanbul ignore next */
this.props.onClick(ev, {
axis: this.props.axis,
offset: this.props.axis === AXIS_DIRECTION.X
? (ev.clientX || ev.touches[0].clientX) - rect.left
: (ev.clientY || ev.touches[0].clientY) - rect.top,
});
}
}
return true;
};
}
componentDidMount() {
if (!this.element) {
this.setState(() => {
throw new Error("Element was not created. Possibly you haven't provided HTMLDivElement to renderer's `elementRef` function.");
});
return;
}
this.element.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
if (this.element) {
this.element.removeEventListener('click', this.handleClick);
this.element = null;
this.elementRef(null);
}
}
render() {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
elementRef, axis,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onClick, ...props } = this.props;
props.className = cnb('ScrollbarsCustom-Track', axis === AXIS_DIRECTION.X ? 'ScrollbarsCustom-TrackX' : 'ScrollbarsCustom-TrackY', props.className);
if (props.renderer) {
props.axis = axis;
}
return renderDivWithRenderer(props, this.elementRef);
}
}
const style = {
holder: {
position: 'relative',
width: '100%',
height: '100%',
},
wrapper: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
content: {
boxSizing: 'border-box',
},
track: {
common: {
position: 'absolute',
overflow: 'hidden',
borderRadius: 4,
background: 'rgba(0,0,0,.1)',
userSelect: 'none',
},
x: {
height: 10,
width: 'calc(100% - 20px)',
bottom: 0,
left: 10,
},
y: {
width: 10,
height: 'calc(100% - 20px)',
top: 10,
},
},
thumb: {
common: {
cursor: 'pointer',
borderRadius: 4,
background: 'rgba(0,0,0,.4)',
},
x: {
height: '100%',
width: 0,
},
y: {
width: '100%',
height: 0,
},
},
};
let pageZoomLevel = isBrowser ? zoomLevel() : 1;
if (isBrowser) {
window.addEventListener('resize', () => {
pageZoomLevel = zoomLevel();
}, { passive: true });
}
const ScrollbarContext = React.createContext({
parentScrollbar: null,
});
class Scrollbar extends React.Component {
constructor(props) {
super(props);
/**
* @description Get current scroll-related values.<br/>
* If <i>force</i> if truthy - will recalculate them instead of returning cached values.
*
* @return ScrollState
*/
this.getScrollState = (force = false) => {
if (this.scrollValues && !force) {
return { ...this.scrollValues };
}
const scrollState = {
clientHeight: 0,
clientWidth: 0,
contentScrollHeight: 0,
contentScrollWidth: 0,
scrollHeight: 0,
scrollWidth: 0,
scrollTop: 0,
scrollLeft: 0,
scrollYBlocked: false,
scrollXBlocked: false,
scrollYPossible: false,
scrollXPossible: false,
trackYVisible: false,
trackXVisible: false,
zoomLevel: pageZoomLevel * 1,
isRTL: undefined,
};
const { props } = this;
scrollState.isRTL = this.state.isRTL;
scrollState.scrollYBlocked = props.noScroll || props.noScrollY;
scrollState.scrollXBlocked = props.noScroll || props.noScrollX;
if (this.scrollerElement) {
scrollState.clientHeight = this.scrollerElement.clientHeight;
scrollState.clientWidth = this.scrollerElement.clientWidth;
scrollState.scrollHeight = this.scrollerElement.scrollHeight;
scrollState.scrollWidth = this.scrollerElement.scrollWidth;
scrollState.scrollTop = this.scrollerElement.scrollTop;
scrollState.scrollLeft = this.scrollerElement.scrollLeft;
scrollState.scrollYPossible =
!scrollState.scrollYBlocked && scrollState.scrollHeight > scrollState.clientHeight;
scrollState.scrollXPossible =
!scrollState.scrollXBlocked && scrollState.scrollWidth > scrollState.clientWidth;
scrollState.trackYVisible =
scrollState.scrollYPossible || props.permanentTracks || props.permanentTrackY;
scrollState.trackXVisible =
scrollState.scrollXPossible || props.permanentTracks || props.permanentTrackX;
}
if (this.contentElement) {
scrollState.contentScrollHeight = this.contentElement.scrollHeight;
scrollState.contentScrollWidth = this.contentElement.scrollWidth;
}
return scrollState;
};
/**
* @description Scroll to top border
*/
this.scrollToTop = () => {
if (this.scrollerElement) {
this.scrollerElement.scrollTop = 0;
}
return this;
};
/**
* @description Scroll to left border
*/
this.scrollToLeft = () => {
if (this.scrollerElement) {
this.scrollerElement.scrollLeft = 0;
}
return this;
};
/**
* @description Scroll to bottom border
*/
this.scrollToBottom = () => {
if (this.scrollerElement) {
this.scrollerElement.scrollTop =
this.scrollerElement.scrollHeight - this.scrollerElement.clientHeight;
}
return this;
};
/**
* @description Scroll to right border
*/
this.scrollToRight = () => {
if (this.scrollerElement) {
this.scrollerElement.scrollLeft =
this.scrollerElement.scrollWidth - this.scrollerElement.clientWidth;
}
return this;
};
/**
* @description Set the scrolls at given coordinates.<br/>
* If coordinate is undefined - current scroll value will persist.
*/
this.scrollTo = (x, y) => {
if (this.scrollerElement) {
if (isNum(x))
this.scrollerElement.scrollLeft = x;
if (isNum(y))
this.scrollerElement.scrollTop = y;
}
return this;
};
/**
* @description Center the viewport at given coordinates.<br/>
* If coordinate is undefined - current scroll value will persist.
*/
this.centerAt = (x, y) => {
if (this.scrollerElement) {
if (isNum(x))
this.scrollerElement.scrollLeft = x - this.scrollerElement.clientWidth / 2;
if (isNum(y))
this.scrollerElement.scrollTop = y - this.scrollerElement.clientHeight / 2;
}
return this;
};
this.update = (force = false) => {
if (!this.scrollerElement) {
return;
}
// autodetect direction if not defined
if (isUndef(this.state.isRTL)) {
this.setState({
isRTL: getComputedStyle(this.scrollerElement).direction === 'rtl',
});
return this.getScrollState();
}
const scrollState = this.getScrollState(true);
const prevScrollState = { ...this.scrollValues };
const { props } = this;
let bitmask = 0;
if (!force) {
if (prevScrollState.clientHeight !== scrollState.clientHeight)
bitmask |= Math.trunc(1);
if (prevScrollState.clientWidth !== scrollState.clientWidth)
bitmask |= 1 << 1;
if (prevScrollState.scrollHeight !== scrollState.scrollHeight)
bitmask |= 1 << 2;
if (prevScrollState.scrollWidth !== scrollState.scrollWidth)
bitmask |= 1 << 3;
if (prevScrollState.scrollTop !== scrollState.scrollTop)
bitmask |= 1 << 4;
if (prevScrollState.scrollLeft !== scrollState.scrollLeft)
bitmask |= 1 << 5;
if (prevScrollState.scrollYBlocked !== scrollState.scrollYBlocked)
bitmask |= 1 << 6;
if (prevScrollState.scrollXBlocked !== scrollState.scrollXBlocked)
bitmask |= 1 << 7;
if (prevScrollState.scrollYPossible !== scrollState.scrollYPossible)
bitmask |= 1 << 8;
if (prevScrollState.scrollXPossible !== scrollState.scrollXPossible)
bitmask |= 1 << 9;
if (prevScrollState.trackYVisible !== scrollState.trackYVisible)
bitmask |= 1 << 10;
if (prevScrollState.trackXVisible !== scrollState.trackXVisible)
bitmask |= 1 << 11;
if (prevScrollState.isRTL !== scrollState.isRTL)
bitmask |= 1 << 12;
if (prevScrollState.contentScrollHeight !== scrollState.contentScrollHeight)
bitmask |= 1 << 13;
if (prevScrollState.contentScrollWidth !== scrollState.contentScrollWidth)
bitmask |= 1 << 14;
if (prevScrollState.zoomLevel !== scrollState.zoomLevel)
bitmask |= 1 << 15;
// if not forced and nothing has changed - skip this update
if (bitmask === 0) {
return prevScrollState;
}
}
else {
bitmask = 32767;
}
if (!props.native && this.holderElement) {
if (bitmask & (1 << 13) &&
(props.translateContentSizesToHolder || props.translateContentSizeYToHolder)) {
this.holderElement.style.height = `${scrollState.contentScrollHeight}px`;
}
if (bitmask & (1 << 14) &&
(props.translateContentSizesToHolder || props.translateContentSizeXToHolder)) {
this.holderElement.style.width = `${scrollState.contentScrollWidth}px`;
}
if (props.translateContentSizesToHolder ||
props.translateContentSizeYToHolder ||
props.translateContentSizeXToHolder) {
if ((!scrollState.clientHeight && scrollState.contentScrollHeight) ||
(!scrollState.clientWidth && scrollState.contentScrollWidth)) {
return;
}
}
}
// if scrollbars visibility has changed
if (bitmask & (1 << 10) || bitmask & (1 << 11)) {
prevScrollState.scrollYBlocked = scrollState.scrollYBlocked;
prevScrollState.scrollXBlocked = scrollState.scrollXBlocked;
prevScrollState.scrollYPossible = scrollState.scrollYPossible;
prevScrollState.scrollXPossible = scrollState.scrollXPossible;
if (this.trackYElement && bitmask & (1 << 10)) {
this.trackYElement.style.display = scrollState.trackYVisible ? '' : 'none';
}
if (this.trackXElement && bitmask & (1 << 11)) {
this.trackXElement.style.display = scrollState.trackXVisible ? '' : 'none';
}
this.scrollValues = prevScrollState;
this.setState({
trackYVisible: (this.scrollValues.trackYVisible = scrollState.trackYVisible),
trackXVisible: (this.scrollValues.trackXVisible = scrollState.trackXVisible),
});
return;
}
(props.native ? this.updaterNative : this.updaterCustom)(bitmask, scrollState);
this.scrollValues = scrollState;
if (!props.native && bitmask & (1 << 15)) {
getScrollbarWidth(true);
this.forceUpdate();
}
this.eventEmitter.emit('update', { ...scrollState }, prevScrollState);
if (bitmask & (1 << 4) || bitmask & (1 << 5))
this.eventEmitter.emit('scroll', { ...scrollState }, prevScrollState);
return this.scrollValues;
};
// eslint-disable-next-line class-methods-use-this
this.updaterNative = () => {
// just for future
return true;
};
this.updaterCustom = (bitmask, scrollValues) => {
const { props } = this;
if (this.trackYElement) {
if (this.thumbYElement &&
(bitmask & Math.trunc(1) ||
bitmask & (1 << 2) ||
bitmask & (1 << 4) ||
bitmask & (1 << 6) ||
bitmask & (1 << 8))) {
if (scrollValues.scrollYPossible) {
const trackInnerSize = getInnerHeight(this.trackYElement);
const thumbSize = calcThumbSize(scrollValues.scrollHeight, scrollValues.clientHeight, trackInnerSize, props.minimalThumbYSize || props.minimalThumbSize, props.maximalThumbYSize || props.maximalThumbSize);
const thumbOffset = calcThumbOffset(scrollValues.scrollHeight, scrollValues.clientHeight, trackInnerSize, thumbSize, scrollValues.scrollTop);
this.thumbYElement.style.transform = `translateY(${thumbOffset}px)`;
this.thumbYElement.style.height = `${thumbSize}px`;
this.thumbYElement.style.display = '';
}
else {
this.thumbYElement.style.transform = '';
this.thumbYElement.style.height = '0px';
this.thumbYElement.style.display = 'none';
}
}
}
if (this.trackXElement) {
if (this.thumbXElement &&
(bitmask & (1 << 1) ||
bitmask & (1 << 3) ||
bitmask & (1 << 5) ||
bitmask & (1 << 7) ||
bitmask & (1 << 9) ||
bitmask & (1 << 12))) {
if (scrollValues.scrollXPossible) {
const trackInnerSize = getInnerWidth(this.trackXElement);
const thumbSize = calcThumbSize(scrollValues.scrollWidth, scrollValues.clientWidth, trackInnerSize, props.minimalThumbXSize || props.minimalThumbSize, props.maximalThumbXSize || props.maximalThumbSize);
let thumbOffset = calcThumbOffset(scrollValues.scrollWidth, scrollValues.clientWidth, trackInnerSize, thumbSize, scrollValues.scrollLeft);
if (this.state.isRTL && shouldReverseRtlScroll()) {
thumbOffset += trackInnerSize - thumbSize;
}
this.thumbXElement.style.transform = `translateX(${thumbOffset}px)`;
this.thumbXElement.style.width = `${thumbSize}px`;
this.thumbXElement.style.display = '';
}
else {
this.thumbXElement.style.transform = '';
this.thumbXElement.style.width = '0px';
this.thumbXElement.style.display = 'none';
}
}
}
return true;
};
this.elementRefHolder = (ref) => {
this.holderElement = ref;
if (isFun(this.props.elementRef)) {
this.props.elementRef(ref);
}
};
this.elementRefWrapper = (ref) => {
this.wrapperElement = ref;
if (isFun(this.props.wrapperProps.elementRef)) {
this.props.wrapperProps.elementRef(ref);
}
};
this.elementRefScroller = (ref) => {
this.scrollerElement = ref;
if (isFun(this.props.scrollerProps.elementRef)) {
this.props.scrollerProps.elementRef(ref);
}
};
this.elementRefContent = (ref) => {
this.contentElement = ref;
if (isFun(this.props.contentProps.elementRef)) {
this.props.contentProps.elementRef(ref);
}
};
this.elementRefTrackX = (ref) => {
this.trackXElement = ref;
if (isFun(this.props.trackXProps.elementRef)) {
this.props.trackXProps.elementRef(ref);
}
};
this.elementRefTrackY = (ref) => {
this.trackYElement = ref;
if (isFun(this.props.trackYProps.elementRef)) {
this.props.trackYProps.elementRef(ref);
}
};
this.elementRefThumbX = (ref) => {
this.thumbXElement = ref;
if (isFun(this.props.thumbXProps.elementRef)) {
this.props.thumbXProps.elementRef(ref);
}
};
this.elementRefThumbY = (ref) => {
this.thumbYElement = ref;
if (isFun(this.props.thumbYProps.elementRef)) {
this.props.thumbYProps.elementRef(ref);
}
};
this.handleTrackXClick = (ev, values) => {
if (this.props.trackXProps.onClick) {
this.props.trackXProps.onClick(ev, values);
}
if (!this.scrollerElement ||
!this.trackXElement ||
!this.thumbXElement ||
!this.scrollValues ||
!this.scrollValues.scrollXPossible) {
return;
}
this._scrollDetection();
const thumbSize = this.thumbXElement.clientWidth;
const trackInnerSize = getInnerWidth(this.trackXElement);
const thumbOffset = (this.scrollValues.isRTL && shouldReverseRtlScroll()
? values.offset + thumbSize / 2 - trackInnerSize
: values.offset - thumbSize / 2) -
(Number.parseFloat(getComputedStyle(this.trackXElement).paddingLeft) || 0);
let target = calcScrollForThumbOffset(this.scrollValues.scrollWidth, this.scrollValues.clientWidth, trackInnerSize, thumbSize, thumbOffset);
if (this.props.trackClickBehavior === TRACK_CLICK_BEHAVIOR.STEP) {
target = (this.scrollValues.isRTL
? this.scrollValues.scrollLeft > target
: this.scrollValues.scrollLeft < target)
? this.scrollValues.scrollLeft + this.scrollValues.clientWidth
: this.scrollValues.scrollLeft - this.scrollValues.clientWidth;
}
this.scrollerElement.scrollLeft = target;
};
this.handleTrackYClick = (ev, values) => {
if (this.props.trackYProps.onClick)
this.props.trackYProps.onClick(ev, values);
if (!this.scrollerElement ||
!this.trackYElement ||
!this.thumbYElement ||
!this.scrollValues ||
!this.scrollValues.scrollYPossible) {
return;
}
this._scrollDetection();
const thumbSize = this.thumbYElement.clientHeight;
const target = calcScrollForThumbOffset(this.scrollValues.scrollHeight, this.scrollValues.clientHeight, getInnerHeight(this.trackYElement), thumbSize, values.offset - thumbSize / 2) - (Number.parseFloat(getComputedStyle(this.trackYElement).paddingTop) || 0);
if (this.props.trackClickBehavior === TRACK_CLICK_BEHAVIOR.JUMP) {
this.scrollerElement.scrollTop = target;
}
else {
this.scrollerElement.scrollTop =
this.scrollValues.scrollTop < target
? this.scrollValues.scrollTop + this.scrollValues.clientHeight
: this.scrollValues.scrollTop - this.scrollValues.clientHeight;
}
};
this.handleTrackYMouseWheel = (ev) => {
const { props } = this;
if (props.trackYProps && props.trackYProps.onWheel) {
props.trackYProps.onWheel(ev);
}
if (props.disableTracksMousewheelScrolling || props.disableTrackYMousewheelScrolling) {
return;
}
this._scrollDetection();
if (!this.scrollerElement || this.scrollValues.scrollYBlocked) {
return;
}
this.scrollTop += ev.deltaY;
};
this.handleTrackXMouseWheel = (ev) => {
const { props } = this;
if (props.trackXProps && props.trackXProps.onWheel) {
props.trackXProps.onWheel(ev);
}
if (props.disableTracksMousewheelScrolling || props.disableTrackXMousewheelScrolling) {
return;
}
this._scrollDetection();
if (!this.scrollerElement || this.scrollValues.scrollXBlocked) {
return;
}
this.scrollLeft += ev.deltaX;
};
this.handleThumbXDrag = (data) => {
var _a;
if (!this.trackXElement ||
!this.thumbXElement ||
!this.scrollerElement ||
!this.scrollValues ||
!this.scrollValues.scrollXPossible) {
return;
}
this._scrollDetection();
const trackRect = this.trackXElement.getBoundingClientRect();
const styles = getComputedStyle(this.trackXElement);
const paddingLeft = Number.parseFloat(styles.paddingLeft) || 0;
const paddingRight = Number.parseFloat(styles.paddingRight) || 0;
const trackInnerSize = trackRect.width - paddingLeft - paddingRight;
const thumbSize = this.thumbXElement.clientWidth;
const offset = this.scrollValues.isRTL && shouldReverseRtlScroll()
? data.x + thumbSize - trackInnerSize + paddingLeft
: data.lastX - paddingLeft;
this.scrollerElement.scrollLeft = calcScrollForThumbOffset(this.scrollValues.scrollWidth, this.scrollValues.clientWidth, trackInnerSize, thumbSize, offset);
if ((_a = this.props.thumbXProps) === null || _a === void 0 ? void 0 : _a.onDrag) {
this.props.thumbXProps.onDrag(data);
}
};
this.handleThumbXDragEnd = (data) => {
var _a;
this.handleThumbXDrag(data);
if ((_a = this.props.thumbXProps) === null || _a === void 0 ? void 0 : _a.onDragEnd) {
this.props.thumbXProps.onDragEnd(data);
}
};
this.handleThumbYDrag = (data) => {
var _a;
if (!this.scrollerElement ||
!this.trackYElement ||
!this.thumbYElement ||
!this.scrollValues ||
!this.scrollValues.scrollYPossible) {
return;
}
this._scrollDetection();
const trackRect = this.trackYElement.getBoundingClientRect();
const styles = getComputedStyle(this.trackYElement);
const paddingTop = Number.parseFloat(styles.paddingTop) || 0;
const paddingBottom = Number.parseFloat(styles.paddingBottom) || 0;
const trackInnerSize = trackRect.height - paddingTop - paddingBottom;
const thumbSize = this.thumbYElement.clientHeight;
const offset = data.y - paddingTop;
this.scrollerElement.scrollTop = calcScrollForThumbOffset(this.scrollValues.scrollHeight, this.scrollValues.clientHeight, trackInnerSize, thumbSize, offset);
if ((_a = this.props.thumbYProps) === null || _a === void 0 ? void 0 : _a.onDrag) {
this.props.thumbYProps.onDrag(data);
}
};
this.handleThumbYDragEnd = (data) => {
var _a;
this.handleThumbYDrag(data);
if ((_a = this.props.thumbYProps) === null || _a === void 0 ? void 0 : _a.onDragEnd) {
this.props.thumbYProps.onDragEnd(data);
}
};
this.handleScrollerScroll = () => {
this._scrollDetection();
};
this._scrollDetection = () => {
if (!this._scrollDetectionTO) {
this.eventEmitter.emit('scrollStart', this.getScrollState());
}
else if (isBrowser) {
window.clearTimeout(this._scrollDetectionTO);
}
this._scrollDetectionTO = isBrowser
? window.setTimeout(this._scrollDetectionCallback, this.props.scrollDetectionThreshold || 0)
: null;
};
this._scrollDetectionCallback = () => {
this._scrollDetectionTO = null;
this.eventEmitter.emit('scrollStop', this.getScrollState());
};
this.state = {
trackXVisible: false,
trackYVisible: false,
isRTL: props.rtl,
};
this.scrollValues = this.getScrollState(true);
this.eventEmitter = new Emittr(15);
if (props.onUpdate)
this.eventEmitter.on('update', props.onUpdate);
if (props.onScroll)
this.eventEmitter.on('scroll', props.onScroll);
if (props.onScrollStart)
this.eventEmitter.on('scrollStart', props.onScrollStart);
if (props.onScrollStop)
this.eventEmitter.on('scrollStop', props.onScrollStop);
this.id = uuid();
}
// eslint-disable-next-line react/sort-comp
get scrollTop() {
if (this.scrollerElement) {
return this.scrollerElement.scrollTop;
}
return 0;
}
set scrollTop(top) {
if (this.scrollerElement) {
this.scrollerElement.scrollTop = top;
this.update();
}
}
get scrollLeft() {
if (this.scrollerElement) {
return this.scrollerElement.scrollLeft;
}
return 0;
}
set scrollLeft(left) {
if (this.scrollerElement) {
this.scrollerElement.scrollLeft = left;
}
}
get scrollHeight() {
if (this.scrollerElement) {
return this.scrollerElement.scrollHeight;
}
return 0;
}
get scrollWidth() {
if (this.scrollerElement) {
return this.scrollerElement.scrollWidth;
}
return