@saleae/react-scrollbars-custom
Version:
The best React custom scrollbars component
1,190 lines (1,181 loc) • 64.6 kB
JavaScript
import cnb from 'cnbuilder';
import { oneOf, func, bool, number, string, object } from 'prop-types';
import { createElement, Component, createContext } from 'react';
import { zoomLevel } from 'zoom-level';
import { DraggableCore } from 'react-draggable';
let doc = typeof document === "object" ? document : null;
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.renderer;
delete props.renderer;
return renderer(props);
}
delete props.elementRef;
return createElement("div", Object.assign({}, props, { ref: elementRef }));
};
const getInnerSize = (el, dimension, padding1, padding2) => {
const styles = getComputedStyle(el);
if (styles.boxSizing === "border-box") {
return Math.max(0, (parseFloat(styles[dimension]) || 0) -
(parseFloat(styles[padding1]) || 0) -
(parseFloat(styles[padding2]) || 0));
}
return 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 = () => {
let uuid = "";
for (let i = 0; i < 32; i++) {
if (i === 8 || i === 20) {
uuid += "-" + ((Math.random() * 16) | 0).toString(16);
}
else if (i === 12) {
uuid += "-4";
}
else if (i === 16) {
uuid += "-" + ((Math.random() * 16) | (0 & 3) | 8).toString(16);
}
else {
uuid += ((Math.random() * 16) | 0).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;
isNum(maximalSize) && (thumbSize = Math.min(maximalSize, thumbSize));
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) {
return (getScrollbarWidth._cache = 0);
}
if (!force && !isUndef(getScrollbarWidth._cache)) {
return getScrollbarWidth._cache;
}
let el = doc.createElement("div");
el.setAttribute("style", "position:absolute;width:100px;height:100px;top:-999px;left:-999px;overflow:scroll;");
doc.body.appendChild(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
doc.body.removeChild(el);
return;
}
getScrollbarWidth._cache = 100 - el.clientWidth;
doc.body.removeChild(el);
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) {
return (shouldReverseRtlScroll._cache = false);
}
const el = doc.createElement("div");
const child = doc.createElement("div");
el.appendChild(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.appendChild(el);
el.scrollLeft = -50;
shouldReverseRtlScroll._cache = el.scrollLeft === -50;
doc.body.removeChild(el);
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, eventName;
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);
prepend ? emitter._handlers[name].unshift(handler) : 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;
}
idx === 0 ? emitter._handlers[name].shift() : 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;
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;
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.indexOf(target) === -1) {
this.targets.push(target);
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);
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++) {
!this.targets[i]._unmounted && this.targets[i].update();
}
return (this.animationFrameID = requestAnimationFrame(this.rafCallback));
};
}
/**
* @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 = {}));
const AXIS_DIRECTION_PROP_TYPE = oneOf([AXIS_DIRECTION.X, AXIS_DIRECTION.Y]);
var TRACK_CLICK_BEHAVIOR;
(function (TRACK_CLICK_BEHAVIOR) {
TRACK_CLICK_BEHAVIOR["JUMP"] = "jump";
TRACK_CLICK_BEHAVIOR["STEP"] = "step";
})(TRACK_CLICK_BEHAVIOR || (TRACK_CLICK_BEHAVIOR = {}));
const TRACK_CLICK_BEHAVIOR_PROP_TYPE = oneOf([TRACK_CLICK_BEHAVIOR.JUMP, TRACK_CLICK_BEHAVIOR.STEP]);
class ScrollbarThumb extends Component {
constructor() {
super(...arguments);
this.initialOffsetX = 0;
this.initialOffsetY = 0;
this.lastDragData = {
x: 0,
y: 0,
deltaX: 0,
deltaY: 0,
lastX: 0,
lastY: 0
};
this.element = null;
this.handleOnDragStart = (ev, data) => {
if (!this.element) {
this.handleOnDragStop(ev, data);
return;
}
if (global.document) {
this.prevUserSelect = global.document.body.style.userSelect;
global.document.body.style.userSelect = "none";
this.prevOnSelectStart = global.document.onselectstart;
global.document.onselectstart = ScrollbarThumb.selectStartReplacer;
}
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;
}
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
}), false);
};
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;
this.props.onDragEnd && this.props.onDragEnd(resultData, true);
this.element && this.element.classList.remove("dragging");
if (global.document) {
global.document.body.style.userSelect = this.prevUserSelect;
global.document.onselectstart = 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) => {
isFun(this.props.elementRef) && this.props.elementRef(ref);
this.element = ref;
};
}
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;
}
}
componentWillUnmount() {
this.handleOnDragStop();
this.elementRef(null);
}
render() {
const { elementRef, axis, onDrag, onDragEnd, 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 (createElement(DraggableCore, { allowAnyClick: false, enableUserSelectHack: false, onMouseDown: this.handleOnMouseDown, onDrag: this.handleOnDrag, onStart: this.handleOnDragStart, onStop: this.handleOnDragStop, children: renderDivWithRenderer(props, this.elementRef) }));
}
}
ScrollbarThumb.propTypes = {
axis: AXIS_DIRECTION_PROP_TYPE,
onDrag: func,
onDragStart: func,
onDragEnd: func,
elementRef: func,
renderer: func
};
ScrollbarThumb.selectStartReplacer = () => false;
class ScrollbarTrack extends Component {
constructor() {
super(...arguments);
this.element = null;
this.elementRef = (ref) => {
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 { elementRef, axis, 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);
}
}
ScrollbarTrack.propTypes = {
axis: AXIS_DIRECTION_PROP_TYPE,
onClick: func,
elementRef: func,
renderer: func
};
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 = global.window ? zoomLevel() : 1;
global.window && global.window.addEventListener("resize", () => (pageZoomLevel = zoomLevel()), { passive: true });
const ScrollbarContext = createContext({
parentScrollbar: null
});
class Scrollbar extends 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 };
}
let 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.props;
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) {
isNum(x) && (this.scrollerElement.scrollLeft = x);
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) {
isNum(x) && (this.scrollerElement.scrollLeft = x - this.scrollerElement.clientWidth / 2);
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.props;
let bitmask = 0;
if (!force) {
prevScrollState.clientHeight !== scrollState.clientHeight && (bitmask |= 1 << 0);
prevScrollState.clientWidth !== scrollState.clientWidth && (bitmask |= 1 << 1);
prevScrollState.scrollHeight !== scrollState.scrollHeight && (bitmask |= 1 << 2);
prevScrollState.scrollWidth !== scrollState.scrollWidth && (bitmask |= 1 << 3);
prevScrollState.scrollTop !== scrollState.scrollTop && (bitmask |= 1 << 4);
prevScrollState.scrollLeft !== scrollState.scrollLeft && (bitmask |= 1 << 5);
prevScrollState.scrollYBlocked !== scrollState.scrollYBlocked && (bitmask |= 1 << 6);
prevScrollState.scrollXBlocked !== scrollState.scrollXBlocked && (bitmask |= 1 << 7);
prevScrollState.scrollYPossible !== scrollState.scrollYPossible && (bitmask |= 1 << 8);
prevScrollState.scrollXPossible !== scrollState.scrollXPossible && (bitmask |= 1 << 9);
prevScrollState.trackYVisible !== scrollState.trackYVisible && (bitmask |= 1 << 10);
prevScrollState.trackXVisible !== scrollState.trackXVisible && (bitmask |= 1 << 11);
prevScrollState.isRTL !== scrollState.isRTL && (bitmask |= 1 << 12);
prevScrollState.contentScrollHeight !== scrollState.contentScrollHeight && (bitmask |= 1 << 13);
prevScrollState.contentScrollWidth !== scrollState.contentScrollWidth && (bitmask |= 1 << 14);
prevScrollState.zoomLevel !== scrollState.zoomLevel && (bitmask |= 1 << 15);
// if not forced and nothing has changed - skip this update
if (bitmask === 0) {
return prevScrollState;
}
}
else {
bitmask = 0b111111111111111;
}
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 ? null : "none";
}
if (this.trackXElement && bitmask & (1 << 11)) {
this.trackXElement.style.display = scrollState.trackXVisible ? null : "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);
(bitmask & (1 << 4) || bitmask & (1 << 5)) && this.eventEmitter.emit("scroll", { ...scrollState }, prevScrollState);
return this.scrollValues;
};
this.updaterNative = () => {
// just for future
return true;
};
this.updaterCustom = (bitmask, scrollValues) => {
const props = this.props;
if (this.trackYElement) {
if (this.thumbYElement &&
(bitmask & (1 << 0) || 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;
isFun(this.props.elementRef) && this.props.elementRef(ref);
};
this.elementRefWrapper = (ref) => {
this.wrapperElement = ref;
isFun(this.props.wrapperProps.elementRef) && this.props.wrapperProps.elementRef(ref);
};
this.elementRefScroller = (ref) => {
this.scrollerElement = ref;
isFun(this.props.scrollerProps.elementRef) && this.props.scrollerProps.elementRef(ref);
};
this.elementRefContent = (ref) => {
this.contentElement = ref;
isFun(this.props.contentProps.elementRef) && this.props.contentProps.elementRef(ref);
};
this.elementRefTrackX = (ref) => {
this.trackXElement = ref;
isFun(this.props.trackXProps.elementRef) && this.props.trackXProps.elementRef(ref);
};
this.elementRefTrackY = (ref) => {
this.trackYElement = ref;
isFun(this.props.trackYProps.elementRef) && this.props.trackYProps.elementRef(ref);
};
this.elementRefThumbX = (ref) => {
this.thumbXElement = ref;
isFun(this.props.thumbXProps.elementRef) && this.props.thumbXProps.elementRef(ref);
};
this.elementRefThumbY = (ref) => {
this.thumbYElement = ref;
isFun(this.props.thumbYProps.elementRef) && this.props.thumbYProps.elementRef(ref);
};
this.handleTrackXClick = (ev, values) => {
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) -
//@ts-ignore
(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) => {
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;
let target = calcScrollForThumbOffset(this.scrollValues.scrollHeight, this.scrollValues.clientHeight, getInnerHeight(this.trackYElement), thumbSize, values.offset - thumbSize / 2) -
//@ts-ignore
(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.props;
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.props;
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, isEnd = false) => {
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);
//@ts-ignore
const paddingLeft = parseFloat(styles.paddingLeft) || 0;
//@ts-ignore
const paddingRight = 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 (isEnd && this.props.thumbXProps && this.props.thumbXProps.onDragEnd) {
this.props.thumbXProps.onDragEnd(data, true);
}
};
this.handleThumbYDrag = (data, isEnd = false) => {
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);
//@ts-ignore
const paddingTop = parseFloat(styles.paddingTop) || 0;
//@ts-ignore
const paddingBottom = 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 (isEnd && this.props.thumbYProps && this.props.thumbYProps.onDragEnd) {
this.props.thumbYProps.onDragEnd(data, true);
}
};
this.handleScrollerScroll = () => {
this._scrollDetection();
};
this._scrollDetection = () => {
!this._scrollDetectionTO && this.eventEmitter.emit("scrollStart", this.getScrollState());
this._scrollDetectionTO && global.window && global.window.clearTimeout(this._scrollDetectionTO);
this._scrollDetectionTO = global.window
? global.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);
props.onUpdate && this.eventEmitter.on("update", props.onUpdate);
props.onScroll && this.eventEmitter.on("scroll", props.onScroll);
props.onScrollStart && this.eventEmitter.on("scrollStart", props.onScrollStart);
props.onScrollStop && this.eventEmitter.on("scrollStop", props.onScrollStop);
this.id = uuid();
}
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 0;
}
get clientHeight() {
if (this.scrollerElement) {
return this.scrollerElement.clientHeight;
}
return 0;
}
get clientWidth() {
if (this.scrollerElement) {
return this.scrollerElement.clientWidth;
}
return 0;
}
static calculateStyles(props, state, scrollValues, scrollbarWidth) {
const useDefaultStyles = !props.noDefaultStyles;
return {
holder: {
...(useDefaultStyles && style.holder),
position: "relative",
...props.style
},
wrapper: {
...(useDefaultStyles && {
...style.wrapper,
...(!props.disableTracksWidthCompensation &&
!props.disableTrackYWidthCompensation && {
[state.isRTL ? "left" : "right"]: state.trackYVisible ? 10 : 0
}),
...(!props.disableTracksWidthCompensation &&
!props.disableTrackXWidthCompensation && {
bottom: state.trackXVisible ? 10 : 0
})
}),
...props.wrapperProps.style,
position: "absolute",
overflow: "hidden"
},
content: {
...(useDefaultStyles && style.content),
...(props.translateContentSizesToHolder ||
props.translateContentSizeYToHolder ||
props.translateContentSizeXToHolder
? {
display: "table-cell"
}
: {
padding: 0.05 // needed to disable margin collapsing without flexboxes, other possible solutions here: https://stackoverflow.com/questions/19718634/how-to-disable-margin-collapsing
}),
...(useDefaultStyles &&
!(props.translateContentSizesToHolder || props.translateContentSizeYToHolder) && {
minHeight: "100%"
}),
...(useDefaultStyles &&
!(props.translateContentSizesToHolder || props.translateContentSizeXToHolder) && {
minWidth: "100%"
}),
...props.contentProps.style
},
scroller: {
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
paddingBottom: !scrollbarWidth && scrollValues.scrollXPossible ? props.fallbackScrollbarWidth : undefined,
[state.isRTL ? "paddingLeft" : "paddingRight"]: !scrollbarWidth && scrollValues.scrollYPossible ? props.fallbackScrollbarWidth : undefined,