UNPKG

twreporter-react

Version:

React-Redux site for The Reporter Foundation in Taiwan

242 lines (201 loc) 7.54 kB
'use strict'; import React from 'react'; import ReactDOM from 'react-dom'; import Radium from 'radium'; import baseStyles from './baseStyles'; import BurgerIcon from './BurgerIcon'; import CrossIcon from './CrossIcon'; export default (styles) => { return Radium(React.createClass({ propTypes: { customBurgerIcon: React.PropTypes.string, customCrossIcon: React.PropTypes.string, id: React.PropTypes.string, isOpen: React.PropTypes.bool, onStateChange: React.PropTypes.func, outerContainerId: React.PropTypes.string, pageWrapId: React.PropTypes.string, right: React.PropTypes.bool, styles: React.PropTypes.object, width: React.PropTypes.number }, toggleMenu() { // Order important: handle wrappers before setting sidebar state. this.applyWrapperStyles(); const newState = { isOpen: !this.state.isOpen }; this.setState(newState, this.props.onStateChange.bind(null, newState)); }, // Applies component-specific styles to external wrapper elements. applyWrapperStyles() { if (styles.pageWrap && this.props.pageWrapId) { this.handleExternalWrapper(this.props.pageWrapId, styles.pageWrap, true); } if (styles.outerContainer && this.props.outerContainerId) { this.handleExternalWrapper(this.props.outerContainerId, styles.outerContainer, true); } }, // Removes component-specific styles applied to external wrapper elements. clearWrapperStyles() { if (styles.pageWrap && this.props.pageWrapId) { this.handleExternalWrapper(this.props.pageWrapId, styles.pageWrap, false); } if (styles.outerContainer && this.props.outerContainerId) { this.handleExternalWrapper(this.props.outerContainerId, styles.outerContainer, false); } }, // Sets or unsets styles on DOM elements outside the menu component. // This is necessary for correct page interaction with some of the menus. // Throws and returns if the required external elements don't exist, // which means any external page animations won't be applied. handleExternalWrapper(id, wrapperStyles, set) { let html = document.querySelector('html'); let body = document.querySelector('body'); let wrapper = document.getElementById(id); if (!wrapper) { console.error("Element with ID '" + id + "' not found"); return; } wrapperStyles = wrapperStyles(this.state.isOpen, this.props.width, this.props.right); for (let prop in wrapperStyles) { if (wrapperStyles.hasOwnProperty(prop)) { wrapper.style[prop] = set ? wrapperStyles[prop] : ''; } } // Prevent any horizontal scroll. [html, body].forEach((element) => { element.style['overflow-x'] = set ? 'hidden' : ''; }); }, // Builds styles incrementally for a given element. getStyles(el, index) { let propName = 'bm' + el.replace(el.charAt(0), el.charAt(0).toUpperCase()); // Set base styles. let output = baseStyles[el] ? [baseStyles[el](this.state.isOpen, this.props.width, this.props.right)] : []; // Add animation-specific styles. if (styles[el]) { output.push(styles[el](this.state.isOpen, this.props.width, this.props.right, index + 1)); } // Add custom styles. if (this.props.styles[propName]) { output.push(this.props.styles[propName]); } return output; }, listenForClose(e) { e = e || window.event; if (this.state.isOpen && (e.key === 'Escape' || e.keyCode === 27)) { this.toggleMenu(); } }, getDefaultProps() { return { customBurgerIcon: '', customCrossIcon: '', id: '', isOpen: false, onStateChange: () => {}, outerContainerId: '', pageWrapId: '', right: false, styles: {}, width: 300 }; }, getInitialState() { return { isOpen: false }; }, componentWillMount() { if (!styles) { throw new Error('No styles supplied'); } // Warn if the selected menu requires external wrapper elements // but none were supplied. if (styles.pageWrap && !this.props.pageWrapId) { console.warn('No pageWrapId supplied'); } if (styles.outerContainer && !this.props.outerContainerId) { console.warn('No outerContainerId supplied'); } // Allow initial open state to be set by props. if (this.props.isOpen !== this.state.isOpen) { this.toggleMenu(); } }, componentDidMount() { window.onkeydown = this.listenForClose; }, componentWillUnmount() { window.onkeydown = null; this.clearWrapperStyles(); }, componentDidUpdate() { if (styles.svg && this.isMounted()) { // Snap.svg workaround for Webpack using imports-loader (https://github.com/webpack/imports-loader). let snap; try { snap = require('imports?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js'); } catch(e) { snap = require('snapsvg'); } let morphShape = ReactDOM.findDOMNode(this, 'bm-morph-shape'); let s = snap(morphShape); let path = s.select('path'); if (this.state.isOpen) { // Animate SVG path. styles.svg.animate(path); } else { // Reset path (timeout ensures animation happens off screen). setTimeout(() => { path.attr('d', styles.svg.pathInitial); }, 300); } } }, componentWillReceiveProps(nextProps) { // Allow open state to be controlled by props. if (nextProps.isOpen !== this.props.isOpen && nextProps.isOpen !== this.state.isOpen) { this.toggleMenu(); } }, render() { let items, svg; // Add styles to user-defined menu items. if (this.props.children) { items = React.Children.map(this.props.children, (item, index) => { let extraProps = { key: index, style: this.getStyles('item', index) }; return React.cloneElement(item, extraProps); }); } // Add a morph shape for animations that use SVG. if (styles.svg) { svg = ( <div className="bm-morph-shape" style={ this.getStyles('morphShape') }> <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 100 800" preserveAspectRatio="none"> <path d={ styles.svg.pathInitial }/> </svg> </div> ); } return ( <div> <div className="bm-overlay" onClick={ this.toggleMenu } style={ this.getStyles('overlay') }></div> <div id={ this.props.id } className={ "bm-menu-wrap" } style={ this.getStyles('menuWrap') }> { svg } <div className="bm-menu" style={ this.getStyles('menu') } > <nav className="bm-item-list" style={ this.getStyles('itemList') }> { items } </nav> </div> <div style={ this.getStyles('closeButton') }> <CrossIcon onClick={ this.toggleMenu } styles={ this.props.styles } image={ this.props.customCrossIcon } /> </div> </div> <BurgerIcon onClick={ this.toggleMenu } styles={ this.props.styles } image={ this.props.customBurgerIcon } /> </div> ); } })); };