@geosolutions/react-joyride
Version:
Create walkthroughs and guided tours for your apps
547 lines (474 loc) • 16.1 kB
JSX
import React from 'react';
import { browser, getDocumentHeight, getBoundingClientRectFromElement, sanitizeSelector } from './utils';
export default class JoyrideTooltip extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
static propTypes = {
allowClicksThruHole: React.PropTypes.bool.isRequired,
animate: React.PropTypes.bool.isRequired,
buttons: React.PropTypes.object.isRequired,
disableOverlay: React.PropTypes.bool,
holePadding: React.PropTypes.number,
offsetParentSelector: React.PropTypes.string,
onClick: React.PropTypes.func.isRequired,
onRender: React.PropTypes.func.isRequired,
// position of tooltip with respect to target
position: React.PropTypes.oneOf([
'top', 'top-left', 'top-right',
'bottom', 'bottom-left', 'bottom-right',
'right', 'left',
]).isRequired,
// sanitized selector string
selector: React.PropTypes.string.isRequired,
showOverlay: React.PropTypes.bool.isRequired,
standalone: React.PropTypes.bool,
step: React.PropTypes.object.isRequired,
// DOM element to target
target: React.PropTypes.object.isRequired,
type: React.PropTypes.string.isRequired,
xPos: React.PropTypes.oneOfType([
React.PropTypes.number,
React.PropTypes.string
]).isRequired,
yPos: React.PropTypes.oneOfType([
React.PropTypes.number,
React.PropTypes.string
]).isRequired
};
static defaultProps = {
buttons: {
primary: 'Close'
},
step: {},
xPos: -1000,
yPos: -1000,
mouseOverHole: false
};
componentWillMount() {
const opts = this.setOpts(this.props);
const styles = this.setStyles(this.props.step.style, opts, this.props);
this.setState({ styles, opts });
}
componentDidMount() {
const { allowClicksThruHole, onRender, showOverlay } = this.props;
this.forceUpdate();
onRender();
if (showOverlay && allowClicksThruHole) {
document.addEventListener('mousemove', this.handleMouseMove, false);
}
}
componentWillReceiveProps(nextProps) {
const {
allowClicksThruHole: nextAllowClicksThruHole,
animate: nextAnimate,
standalone: nextStandalone,
step: nextStep,
holePadding: nextHolePadding,
position: nextPosition,
xPos: nextXPos,
yPos: nextYPos,
showOverlay: nextShowOverlay,
} = nextProps;
const {
allowClicksThruHole,
animate,
standalone,
step,
holePadding,
position,
xPos,
yPos,
showOverlay,
} = this.props;
/* istanbul ignore else */
if (
nextAnimate !== animate ||
nextStandalone !== standalone ||
nextStep !== step ||
nextHolePadding !== holePadding ||
nextPosition !== position ||
nextXPos !== xPos ||
nextYPos !== yPos
) {
const opts = this.setOpts(nextProps);
const styles = this.setStyles(nextProps.step.style, opts, nextProps);
this.setState({ styles, opts });
}
// If showOverlay changed, we might need to allow clicks in the overlay hole
if (nextShowOverlay !== showOverlay) {
if (nextShowOverlay && nextAllowClicksThruHole) {
document.addEventListener('mousemove', this.handleMouseMove, false);
}
else {
document.removeEventListener('mousemove', this.handleMouseMove, false);
this.setState({ mouseOverHole: false });
}
}
// If allowClickInHole changed, we need to enable or disable clicking in the overlay hole
if (nextAllowClicksThruHole !== allowClicksThruHole) {
if (nextAllowClicksThruHole) {
document.addEventListener('mousemove', this.handleMouseMove, false);
}
else {
document.removeEventListener('mousemove', this.handleMouseMove, false);
this.setState({ mouseOverHole: false });
}
}
}
componentDidUpdate(prevProps) {
const { onRender, selector } = this.props;
if (prevProps.selector !== selector) {
this.forceUpdate();
onRender();
}
}
componentWillUnmount() {
document.removeEventListener('mousemove', this.handleMouseMove, false);
this.setState({ mouseOverHole: false });
}
getArrowPosition(position) {
let arrowPosition = position;
if (window.innerWidth < 480) {
if (position < 8) {
arrowPosition = 8;
}
else if (position > 92) {
arrowPosition = 92;
}
}
else if (window.innerWidth < 1024) {
if (position < 6) {
arrowPosition = 6;
}
else if (position > 94) {
arrowPosition = 94;
}
}
else if (position < 5) {
arrowPosition = 5;
}
else if (position > 95) {
arrowPosition = 95;
}
return arrowPosition;
}
generateArrow(opts = {}) {
let width;
let height;
let rotate;
opts.location = opts.location || 'top';
opts.color = opts.color || '#f04';
opts.color = opts.color.replace('#', '%23');
opts.width = opts.width || 36;
opts.height = opts.width / 2;
opts.scale = opts.width / 16;
opts.rotate = '0';
height = opts.height;
rotate = opts.rotate;
width = opts.width;
if (opts.location === 'bottom') {
rotate = '180 8 4';
}
else if (opts.location === 'left') {
height = opts.width;
width = opts.height;
rotate = '270 8 8';
}
else if (opts.location === 'right') {
height = opts.width;
width = opts.height;
rotate = '90 4 4';
}
return `data:image/svg+xml,%3Csvg%20width%3D%22${width}%22%20height%3D%22${height}%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpolygon%20points%3D%220%2C%200%208%2C%208%2016%2C0%22%20fill%3D%22${opts.color}%22%20transform%3D%22scale%28${opts.scale}%29%20rotate%28${rotate}%29%22%3E%3C%2Fpolygon%3E%3C%2Fsvg%3E`;
}
/**
* Calculate styles based on those passed in with the step, or calculated opts, or props
*
* @param {Object} stepStyles Style object provided with step
* @param {Object} opts Options object calculated from this.setOpts
* @param {string} opts.arrowPosition Used for left/right positioing of arrow when on bottom or top
* @param {Object} opts.rect BoundingClientRect of target element
* @param {string} opts.positonBaseClass Base position of tooltip (top, bottom, left, right)
* @param {Object} props Positioning properties: cssPosition, xPos, and yPos
* @returns {Object} Calculated styles for arrow, buttons, header, main, footer, hole, and tooltip
*/
setStyles(stepStyles, opts, props) {
const { holePadding, step, xPos, yPos } = props;
const isFixed = step.isFixed === true;
const styles = {
arrow: {
left: opts.arrowPosition
},
buttons: {},
header: {},
main: {},
footer: {},
hole: {},
tooltip: {
position: isFixed ? 'fixed' : 'absolute',
top: Math.round(yPos),
left: Math.round(xPos)
}
};
styles.hole = {
top: Math.round((opts.rect.top - (isFixed ? 0 : getBoundingClientRectFromElement(document.body).top)) - holePadding),
left: Math.round(opts.rect.left - holePadding),
width: Math.round(opts.rect.width + (holePadding * 2)),
height: Math.round(opts.rect.height + (holePadding * 2))
};
if (isFixed) {
styles.hole.position = 'fixed';
}
styles.buttons = {
back: {},
close: {},
primary: {},
skip: {}
};
/* Styling */
/* istanbul ignore else */
if (stepStyles) {
if (stepStyles.backgroundColor) {
styles.arrow.backgroundImage = `url("${this.generateArrow({
location: opts.positonBaseClass,
color: stepStyles.backgroundColor
})}")`;
styles.tooltip.backgroundColor = stepStyles.backgroundColor;
}
if (stepStyles.borderRadius) {
styles.tooltip.borderRadius = stepStyles.borderRadius;
}
if (stepStyles.color) {
styles.buttons.primary.color = stepStyles.color;
styles.buttons.close.color = stepStyles.color;
styles.buttons.skip.color = stepStyles.color;
styles.header.color = stepStyles.color;
styles.tooltip.color = stepStyles.color;
if (stepStyles.mainColor && stepStyles.mainColor === stepStyles.color) {
styles.buttons.primary.color = stepStyles.backgroundColor;
}
}
if (stepStyles.mainColor) {
styles.buttons.primary.backgroundColor = stepStyles.mainColor;
styles.buttons.back.color = stepStyles.mainColor;
styles.header.borderColor = stepStyles.mainColor;
}
if (stepStyles.textAlign) {
styles.tooltip.textAlign = stepStyles.textAlign;
}
if (stepStyles.width) {
styles.tooltip.width = stepStyles.width;
}
if (stepStyles.header) {
styles.header = {
...styles.header,
...stepStyles.header
};
}
if (stepStyles.main) {
styles.main = {
...styles.main,
...stepStyles.main
};
}
if (stepStyles.footer) {
styles.footer = {
...styles.footer,
...stepStyles.footer
};
}
if (stepStyles.back) {
styles.buttons.back = {
...styles.buttons.back,
...stepStyles.back
};
}
if (stepStyles.arrow) {
styles.arrow = {
...styles.arrow,
...stepStyles.arrow
};
}
if (stepStyles.button) {
styles.buttons.primary = {
...styles.buttons.primary,
...stepStyles.button
};
}
if (stepStyles.close) {
styles.buttons.close = {
...styles.buttons.close,
...stepStyles.close
};
}
if (stepStyles.skip) {
styles.buttons.skip = {
...styles.buttons.skip,
...stepStyles.skip
};
}
if (stepStyles.hole) {
styles.hole = {
...stepStyles.hole,
...styles.hole
};
}
}
return styles;
}
setOpts(props) {
const { animate, offsetParentSelector, position, standalone, target, xPos } = props;
const offsetParent = document.querySelector(sanitizeSelector(offsetParentSelector));
const tooltip = document.querySelector('.joyride-tooltip');
const opts = {
classes: ['joyride-tooltip'],
rect: getBoundingClientRectFromElement(target),
positionClass: position,
};
opts.positonBaseClass = opts.positionClass.match(/-/) ? opts.positionClass.split('-')[0] : opts.positionClass;
if ((/^bottom$/.test(opts.positionClass) || /^top$/.test(opts.positionClass)) && xPos > -1) {
opts.tooltip = { width: 450 };
/* istanbul ignore else */
if (tooltip) {
opts.tooltip = getBoundingClientRectFromElement(tooltip);
}
opts.targetMiddle = (opts.rect.left + (opts.rect.width / 2));
opts.arrowPosition = (((opts.targetMiddle - xPos) / opts.tooltip.width) * 100).toFixed(2);
opts.arrowPosition = `${this.getArrowPosition(opts.arrowPosition)}%`;
}
if (standalone) {
opts.classes.push('joyride-tooltip--standalone');
}
if (opts.positonBaseClass !== opts.positionClass) {
opts.classes.push(opts.positonBaseClass);
}
opts.classes.push(opts.positionClass);
if (animate) {
opts.classes.push('joyride-tooltip--animate');
}
return opts;
}
handleMouseMove = (e) => {
const event = e || window.e;
const hole = this.state.styles.hole;
const offsetY = hole.position === 'fixed' ? event.clientY : event.pageY;
const offsetX = hole.position === 'fixed' ? event.clientX : event.pageX;
const inHoleHeight = (offsetY >= hole.top && offsetY <= hole.top + hole.height);
const inHoleWidth = (offsetX >= hole.left && offsetX <= hole.left + hole.width);
const inHole = inHoleWidth && inHoleHeight;
if (inHole && !this.state.mouseOverHole) {
this.setState({ mouseOverHole: true });
}
if (!inHole && this.state.mouseOverHole) {
this.setState({ mouseOverHole: false });
}
};
render() {
const {
allowClicksThruHole,
buttons,
disableOverlay,
onClick,
selector,
showOverlay,
step,
target,
type
} = this.props;
if (!target) {
return undefined;
}
const opts = this.state.opts;
const styles = this.state.styles;
const output = {};
if (step.title) {
output.header = (
<div className="joyride-tooltip__header" style={styles.header}>
{step.title}</div>
);
}
if (buttons.skip) {
output.skip = (<button
className="joyride-tooltip__button joyride-tooltip__button--skip"
style={styles.buttons.skip}
data-type="skip"
onClick={onClick}>
{buttons.skip}
</button>);
}
// Why is this here?
if (!step.text || typeof step.text === 'string') {
output.main = (
<div
className="joyride-tooltip__main"
style={styles.main}
dangerouslySetInnerHTML={{ __html: step.text || '' }} />);
}
else {
output.main = (<div className="joyride-tooltip__main" style={styles.main}>{step.text}</div>);
}
if (buttons.secondary) {
output.secondary = (<button
className="joyride-tooltip__button joyride-tooltip__button--secondary"
style={styles.buttons.back}
data-type="back"
onClick={onClick}>
{buttons.secondary}
</button>);
}
if (step.event === 'hover') {
styles.buttons.close.opacity = 0;
}
output.tooltipComponent = (
<div className={opts.classes.join(' ')} style={styles.tooltip} data-target={selector}>
<div
className={`joyride-tooltip__triangle joyride-tooltip__triangle-${opts.positionClass}`}
style={styles.arrow} />
<button
className={`joyride-tooltip__close${(output.header ? ' joyride-tooltip__close--header' : '')}`}
style={styles.buttons.close}
data-type="close"
onClick={onClick} />
{output.header}
{output.main}
<div className="joyride-tooltip__footer" style={styles.footer}>
{output.skip}
{output.secondary}
<button
className="joyride-tooltip__button joyride-tooltip__button--primary"
style={styles.buttons.primary}
data-type={['single', 'casual'].indexOf(type) > -1 ? 'close' : 'next'}
onClick={onClick}>
{buttons.primary}
</button>
</div>
</div>
);
if (showOverlay) {
// Empty onClick handler is for iOS touch devices (https://github.com/gilbarbara/react-joyride/issues/204)
output.hole = (
<div className={`joyride-hole ${browser}`} style={styles.hole} onClick={() => {}} />
);
}
if (!showOverlay) {
return output.tooltipComponent;
}
const overlayStyles = {
cursor: disableOverlay ? 'default' : 'pointer',
height: getDocumentHeight(),
pointerEvents: this.state.mouseOverHole ? 'none' : 'auto',
};
return (
<div
className="joyride-overlay"
style={overlayStyles}
data-type="close"
onClick={!disableOverlay ? onClick : undefined}>
{output.hole}
{output.tooltipComponent}
</div>
);
}
}