Skip to content

Instantly share code, notes, and snippets.

@Morgantheplant
Last active February 22, 2019 19:51
Show Gist options
  • Save Morgantheplant/0dacbffd4309401208bade116fe58907 to your computer and use it in GitHub Desktop.
Save Morgantheplant/0dacbffd4309401208bade116fe58907 to your computer and use it in GitHub Desktop.
abstracted popup logic
import PropTypes from 'prop-types';
import React, {Component} from 'react';
// wrapper component that will call dismiss if a click is detected outside of itself or its children
class DismissableClick extends Component {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.removeListener = this.removeListener.bind(this);
}
componentDidMount() {
this.props.onDismiss && document.addEventListener('mousedown', this.handleMouseDown);
}
componentWillUnmount() {
this.props.onDismiss && this.removeListener();
}
handleMouseDown({ target }) {
if (this.node.contains(target)) {
return;
}
this.removeListener();
this.props.onDismiss();
}
removeListener() {
document.removeEventListener('mousedown', this.handleMouseDown, false);
}
render() {
const { children, component, onDismiss, ...restProps } = this.props;
restProps.ref = onDismiss ? node => (this.node = node) : undefined;
return React.createElement(component || 'div', restProps, children);
}
}
/**
* PortalPopUp - Write a short description of your component here.
*/
// maps events to component's internal methods
const defaultTriggerMap = {
onClick: 'showPopUpEvent',
onMouseOut: 'hidePopUp',
onMouseOver: 'showPopUpEvent',
ref: 'showPopUpEvent'
};
// we can pass this in as a prop later if we need padding to be configurable
// this can also be done via CSS so making it a constant
const PADDING = 10;
// this calculates where to position the popup panel
const positionStrategy = {
bottom: (rect) => ({ left: rect.x, top: rect.y + rect.height + PADDING })
};
// using the trigger map this attaches hide/remove action to component events
const addTriggersToProps = function addTriggersToProps(restProps, triggers) {
const { TriggerMap } = this.props;
// allow triggers to be passed in as Array or String
const triggerAry = Array.isArray(triggers) ? triggers : [triggers];
return triggerAry.reduce((acc, trigger) => {
const triggerAction = TriggerMap[trigger];
if (triggerAction) {
acc[trigger] = this[triggerAction];
}
return acc;
}, restProps);
};
export class PortalPopUp extends Component {
constructor(props) {
super(props);
this.state = {
currentToolBarPosition: null,
showPopUp: props.showPopUp
};
this.setPopUpPosition = this.setPopUpPosition.bind(this);
this.showPopUpEvent = this.showPopUpEvent.bind(this);
this.toggleShown = this.toggleShown.bind(this);
this.hidePopUp = this.setPopUpPosition.bind(this, null);
}
setPopUpPosition(el) {
let coords = null;
if (el) {
const rect = el.getBoundingClientRect();
coords = positionStrategy[this.props.position](rect);
}
this.setState(() => ({
currentToolBarPosition: coords
}));
}
showPopUpEvent(e) {
this.setPopUpPosition(e.target);
}
toggleShown(shouldShow) {
this.setState(showPopUp => ({
showPopUp: shouldShow !== 'undefined' ? shouldShow : !prevState.showPopUp
}));
}
render() {
const {
component,
triggers,
showPopUp,
popup,
position,
children,
panelClassName,
dismissWithClick,
TriggerMap,
...restProps
} = this.props;
const rootProps = addTriggersToProps.call(this, restProps, triggers);
return React.createElement(
component || 'div',
rootProps,
children,
showPopUp &&
this.state.currentToolBarPosition &&
<Portal>
<DismissableClick
onDismiss={dismissWithClick ? this.hidePopUp : null}
className={`panel ${position} ${panelClassName || ''}`}
style={{
position: 'absolute',
left: this.state.currentToolBarPosition.left,
top: this.state.currentToolBarPosition.top
}}>
{popup({
...this.state,
toggleShown: this.toggleShown,
setPopUpPosition: this.setPopUpPosition
})}
</DismissableClick>
</Portal>
);
}
}
//// example of it in use with click
class LinkElement extends Component {
constructor(props) {
super(props);
this.handleRemove = this.handleRemove.bind(this);
}
handleRemove() {
const { entityKey, handleAction } = this.props;
if (entityKey) {
const action = toggleLink(entityKey);
handleAction(action);
}
}
goToLink() {
const href = this.props.getHrefFromProps(this.props);
if (href) {
const newWindow = window.open(href, '_blank');
// reset the "opener" property ie noopener
newWindow.opener = null;
}
}
getHrefFromProps() {
return this.props.contentState.getEntity(this.props.entityKey).getData().url;
}
render() {
const href = this.getHrefFromProps();
return (
<PopupPortal
href={href}
component="a"
dismissWithClick={true}
triggers={['onClick']}
popup={() =>
<div className="cherry">
<span className="ml-L">{this.props.labels.GO_TO_LINK}</span>
<a href={href} className="mr-S ml-S" title={href} onClick={this.goToLink}>
{href}
</a>|<button className="btn btn-link">{this.props.labels.EDIT}</button>|<button
onClick={this.handleRemove}
className="btn btn-link">
{this.props.labels.REMOVE}
</button>
</div>}>
{this.props.children}
</PopupPortal>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment