UNPKG

react-dropdown-button

Version:

A carefully crafted drop-down button for React

374 lines (278 loc) 9.66 kB
'use strict'; var React = require('react') var assign = require('object-assign') var Button = require('react-button') var cloneWithProps = React.cloneElement || require('react-clonewithprops') var hasTouch = require('has-touch') var Menu = require('react-menus') var MenuFactory = React.createFactory(Menu) function emptyFn(){} var DISPLAY_NAME = 'ReactDropDownButton' var THEME = assign({}, Button.themes) THEME.default = assign({}, THEME.default, { openedStyle: { background: 'linear-gradient(to bottom, rgb(162,210,246) 0%,rgb(151,204,245) 50%,rgb(154,206,246) 100%)', color: 'white' } }) var DropDownButton = React.createClass({ displayName: DISPLAY_NAME, getInitialState: function(){ return {} }, getDefaultProps: function(){ return { 'data-display-name': DISPLAY_NAME, stopClickPropagation: false, hideMenuOnClick: true, smartArrowPadding: true, defaultStyle: { boxSizing: 'border-box', verticalAlign: 'top', padding: 5 }, defaultArrowStyle: { display : 'inline-block', boxSizing: 'border-box', fontSize : '0.8em' }, defaultMenuStyle: {}, alignOffset: { left: 0, top: 1 }, enforceNonStatic: true, arrowPosition: 'right' } }, render: function(){ var props = this.prepareProps(this.props, this.state) var button = React.createElement(Button, React.__spread({themes: this.constructor.themes, ref: "button"}, props)) var menu = this.state.menu var wrapperProps = this.prepareWrapperProps(props) wrapperProps.children = [button, menu] var defaultWrapperFactory = React.DOM.div var wrapperFactory = props.wrapperFactory || defaultWrapperFactory var wrapper = wrapperFactory(wrapperProps) if (wrapper === undefined){ wrapper = defaultWrapperFactory(wrapperProps) } return wrapper? wrapper: button }, onClick: function(props, event) { if (event && event.nativeEvent && event.nativeEvent.clickInsideMenu){ //there was a click inside the menu, //so don't count that as a click in the button return } props.stopClickPropagation && event.stopPropagation() this.ignoreClick(function(){ //in order to get picked up after the click event has propagated to the window this.toggleMenu(props) }) }, prepareWrapperProps: function(props) { var defaultWrapperStyle = { verticalAlign: 'top', display: props.block? 'block': 'inline-block' } var wrapperStyle = assign({}, defaultWrapperStyle, props.wrapperStyle) //enforce relative position so that menu gets rendered correctly wrapperStyle.position = wrapperStyle.position == 'absolute'? wrapperStyle.position: 'relative' wrapperStyle.overflow = 'visible' var wrapperProps = assign({ style: wrapperStyle }, props.wrapperProps) return wrapperProps }, prepareProps: function(thisProps, state) { var props = assign({}, thisProps) props.style = this.prepareStyle(props) props.addStateStyle = this.addStateStyle props.renderChildren = this.renderChildren.bind(this, props) props.onClick = this.handleClick.bind(this, props) return props }, prepareStyle: function(props){ var style = assign({}, props.defaultStyle, props.style) delete props.defaultStyle return style }, handleClick: function(props, event) { var args = [].slice.call(arguments, 1) ;(this.props.onClick || emptyFn).apply(null, args) this.onClick(props, event) }, addStateStyle: function(names) { if (this.menu){ names.push('openedStyle') } }, renderChildren: function(props, children) { var arrow = this.renderArrow(props) var leftArrow var rightArrow = arrow if (props.arrowPosition != 'right'){ leftArrow = arrow rightArrow = null } var children = [ // this.state.menu, leftArrow, children, rightArrow ] if (typeof this.props.renderChildren == 'function'){ return this.props.renderChildren(children) } return children }, renderArrow: function(props) { var arrow = props.arrow if (props.renderArrow){ arrow = props.renderArrow(props) } if (arrow === false){ return } return arrow || this.getDefaultArrow(props) }, getDefaultArrow: function(props) { var otherSide = props.arrowPosition != 'right'? 'Right': 'Left' var defaultArrowStyle = props.defaultArrowStyle if (props.smartArrowPadding){ defaultArrowStyle = assign({}, defaultArrowStyle) defaultArrowStyle['padding' + otherSide] = props.style.padding } var arrowStyle = assign({}, defaultArrowStyle, props.arrowStyle) return React.createElement("span", {style: arrowStyle}, "▼") }, toggleMenu: function(props) { if (this.menu){ this.hideMenu() } else { this.showMenu(props) } }, setMenu: function(menu) { this.menu = menu if (typeof this.props.onMenuChange == 'function'){ this.props.onMenuChange(menu, this, this._rootNodeID) } if (this.props.renderMenu === false){ return } this.setState({ menu: menu }) }, showMenu: function(props) { var target = props.getAlignTarget? props.getAlignTarget.call(this): this.refs.button.getDOMNode() this.removeClickListener() window.addEventListener('click', this.clickEventListener = this.onDocumentClick) var menuProps = assign({}, props.menuProps) var menuStyle = assign({ position: 'absolute', zIndex: 1 }, props.defaultMenuStyle, menuProps.style, props.menuStyle ) menuProps.style = menuStyle menuProps.alignPositions = props.alignPositions || menuProps.alignPositions || [ 'tl-bl', 'tr-br', 'bl-tl', 'br-tr' ] var prevOnChildClick = menuProps.onChildClick assign(menuProps, { // onClick : this.onMenuItemClick.bind(this, props), onChildClick : this.onMenuChildClick.bind(this, props), alignOffset: props.alignOffset || menuProps.alignOffset, alignTo : target, items : menuProps.items || props.items }) var menu if (props.menu){ if (props.menu.props){ prevOnChildClick = props.menu.props.onChildClick || prevOnChildClick } menu = cloneWithProps(props.menu, menuProps) } else { menu = typeof props.menuFactory == 'function'? props.menuFactory(menuProps): undefined if (menu === undefined && menuProps.items){ menu = MenuFactory(menuProps) } } this.prevOnChildClick = prevOnChildClick this.setMenu(menu) }, isExpanderClick: function(event) { if ( hasTouch && event && ((event.nativeEvent && event.nativeEvent.expanderClick) || event.expanderClick) ){ return true } return false }, onDocumentClick: function(event) { if ( this.ignoreWindowClick || this.isExpanderClick(event) ){ return } this.hideMenu() }, componentWillUnmount: function(){ this.removeClickListener() this.setMenu(false) }, removeClickListener: function() { if (this.clickEventListener){ window.removeEventListener('click', this.clickEventListener) this.clickEventListener = null } }, hideMenu: function() { if (this.menu){ this.removeClickListener() this.setMenu(false) } }, onMenuChildClick: function(props, event, itemProps) { if (itemProps === undefined){ //default click event propagated, not called by the menu cmp return } if (this.isExpanderClick(event)){ return } if (typeof this.prevOnChildClick == 'function'){ this.prevOnChildClick(event, itemProps) } event.nativeEvent.clickInsideMenu = true ;(this.props.onMenuClick || emptyFn)(event, itemProps) if (!this.props.hideMenuOnClick || event.nativeEvent.hideMenu === false || (itemProps && itemProps.data && itemProps.data.hideMenu === false)){ this.ignoreClick() } else { //when menu is hidden, mouseleave from button is not triggered, so we trigger it manually this.menu && this.refs.button && this.refs.button.handleMouseLeave(props, event) this.hideMenu() } }, ignoreClick: function(callback) { this.ignoreWindowClick = true setTimeout(function(){ this.ignoreWindowClick = false typeof callback == 'function' && callback.call(this) }.bind(this), 0) } }) DropDownButton.themes = THEME module.exports = DropDownButton