UNPKG

react-nav-bar

Version:

Simple yet very coustomizable navigation bar for react

277 lines (247 loc) 8.96 kB
import _ from 'lodash'; import React, { Component, PropTypes } from 'react'; import { Link, IndexLink } from 'react-router'; import FontAwesome from 'react-fontawesome'; import { Motion, spring } from 'react-motion'; import { createClassName } from './../../lib/utils'; import { DEFAULT_NAME } from './../../lib/constants'; import { springShape, toggleShape } from './../../lib/menuShapes'; export default class Menu extends Component { constructor(props) { super(props); this.state = { theme: this.props.theme || DEFAULT_NAME, opened: this.props.opened, openOnHover: typeof this.props.openOnHover === 'boolean' ? this.props.openOnHover : true, visible: this.isVisible(this.props.permission) || true }; } /** * Checks if menu should be visible * * @param {Function|Boolean} permission * @returns {*} */ isVisible(permission){ let visible = typeof permission === 'function' ? permission() : permission; return visible; } /** * Toggle this.state.opened true/false * * @param {Boolean} opened */ toggleMenu(opened){ this.setState({ opened: typeof opened!== 'undefined' ? opened : !this.state.opened }); } /** * If onAction was supplied we invoke the action and prevent default. * * @param {Object} event * @param {Function} fn * @returns {*} */ onMenuClick(event, fn){ if( !_.isFunction(fn)) return; event.preventDefault(); return fn.call(this,event); } /** * Prepare all classes and states for render. * * @returns {{displayToggle: boolean, labelClassName: *, contentClassName: *, liClassName: *, chevron: *, active: (string|*)}} */ prepareForRender(){ const menu = this.props; const { theme } = this.state; let chevron, liClassName, labelClassName, contentClassName, displayToggle = true, toggleParent = {}, toggleChild = {}, toggleDefault, active; const { parentIndex, index, toggle} = this.props; if ( typeof toggle === 'object') { displayToggle = (typeof toggle.display === 'boolean') ? toggle.display : true; toggleParent = toggle.parent || {}; toggleChild = toggle.child || {}; toggleDefault = toggle.default; } if ( typeof toggle === 'boolean') { displayToggle = toggle; } active = menu.active ? 'active' : ''; // if has parent index - this is a child menu if (parentIndex) { labelClassName = createClassName({ theme, classNames: [ 'nav-label', 'label-child', active ] }) ; contentClassName = createClassName({ theme, classNames: [ 'nav-content', 'content-child'] }) ; liClassName = createClassName({ theme, classNames: [ 'nav-li', 'child-li' ] }) ; chevron = this.state.opened ? toggleChild.opened || toggleDefault || 'chevron-right' : toggleChild.closed || toggleDefault || 'chevron-left'; } else { labelClassName = createClassName({ theme, classNames: [ 'nav-label', 'label-parent', active ] }) ; contentClassName = createClassName({ theme, classNames: [ 'nav-content' ] }) ; liClassName = createClassName({ theme, classNames: [ 'nav-li', 'parent-li' ] }) ; chevron = this.state.opened ? toggleParent.opened || toggleDefault || 'chevron-down' : toggleParent.closed || toggleDefault || 'chevron-up'; } return { displayToggle, labelClassName, contentClassName, liClassName, chevron, active }; } /** * Renders menu content with spring motion * * @param {String} contentClassName * @returns {XML} */ renderContentWithSpring(contentClassName){ const { theme } = this.state; let springOpts = this.props.spring; return ( <Motion style={{x: spring(this.state.opened ? springOpts.opened : springOpts.closed) }}> {({x}) => <div className={contentClassName + ( !this.state.opened && springOpts.closed == x ? ' '+ createClassName({ theme, classNames: 'isClosed' }) : ' '+ createClassName({ theme, classNames: 'isOpened' }) )} style={springOpts.style(x)}> <ul className={ createClassName({ theme, classNames: 'nav-ul' }) }> {this.props.children} </ul> </div> } </Motion> ); } /** * Renders menu content without spring motion * * @param {String} contentClassName * @returns {XML} */ renderContentWithoutSpring(contentClassName){ const { theme } = this.state; return ( <div className={contentClassName + ( this.state.opened ? ' '+ createClassName({ theme, classNames: 'isOpened' }) : ' '+ createClassName({ theme, classNames: 'isClosed' }) )}> <ul className={ createClassName({ theme, classNames: 'nav-ul' }) }> {this.props.children} </ul> </div> ); } /** * Render menu icon in-case it exists * * @returns {*} */ renderMenuIcon(){ const menu = this.props; const { theme } = this.state; return (menu.icon) ? <FontAwesome className={ createClassName({ theme, classNames: 'menu-icon' }) } name={menu.icon} /> : false; } /** * Render menu label * * @returns {*} */ renderLabel(){ const menu = this.props; return ( _.isFunction(menu.label) || _.isObject(menu.label)) ? menu.label : <Link to={menu.path} onClick={(e) =>{ this.onMenuClick(e, menu.action) }}>{menu.label}</Link>; } /** * Render toggle button incase it exists * @param {Boolean} displayToggle * @param {String} chevron * @returns {*} */ renderToggleButton({ displayToggle, chevron }){ const { theme } = this.state; return (displayToggle) ? <FontAwesome className={ createClassName({ theme, classNames: 'toggle-button' }) } name={chevron} onClick={ ()=>{ this.toggleMenu() } }/> : false; } /** * reacts render * * @returns {*} */ render() { const menu = this.props; if ( !this.state.visible ) return false; const { theme, openOnHover } = this.state; const { displayToggle, labelClassName, contentClassName, liClassName, chevron, active } = this.prepareForRender(); if(this.props.children) { let springOpts = this.props.spring; return ( <li className={liClassName + ' '+ menu.className + ( this.state.opened ? ' '+ createClassName({ theme, classNames: 'isOpened' }) : '') } onMouseEnter={ ()=> { if ( openOnHover ) this.toggleMenu(true) } } onMouseLeave={ ()=> { if ( openOnHover ) this.toggleMenu(false) } }> <div className={labelClassName}> { this.renderMenuIcon() } { this.renderLabel() } { this.renderToggleButton({ displayToggle, chevron })} </div> { (springOpts) ? this.renderContentWithSpring(contentClassName) : this.renderContentWithoutSpring(contentClassName) } </li> ); } return ( <li className={liClassName + ' '+ menu.className}> <div className={createClassName({ theme, classNames: [ 'nav-label', active ] }) }> { this.renderMenuIcon() } { this.renderLabel() } </div> </li> ); } } Menu.propTypes = { spring: springShape, toggle: PropTypes.oneOfType([ toggleShape, React.PropTypes.bool ]), index: PropTypes.number, parentIndex: PropTypes.number, openOnHover: PropTypes.bool, /** * path {String} - required - route to redirect on click. * label {String|component} - what will be the menu's text Or component instead. * active {Boolean|Function|Undefined|String} - Determines if the menu is active currently. * - If String or Undefined will check if that string is in pathname to determine if is active. * - If Boolean will do nothing and use the given value. * - If Function will invoke the function and assign the returned value to active. * action {Function} - Will get invoked when a menu item is clicked and prevent default * opened {Boolean} - Flag to indicate if submenu is opened or closed. * permission {Function|Boolean} - determines whether or not to show this menu - can be use for access control. * - If Function Will invoke the function and assign the returned value to visible * - If Boolean will be assigned to visible * subMenus {Array} - an array of submenus with the same signature. * className {String} - class name to be used for that menu(in the li) * icon {String} - specify an icon for menu. */ path: PropTypes.string.isRequired, label: PropTypes.oneOfType([ PropTypes.node, PropTypes.string ]), active: PropTypes.oneOfType([ PropTypes.bool, PropTypes.func, PropTypes.string ]), action: PropTypes.func, opened: PropTypes.bool, permission: PropTypes.oneOfType([ PropTypes.bool, PropTypes.func ]), subMenus: PropTypes.array, className: PropTypes.string, icon: PropTypes.string };