Created
January 18, 2017 04:22
-
-
Save basarat/3fef3811e502981d0bec4552e64ce78a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from "react"; | |
import * as ReactDOM from 'react-dom' | |
const Tether = require('tether'); | |
import { PropTypes, Children } from 'react'; | |
const renderElementToPropTypes = [ | |
PropTypes.string, | |
PropTypes.shape({ | |
appendChild: PropTypes.func.isRequired | |
}) | |
] | |
const childrenPropType = ({ children }, propName, componentName) => { | |
const childCount = Children.count(children) | |
if (childCount <= 0) { | |
return new Error(`${componentName} expects at least one child to use as the target element.`) | |
} else if (childCount > 2) { | |
return new Error(`Only a max of two children allowed in ${componentName}.`) | |
} | |
} | |
const attachmentPositions = [ | |
'auto auto', | |
'top left', | |
'top center', | |
'top right', | |
'middle left', | |
'middle center', | |
'middle right', | |
'bottom left', | |
'bottom center', | |
'bottom right' | |
] | |
export type AttachmentPosition = | |
'auto auto' | |
| 'top left' | |
| 'top center' | |
| 'top right' | |
| 'middle left' | |
| 'middle center' | |
| 'middle right' | |
| 'bottom left' | |
| 'bottom center' | |
| 'bottom right'; | |
/** | |
* Thether two children. | |
* - The first child becomes the parent. | |
* - The second child if present is tetherd to the parent. | |
* | |
* TSified https://github.com/souporserious/react-tether | |
*/ | |
export class TetherComponents extends React.Component<{ | |
/** | |
* The tag that is used to render the Tether element, defaults to div. | |
*/ | |
renderElementTag?: string, | |
/** | |
* Where in the DOM the Tether element is appended. | |
* Passes in any valid selector to document.querySelector. Defaults to document.body. | |
*/ | |
renderElementTo?: string, | |
onUpdate?: () => void, | |
onRepositioned?: () => void, | |
id?: string, | |
className?: string, | |
style?: React.CSSProperties, | |
/** | |
* Which point on the child | |
*/ | |
attachment: AttachmentPosition, | |
/** | |
* Which point on the parent | |
*/ | |
targetAttachment: AttachmentPosition, | |
}, {}> { | |
static propTypes = { | |
renderElementTag: PropTypes.string, | |
renderElementTo: PropTypes.oneOfType(renderElementToPropTypes), | |
attachment: PropTypes.oneOf(attachmentPositions).isRequired, | |
targetAttachment: PropTypes.oneOf(attachmentPositions).isRequired, | |
offset: PropTypes.string, | |
targetOffset: PropTypes.string, | |
targetModifier: PropTypes.string, | |
enabled: PropTypes.bool, | |
classes: PropTypes.object, | |
classPrefix: PropTypes.string, | |
optimizations: PropTypes.object, | |
constraints: PropTypes.array, | |
id: PropTypes.string, | |
className: PropTypes.string, | |
style: PropTypes.object, | |
onUpdate: PropTypes.func, | |
onRepositioned: PropTypes.func, | |
children: childrenPropType | |
} | |
static defaultProps = { | |
renderElementTag: 'div', | |
renderElementTo: null | |
} | |
_targetNode = null | |
_elementParentNode = null | |
_tether: any = null; | |
componentDidMount() { | |
this._targetNode = ReactDOM.findDOMNode(this as any); | |
this._update(); | |
this._registerEventListeners(); | |
} | |
componentDidUpdate(prevProps) { | |
this._update(); | |
} | |
componentWillUnmount() { | |
this._destroy(); | |
} | |
getTetherInstance() { | |
return this._tether; | |
} | |
disable() { | |
this._tether.disable(); | |
} | |
enable() { | |
this._tether.enable(); | |
} | |
on(event, handler) { | |
this._tether.on(event, handler); | |
} | |
once(event, handler) { | |
this._tether.once(event, handler); | |
} | |
off(event, handler) { | |
this._tether.off(event, handler); | |
} | |
position() { | |
this._tether.position(); | |
} | |
_registerEventListeners() { | |
if (this.props.onUpdate) { | |
this.on('update', this.props.onUpdate); | |
} | |
if (this.props.onRepositioned) { | |
this.on('repositioned', this.props.onRepositioned); | |
} | |
} | |
get _renderNode() { | |
const { renderElementTo } = this.props | |
if (typeof renderElementTo === 'string') { | |
return document.querySelector(renderElementTo) | |
} else { | |
return renderElementTo || document.body | |
} | |
} | |
_destroy() { | |
if (this._elementParentNode) { | |
ReactDOM.unmountComponentAtNode(this._elementParentNode) | |
this._elementParentNode.parentNode.removeChild(this._elementParentNode) | |
} | |
if (this._tether) { | |
this._tether.destroy() | |
} | |
this._elementParentNode = null | |
this._tether = null | |
} | |
_update() { | |
const { children, renderElementTag } = this.props | |
const elementComponent = Children.toArray(children)[1] | |
// if no element component provided, bail out | |
if (!elementComponent) { | |
// destroy Tether element if it has been created | |
if (this._tether) { | |
this._destroy() | |
} | |
return | |
} | |
// create element node container if it hasn't been yet | |
if (!this._elementParentNode) { | |
// create a node that we can stick our content Component in | |
this._elementParentNode = document.createElement(renderElementTag) | |
// append node to the render node | |
this._renderNode.appendChild(this._elementParentNode) | |
} | |
// render element component into the DOM | |
ReactDOM.unstable_renderSubtreeIntoContainer( | |
this as any, elementComponent as any, this._elementParentNode, () => { | |
// don't update Tether until the subtree has finished rendering | |
this._updateTether() | |
} | |
) | |
} | |
_updateTether() { | |
const { children, renderElementTag, renderElementTo, id, className, style, ...options } = this.props | |
const tetherOptions = { | |
target: this._targetNode, | |
element: this._elementParentNode, | |
...options | |
} | |
if (id) { | |
this._elementParentNode.id = id | |
} | |
if (className) { | |
this._elementParentNode.className = className | |
} | |
if (style) { | |
Object.keys(style).forEach(key => { | |
this._elementParentNode.style[key] = style[key] | |
}) | |
} | |
if (!this._tether) { | |
this._tether = new Tether(tetherOptions) | |
} else { | |
this._tether.setOptions(tetherOptions) | |
} | |
this._tether.position() | |
} | |
render() { | |
return Children.toArray(this.props.children)[0] as any; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment