2016-06-04 00:44:33 +03:00
|
|
|
import React, { Component, PropTypes } from 'react';
|
2015-11-22 23:41:28 +03:00
|
|
|
import ReactDOM from 'react-dom';
|
2016-05-16 15:17:26 +03:00
|
|
|
import classnames from 'classnames';
|
|
|
|
import { themr } from 'react-css-themr';
|
2017-01-05 04:42:18 +03:00
|
|
|
import dissoc from 'ramda/src/dissoc';
|
2016-05-28 16:37:10 +03:00
|
|
|
import { RIPPLE } from '../identifiers.js';
|
2016-04-12 22:41:28 +03:00
|
|
|
import events from '../utils/events';
|
2015-12-07 04:34:12 +03:00
|
|
|
import prefixer from '../utils/prefixer';
|
2015-11-22 23:41:28 +03:00
|
|
|
|
2015-12-07 04:34:12 +03:00
|
|
|
const defaults = {
|
|
|
|
centered: false,
|
|
|
|
className: '',
|
2016-08-06 15:24:40 +03:00
|
|
|
multiple: true,
|
2016-05-28 16:37:10 +03:00
|
|
|
spread: 2,
|
|
|
|
theme: {}
|
2015-12-07 04:34:12 +03:00
|
|
|
};
|
2015-11-22 23:41:28 +03:00
|
|
|
|
2016-05-28 16:37:10 +03:00
|
|
|
const rippleFactory = (options = {}) => {
|
2015-12-07 04:34:12 +03:00
|
|
|
const {
|
|
|
|
centered: defaultCentered,
|
|
|
|
className: defaultClassName,
|
2016-08-06 15:24:40 +03:00
|
|
|
multiple: defaultMultiple,
|
2016-01-26 16:58:28 +03:00
|
|
|
spread: defaultSpread,
|
2016-05-28 16:37:10 +03:00
|
|
|
theme: defaultTheme,
|
2016-01-26 16:58:28 +03:00
|
|
|
...props
|
2015-12-07 04:34:12 +03:00
|
|
|
} = {...defaults, ...options};
|
2015-11-22 23:41:28 +03:00
|
|
|
|
2015-12-07 04:34:12 +03:00
|
|
|
return ComposedComponent => {
|
2016-06-04 00:44:33 +03:00
|
|
|
class RippledComponent extends Component {
|
2015-12-07 04:34:12 +03:00
|
|
|
static propTypes = {
|
2016-06-04 00:44:33 +03:00
|
|
|
children: PropTypes.any,
|
|
|
|
disabled: PropTypes.bool,
|
|
|
|
onRippleEnded: PropTypes.func,
|
|
|
|
ripple: PropTypes.bool,
|
|
|
|
rippleCentered: PropTypes.bool,
|
|
|
|
rippleClassName: PropTypes.string,
|
2016-08-06 15:24:40 +03:00
|
|
|
rippleMultiple: PropTypes.bool,
|
2016-06-04 00:44:33 +03:00
|
|
|
rippleSpread: PropTypes.number,
|
|
|
|
theme: PropTypes.shape({
|
|
|
|
ripple: PropTypes.string,
|
|
|
|
rippleActive: PropTypes.string,
|
|
|
|
rippleRestarting: PropTypes.string,
|
|
|
|
rippleWrapper: PropTypes.string
|
2016-05-16 15:17:26 +03:00
|
|
|
})
|
2015-12-07 04:34:12 +03:00
|
|
|
};
|
2015-11-22 23:41:28 +03:00
|
|
|
|
2015-12-07 04:34:12 +03:00
|
|
|
static defaultProps = {
|
|
|
|
disabled: false,
|
|
|
|
ripple: true,
|
|
|
|
rippleCentered: defaultCentered,
|
|
|
|
rippleClassName: defaultClassName,
|
2016-08-06 15:24:40 +03:00
|
|
|
rippleMultiple: defaultMultiple,
|
2015-12-07 04:34:12 +03:00
|
|
|
rippleSpread: defaultSpread
|
|
|
|
};
|
2015-11-22 23:41:28 +03:00
|
|
|
|
2015-12-07 04:34:12 +03:00
|
|
|
state = {
|
2016-08-06 15:24:40 +03:00
|
|
|
ripples: {}
|
2015-12-07 04:34:12 +03:00
|
|
|
};
|
|
|
|
|
2016-08-04 23:23:43 +03:00
|
|
|
componentDidUpdate (prevProps, prevState) {
|
2016-08-06 15:24:40 +03:00
|
|
|
// If a new ripple was just added, add a remove event listener to its animation
|
|
|
|
if (Object.keys(prevState.ripples).length < Object.keys(this.state.ripples).length) {
|
|
|
|
this.addRippleRemoveEventListener(this.getLastKey());
|
2016-04-12 22:41:28 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-06 21:40:24 +03:00
|
|
|
componentWillUnmount () {
|
|
|
|
// Remove document event listeners for ripple if they still exists
|
|
|
|
Object.keys(this.state.ripples).forEach(key => {
|
|
|
|
this.state.ripples[key].endRipple();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-08-06 15:24:40 +03:00
|
|
|
/**
|
|
|
|
* Add an event listener to the reference with given key so when the animation transition
|
|
|
|
* ends we can be sure that it finished and it can be safely removed from the state.
|
|
|
|
* This function is called whenever a new ripple is added to the component.
|
|
|
|
*
|
|
|
|
* @param {String} rippleKey Is the key of the ripple to add the event.
|
|
|
|
*/
|
|
|
|
addRippleRemoveEventListener (rippleKey) {
|
|
|
|
const self = this;
|
|
|
|
events.addEventListenerOnTransitionEnded(this.refs[rippleKey], function onOpacityEnd (e) {
|
|
|
|
if (e.propertyName === 'opacity') {
|
|
|
|
if (self.props.onRippleEnded) self.props.onRippleEnded(e);
|
|
|
|
events.removeEventListenerOnTransitionEnded(self.refs[rippleKey], onOpacityEnd);
|
2017-01-05 04:42:18 +03:00
|
|
|
self.setState({ ripples: dissoc(rippleKey, self.state.ripples) });
|
2016-08-06 15:24:40 +03:00
|
|
|
}
|
|
|
|
});
|
2016-04-12 22:41:28 +03:00
|
|
|
}
|
|
|
|
|
2016-08-06 15:24:40 +03:00
|
|
|
/**
|
|
|
|
* Start a ripple animation on an specific point with touch or mouse events. First
|
|
|
|
* decides if the animation should trigger. If the ripple is multiple or there is no
|
|
|
|
* ripple present, it creates a new key. If it's a simple ripple and already exists,
|
|
|
|
* it just restarts the current ripple. The animation happens in two state changes
|
|
|
|
* to allow triggering via css.
|
|
|
|
*
|
|
|
|
* @param {Number} x Coordinate X on the screen where animation should start
|
|
|
|
* @param {Number} y Coordinate Y on the screen where animation should start
|
|
|
|
* @param {Boolean} isTouch Use events from touch or mouse.
|
|
|
|
*/
|
|
|
|
animateRipple (x, y, isTouch) {
|
|
|
|
if (this.rippleShouldTrigger(isTouch)) {
|
|
|
|
const { top, left, width } = this.getDescriptor(x, y);
|
|
|
|
const noRipplesActive = Object.keys(this.state.ripples).length === 0;
|
|
|
|
const key = this.props.rippleMultiple || noRipplesActive ? this.getNextKey() : this.getLastKey();
|
2016-08-06 21:40:24 +03:00
|
|
|
const endRipple = this.addRippleDeactivateEventListener(isTouch, key);
|
|
|
|
const initialState = { active: false, restarting: true, top, left, width, endRipple };
|
2016-08-06 15:24:40 +03:00
|
|
|
const runningState = { active: true, restarting: false };
|
2017-01-05 04:42:18 +03:00
|
|
|
const ripples = {...this.state.ripples, [key]: initialState };
|
|
|
|
this.setState({ ripples }, () => {
|
2016-08-06 15:24:40 +03:00
|
|
|
this.refs[key].offsetWidth; //eslint-disable-line no-unused-expressions
|
2017-01-05 04:42:18 +03:00
|
|
|
this.setState({ ripples: {
|
|
|
|
...this.state.ripples,
|
|
|
|
[key]: Object.assign({}, this.state.ripples[key], runningState)
|
|
|
|
} });
|
2015-12-07 04:34:12 +03:00
|
|
|
});
|
|
|
|
}
|
2016-08-06 15:24:40 +03:00
|
|
|
}
|
2015-11-29 14:40:09 +03:00
|
|
|
|
2016-08-06 15:24:40 +03:00
|
|
|
/**
|
|
|
|
* Determine if a ripple should start depending if its a touch event. For mobile both
|
|
|
|
* touchStart and mouseDown are launched so in case is touch we should always trigger
|
|
|
|
* but if its not we should check if a touch was already triggered to decide.
|
|
|
|
*
|
|
|
|
* @param {Boolean} isTouch True in case a touch event triggered the ripple false otherwise.
|
|
|
|
* @return {Boolean} True in case the ripple should trigger or false if it shouldn't.
|
|
|
|
*/
|
|
|
|
rippleShouldTrigger (isTouch) {
|
|
|
|
const shouldStart = isTouch ? true : !this.touchCache;
|
|
|
|
this.touchCache = isTouch;
|
|
|
|
return shouldStart;
|
2015-12-07 04:34:12 +03:00
|
|
|
}
|
|
|
|
|
2016-08-06 15:24:40 +03:00
|
|
|
/**
|
|
|
|
* Find out a descriptor object for the ripple element being created depending on
|
|
|
|
* the position where the it was triggered and the component's dimensions.
|
|
|
|
*
|
|
|
|
* @param {Number} x Coordinate x in the viewport where ripple was triggered
|
|
|
|
* @param {Number} y Coordinate y in the viewport where ripple was triggered
|
|
|
|
* @return {Object} Descriptor element including position and size of the element
|
|
|
|
*/
|
|
|
|
getDescriptor (x, y) {
|
|
|
|
const { left, top, height, width } = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
|
|
|
const { rippleCentered: centered, rippleSpread: spread } = this.props;
|
2015-12-07 04:34:12 +03:00
|
|
|
return {
|
2016-08-06 15:24:40 +03:00
|
|
|
left: centered ? 0 : x - left - width / 2,
|
|
|
|
top: centered ? 0 : y - top - height / 2,
|
2015-12-07 04:34:12 +03:00
|
|
|
width: width * spread
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-08-06 15:24:40 +03:00
|
|
|
/**
|
|
|
|
* Increments and internal counter and returns the next value as a string. It
|
|
|
|
* is used to assign key references to new ripple elements.
|
|
|
|
*
|
|
|
|
* @return {String} Key to be assigned to a ripple.
|
|
|
|
*/
|
|
|
|
getNextKey () {
|
|
|
|
this.currentCount = this.currentCount ? this.currentCount + 1 : 1;
|
2016-08-21 19:44:04 +03:00
|
|
|
return `ripple${this.currentCount}`;
|
2016-08-06 15:24:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the last generated key for a ripple element. When there is only one ripple
|
|
|
|
* and to get the reference when a ripple was just created.
|
|
|
|
*
|
|
|
|
* @return {String} The last generated ripple key.
|
|
|
|
*/
|
|
|
|
getLastKey () {
|
2016-08-21 19:44:04 +03:00
|
|
|
return `ripple${this.currentCount}`;
|
2016-08-06 15:24:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-08-06 21:40:24 +03:00
|
|
|
* Add an event listener to the document needed to deactivate a ripple and make it dissappear.
|
|
|
|
* Deactivation can happen with a touchend or mouseup depending on the trigger type. The
|
|
|
|
* ending function is created from a factory function and returned.
|
2016-08-06 15:24:40 +03:00
|
|
|
*
|
|
|
|
* @param {Boolean} isTouch True in case the trigger was a touch event false otherwise.
|
|
|
|
* @param {String} rippleKey It's a key to identify the ripple that should be deactivated.
|
2016-08-06 21:40:24 +03:00
|
|
|
* @return {Function} Callback function that deactivates the ripple and removes the event listener
|
2016-08-06 15:24:40 +03:00
|
|
|
*/
|
|
|
|
addRippleDeactivateEventListener (isTouch, rippleKey) {
|
|
|
|
const eventType = isTouch ? 'touchend' : 'mouseup';
|
2016-08-06 21:40:24 +03:00
|
|
|
const endRipple = this.createRippleDeactivateCallback(eventType, rippleKey);
|
|
|
|
document.addEventListener(eventType, endRipple);
|
|
|
|
return endRipple;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generates a function that can be called to deactivate a given ripple and remove its finishing
|
|
|
|
* event listener. If is generated because we need to store it to be called on unmount in case
|
|
|
|
* the ripple is still running.
|
|
|
|
*
|
|
|
|
* @param {String} eventType Is the event type that can be touchend or mouseup
|
|
|
|
* @param {String} rippleKey Is the key representing the ripple
|
|
|
|
* @return {Function} Callback function that deactivates the ripple and removes the listener
|
|
|
|
*/
|
|
|
|
createRippleDeactivateCallback (eventType, rippleKey) {
|
|
|
|
const self = this;
|
|
|
|
return function endRipple () {
|
2016-08-06 15:24:40 +03:00
|
|
|
document.removeEventListener(eventType, endRipple);
|
2017-01-05 04:42:18 +03:00
|
|
|
self.setState({ ripples: {
|
|
|
|
...self.state.ripples,
|
|
|
|
[rippleKey]: Object.assign({}, self.state.ripples[rippleKey], { active: false })
|
|
|
|
} });
|
2016-08-06 21:40:24 +03:00
|
|
|
};
|
2016-08-06 15:24:40 +03:00
|
|
|
}
|
|
|
|
|
2015-12-07 04:34:12 +03:00
|
|
|
handleMouseDown = (event) => {
|
|
|
|
if (this.props.onMouseDown) this.props.onMouseDown(event);
|
2016-08-06 15:24:40 +03:00
|
|
|
if (!this.props.disabled) {
|
|
|
|
const { x, y } = events.getMousePosition(event);
|
|
|
|
this.animateRipple(x, y, false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
handleTouchStart = (event) => {
|
|
|
|
if (this.props.onTouchStart) this.props.onTouchStart(event);
|
|
|
|
if (!this.props.disabled) {
|
|
|
|
const { x, y } = events.getTouchPosition(event);
|
|
|
|
this.animateRipple(x, y, true);
|
|
|
|
}
|
2015-12-07 04:34:12 +03:00
|
|
|
};
|
|
|
|
|
2016-08-06 15:24:40 +03:00
|
|
|
renderRipple (key, className, { active, left, restarting, top, width }) {
|
|
|
|
const scale = restarting ? 0 : 1;
|
|
|
|
const transform = `translate3d(${-width / 2 + left}px, ${-width / 2 + top}px, 0) scale(${scale})`;
|
|
|
|
const _className = classnames(this.props.theme.ripple, {
|
|
|
|
[this.props.theme.rippleActive]: active,
|
|
|
|
[this.props.theme.rippleRestarting]: restarting
|
|
|
|
}, className);
|
|
|
|
return (
|
|
|
|
<span key={key} data-react-toolbox='ripple' className={this.props.theme.rippleWrapper} {...props}>
|
|
|
|
<span
|
|
|
|
role='ripple'
|
|
|
|
ref={key}
|
|
|
|
className={_className}
|
|
|
|
style={prefixer({ transform }, {width, height: width})}
|
|
|
|
/>
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2015-12-07 04:34:12 +03:00
|
|
|
render () {
|
2016-09-05 21:28:24 +03:00
|
|
|
const { ripples } = this.state;
|
|
|
|
const { onRippleEnded, rippleCentered, rippleMultiple, rippleSpread, // eslint-disable-line
|
|
|
|
children, ripple, rippleClassName, ...other } = this.props;
|
|
|
|
|
|
|
|
if (!ripple) return <ComposedComponent children={children} {...other} />;
|
|
|
|
return (
|
|
|
|
<ComposedComponent {...other} onMouseDown={this.handleMouseDown} onTouchStart={this.handleTouchStart}>
|
|
|
|
{children}
|
|
|
|
{Object.keys(ripples).map(key => this.renderRipple(key, rippleClassName, ripples[key]))}
|
|
|
|
</ComposedComponent>
|
|
|
|
);
|
2015-12-07 04:34:12 +03:00
|
|
|
}
|
2016-05-16 15:17:26 +03:00
|
|
|
}
|
|
|
|
|
2016-05-28 16:37:10 +03:00
|
|
|
return themr(RIPPLE, defaultTheme)(RippledComponent);
|
2015-12-07 04:34:12 +03:00
|
|
|
};
|
|
|
|
};
|
2015-11-22 23:41:28 +03:00
|
|
|
|
2016-05-28 16:37:10 +03:00
|
|
|
export default rippleFactory;
|