Skip to content

Instantly share code, notes, and snippets.

@tkh44
Created April 11, 2016 04:54
Show Gist options
  • Save tkh44/b20a330ed69ba56915c89190e04fe96d to your computer and use it in GitHub Desktop.
Save tkh44/b20a330ed69ba56915c89190e04fe96d to your computer and use it in GitHub Desktop.
reactScrollbar rewrite. I'm not sure about bugs in here. We ended up using another library, but this is what I had in my stash.
.scrollarea-content {
margin: 0;
padding: 0;
overflow: hidden;
position: relative;
will-change: transform;
}
.scrollarea {
position: relative;
overflow: hidden;
.scrollbar-container{
position: absolute;
background: none;
opacity: .1;
z-index: 9999;
&.horizontal{
width: 100%;
height: 10px;
left: 0;
bottom: 0;
.scrollbar{
width: 20px;
height: 8px;
background: $color-darkgrey;
margin-top: 1px;
}
}
&.vertical{
width: 10px;
height: 100%;
right: 0;
top: 0;
.scrollbar{
width: 8px;
height: 20px;
background: $color-darkgrey;
margin-left: 1px;
border-radius: $base-border-radius;
}
}
&:hover{
background: $color-lightgrey;
opacity: .6;
}
&.active{
background: $color-lightgrey;
opacity: .6;
}
}
&:hover .scrollbar-container{
opacity: .3;
}
}
import { Component, PropTypes, createElement, DOM } from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import Measure from 'react-measure';
import { addListener } from 'utils/events';
import ScrollBar from './Scrollbar';
import lineHeight from 'line-height';
import { Motion, spring, presets } from 'react-motion';
import { actions, normalizeTopPosition } from './state';
const SPRING_SETTING = { stiffness: 170, damping: 26 };
const { div } = DOM;
const { updateScroll, bottom } = actions;
const eventTypes = {
wheel: 'wheel',
api: 'api',
touch: 'touch',
touchEnd: 'touchEnd',
mousemove: 'mousemove',
mouseup: 'mouseup',
resize: 'resize'
};
const shouldMeasureHeight = (mutations) => {
return mutations ? mutations[0].target : false;
};
// const useMotion = (eventType) => (eventType === eventTypes.wheel ||
// eventType === eventTypes.api ||
// eventType === eventTypes.touchEnd);
const useMotion = () => false;
const scrollRatio = (containerHeight, realHeight) => (containerHeight / realHeight);
class ScrollArea extends Component {
static propTypes = {
className: PropTypes.string,
style: PropTypes.object,
speed: PropTypes.number,
contentClassName: PropTypes.string,
contentStyle: PropTypes.object,
verticalContainerStyle: PropTypes.object,
verticalScrollbarStyle: PropTypes.object,
onScroll: PropTypes.func,
minScrollSize: PropTypes.number,
swapWheelAxes: PropTypes.bool
};
static childContextTypes = {
scrollArea: PropTypes.object
};
static defaultProps = {
speed: 1,
vertical: true,
horizontal: true,
smoothScrolling: false,
swapWheelAxes: false
};
componentDidMount() {
this.mouseMoveListener = addListener(window, 'mousemove', this.handleMouseMove);
this.mouseUpListener = addListener(window, 'mouseup', this.handleMouseUp);
this.resizeListener = addListener(window, 'resize', this.handleWindowResize);
this.handleWindowResize();
this.props.dispatch(bottom());
}
componentWillUnmount() {
this.resizeListener();
this.mouseMoveListener();
this.mouseUpListener();
}
render() {
const { contentStyle, scroller: { topPosition, eventType } } = this.props;
const motionStyles = useMotion(eventType) ?
merge(contentStyle, { y: spring(-topPosition, SPRING_SETTING) }) :
merge(contentStyle, { y: -topPosition });
return createElement(Measure, {
whitelist: ['height', 'top'],
shouldMeasure: shouldMeasureHeight,
onMeasure: this.handleContainerMeasure
},
createElement(Motion, { style: motionStyles }, this.renderWithCurrentStyles)
);
}
renderWithCurrentStyles = (currentStyles) => {
const {
children,
className,
contentClassName,
scroller,
style,
verticalContainerStyle,
verticalScrollbarStyle
} = this.props;
return createElement(Measure, {
whitelist: ['height', 'top'],
shouldMeasure: shouldMeasureHeight,
onMeasure: this.handleContainerMeasure
},
div({
ref: (el) => (this.wrapper = el),
className: cn('scrollarea', className),
onWheel: this.handleWheel,
style
},
createElement(Measure,{
whitelist: ['height'],
accurate: true,
shouldMeasure: shouldMeasureHeight,
onMeasure: this.handleContentMeasure
},
div({
ref: (el) => (this.content = el),
style: Object.assign({}, this.props.contentStyle, {
transform: `translate3d(0, ${currentStyles.y}px, 0)`
}),
className: cn('scrollarea-content', contentClassName),
onTouchStart: this.handleTouchStart,
onTouchMove: this.handleTouchMove,
onTouchEnd: this.handleTouchEnd
}, children)
),
createElement(ScrollBar, {
scroller,
springSetting: SPRING_SETTING,
smoothScrolling: useMotion(scroller.eventType),
onScrollbarMouseDown: this.handleScrollBarMouseDown,
onScrollbarContainerMouseDown: this.handleScrollbarContainerMouseDown,
containerStyle: verticalContainerStyle,
scrollbarStyle: verticalScrollbarStyle
})
)
);
};
handleContainerMeasure = (dimensions, mutations, target) => {
console.log('handleContainerMeasure called');
console.log(dimensions, mutations, target);
const { dispatch } = this.props;
dispatch(updateScroll({ containerHeight: dimensions.height }));
};
handleContentMeasure = (dimensions, mutations, target) => {
console.log('handleContentMeasure called');
console.log(dimensions, mutations, target);
const { dispatch } = this.props;
dispatch(updateScroll({ realHeight: dimensions.height }));
};
handleWindowResize = () => {
const { dispatch } = this.props;
const {
height: containerHeight,
width: containerWidth,
top: containerTop
} = this.wrapper.getBoundingClientRect();
const {
height: realHeight,
width: realWidth,
top: realTop
} = this.content.getBoundingClientRect();
const lineHeightPx = lineHeight(this.content);
dispatch(updateScroll({
containerHeight,
containerWidth,
containerTop,
realHeight,
realWidth,
realTop,
lineHeightPx
}, eventTypes.resize));
};
handleWheel = (e) => {
const { dispatch, scroller, speed } = this.props;
const { topPosition, lineHeightPx } = scroller;
let finalDeltaY = e.deltaY;
if (e.deltaMode === 1) {
finalDeltaY = finalDeltaY * lineHeightPx;
}
finalDeltaY = finalDeltaY * speed * -1;
const newTopPos = normalizeTopPosition(scroller, finalDeltaY);
if ((newTopPos && topPosition !== newTopPos)) {
e.preventDefault();
}
dispatch(updateScroll({ deltaY: finalDeltaY }, eventTypes.wheel ));
};
handleTouchMove = (e) => {
e.preventDefault();
const { touches } = e;
if (touches.length === 1) {
const { dispatch, scroller: { prevEvent } } = this.props;
const { clientY } = touches[0];
const deltaY = prevEvent.clientY - clientY;
dispatch(updateScroll({ deltaY }, eventTypes.touchEnd));
}
};
handleTouchStart = (e) => {
const { dispatch } = this.props;
const { touches } = e;
if (touches.length === 1) {
const { clientY } = touches[0];
dispatch(updateScroll({ clientY, timestamp: Date.now() }));
}
};
handleTouchEnd = (ignore) => {
const {
dispatch,
scroller: { deltaY: lastDeltaY, timestamp: lastTimestamp }
} = this.props;
if (Date.now() - lastTimestamp < 200) {
dispatch(updateScroll({ deltaY: lastDeltaY, prevDeltaY: 0, eventType: eventTypes.touchEnd }));
}
};
handleScrollbarContainerMouseDown = (e) => {
e.preventDefault();
const { dispatch, scroller: { containerHeight, realHeight, containerTop } } = this.props;
const clientPosition = e.pageY;
const top = containerTop;
const newPosition = clientPosition - top;
const proportionalToPageScrollSize = containerHeight * containerHeight / realHeight;
const topPosition = (newPosition - proportionalToPageScrollSize / 2) / scrollRatio(containerHeight, realHeight);
dispatch(updateScroll({
barPressed: true,
topPosition,
prevClientPosition: clientPosition
}, eventTypes.api));
};
handleScrollBarMouseDown = (y, e) => {
e.preventDefault();
e.stopPropagation();
const { dispatch } = this.props;
console.log('handleScrollBarMouseDown', y - e.pageY, e.pageY);
dispatch(updateScroll({
barPressed: true,
prevBarPosition: e.pageY
}));
};
handleMouseMove = (e) => {
const { dispatch, scroller } = this.props;
const { containerHeight, realHeight, barPressed, prevBarPosition } = scroller;
if (barPressed) {
e.preventDefault();
const deltaY = (prevBarPosition - e.pageY) / scrollRatio(containerHeight, realHeight);
dispatch(updateScroll({ deltaY }, eventTypes.mousemove));
}
};
handleMouseUp = (e) => {
e.preventDefault();
const { dispatch } = this.props;
dispatch(updateScroll({ barPressed: false }, eventTypes.mouseup));
};
}
const mapStateToProps = (state) => {
return {
scroller: state.scroller
};
};
export default connect(mapStateToProps)(ScrollArea);
import { PropTypes, createElement, DOM } from 'react';
import cn from 'classnames';
import { merge } from 'utils/func';
import { Motion, spring } from 'react-motion';
const { div } = DOM;
const ScrollBar = (props) => {
const {
smoothScrolling,
scrollbarStyle,
springSetting,
scroller: { barSize, barPosition }
} = props;
const motionStyles = smoothScrolling ?
merge(scrollbarStyle, { height: spring(barSize), y: spring(barPosition, springSetting) }) :
merge(scrollbarStyle, { height: barSize, y: barPosition });
return createElement(Motion, { style: motionStyles }, ({ height, y }) => {
const { scroller: { barPressed }, containerStyle } = props;
return div({
style: containerStyle,
className: cn('scrollbar-container vertical', { 'active': barPressed }),
onMouseDown: props.onScrollbarContainerMouseDown
},
div({
className: 'scrollbar',
onMouseDown: props.onScrollbarMouseDown.bind(null, y),
style: {
transform: `translate3d(0, ${y}px, 0)`,
height
}
})
);
});
};
ScrollBar.propTypes = {
onScrollbarContainerMouseDown: PropTypes.func,
onScrollbarMouseDown: PropTypes.func,
scroller: PropTypes.object,
containerStyle: PropTypes.object,
scrollbarStyle: PropTypes.object
};
ScrollBar.defaultProps = {
smoothScrolling: true
};
export default ScrollBar;
import { createAction, handleActions } from 'redux-actions';
import { merge, clamp, logWithLabel } from 'utils/func';
export const normalizeTopPosition = (state, deltaY) => {
const { containerHeight, realHeight, topPosition } = state;
const heightDiff = realHeight - containerHeight;
let newTopPosition = topPosition - (deltaY);
if (newTopPosition > heightDiff) {
newTopPosition = heightDiff;
}
if (newTopPosition < 0) {
newTopPosition = 0;
}
return newTopPosition;
};
export const calcFractionalPos = (realHeight, containerHeight, contentPosition) => {
const relativeSize = realHeight - containerHeight;
return 1 - ((relativeSize - contentPosition) / relativeSize);
};
export const calcBarState = (currentBarPos, realHeight, containerHeight, topPosition) => {
if (containerHeight === 0) {
return { barSize: 0, barPosition: 0, lastClientPosition: 0 };
}
const fractionalPosition = calcFractionalPos(realHeight, containerHeight, topPosition);
const barSize = Math.max(24, Math.round(containerHeight * (containerHeight / realHeight)));
const scrollPosition = Math.round((containerHeight - barSize) * fractionalPosition);
return {
barSize: barSize,
barPosition: scrollPosition
};
};
export const actions = {};
actions.updateScroll = createAction('scroll/UPDATE', (update, eventType = 'api') => {
return merge(update, { eventType });
});
actions.scrollTo = createAction('scroll/UPDATE', (position, eventType = 'api') => {
return { topPosition: position, eventType };
});
actions.scrollBy = createAction('scroll/UPDATE', (deltaY, eventType = 'api') => {
return { deltaY, eventType };
});
actions.bottom = () => (actions.scrollTo('bottom'));
actions.top = (actions.scrollTo('top'));
export default handleActions({
['scroll/UPDATE']: (state, action) => {
const {
deltaY = state.deltaY,
barPosition = state.barPosition,
containerHeight = state.containerHeight,
realHeight = state.realHeight
} = action.payload;
let {
topPosition = state.topPosition
} = action.payload;
if (deltaY !== state.deltaY) {
topPosition = normalizeTopPosition(state, deltaY);
}
const bottomPosition = realHeight - containerHeight;
if (typeof barPosition === 'string') {
if (topPosition === 'top') {
topPosition = 0;
}
if (topPosition === 'bottom') {
topPosition = bottomPosition;
}
}
topPosition = clamp(topPosition, 0, bottomPosition);
const newState = merge(
state,
action.payload,
calcBarState(barPosition, realHeight, containerHeight, topPosition),
{
topPosition: topPosition,
deltaY: deltaY,
timestamp: Date.now()
}
);
logWithLabel(newState, 'new scroll state');
return newState;
},
['scroll/FIRE_EVENT']: (state, action) => {
return merge(state, {
prevEvent: merge(state.prevEvent, action.payload)
});
}
}, {
topPosition: 0,
realHeight: 0,
realWidth: 0,
realTop: 0,
containerHeight: 0,
containerWidth: 0,
containerTop: 0,
lineHeightPx: 0,
clientY: 0,
deltaY: 0,
prevClientY: 0,
prevDeltaY: 0,
barPosition: 0,
prevBarPosition: 0,
barSize: 0,
barPressed: false,
barClientY: 0,
lastBarClientY: 0,
timestamp: Date.now()
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment