├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── example ├── App.jsx ├── Fork.jsx ├── ModalSample.jsx ├── index.html ├── index.js ├── store │ ├── index.js │ ├── modules │ │ └── user │ │ │ ├── actions.js │ │ │ └── index.js │ └── reducers.js └── styles.css ├── package.json ├── src ├── Modal.jsx ├── components │ ├── Content │ │ ├── components │ │ │ ├── Footer │ │ │ │ ├── components │ │ │ │ │ └── Button │ │ │ │ │ │ └── index.jsx │ │ │ │ └── index.jsx │ │ │ ├── Header │ │ │ │ └── index.jsx │ │ │ └── Main │ │ │ │ └── index.jsx │ │ └── index.jsx │ └── ModalDefault │ │ └── index.jsx ├── index.js ├── modules │ ├── actions.js │ ├── index.js │ └── selectors.js └── styled.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-class-properties", "ramda"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # misc 10 | .DS_Store 11 | 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | /libs 17 | bundle.js 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Nghiep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DEPRECATED] REACT-REDUX-MODAL-FLEX 2 | 3 | [![NPM version](https://img.shields.io/npm/v/react-redux-modal-flex.svg)](https://www.npmjs.com/package/react-redux-modal-flex) 4 | [![NPM monthly download](https://img.shields.io/npm/dm/react-redux-modal-flex.svg)](https://www.npmjs.com/package/react-redux-modal-flex) 5 | 6 | > Make easy a modal/popup with Redux. 7 | 8 | :warning: This will work only with React 16.3+ :warning: 9 | 10 | If you're looking for a version for React 16.2 (for individual pages) use [1.x branch](https://github.com/nghiepit/react-redux-modal-flex/tree/1.x) branch. 11 | 12 | ## Demo 13 | 14 | [https://react-redux-modal-flex.netlify.com](https://react-redux-modal-flex.netlify.com) 15 | 16 | ## Features 17 | 18 | - Responsive 19 | - Easy custom `animation` effect by [Animate.css](https://daneden.github.io/animate.css/) 20 | 21 | ## Installation 22 | 23 | To install the stable version you can use: 24 | 25 | ```sh 26 | $ yarn add react-redux-modal-flex 27 | ``` 28 | 29 | ## Example 30 | 31 | ### Step 1: 32 | 33 | `rootReducer.js` 34 | 35 | ```js 36 | import { combineReducers } from "redux"; 37 | import { reducer as modal } from "react-redux-modal-flex"; 38 | import todos from "./todos"; 39 | 40 | export default combineReducers({ 41 | todos, 42 | modal: modal({ 43 | classContent: "modal-content", 44 | animation: "zoomIn", 45 | duration: 200, 46 | mask: true, 47 | /* initial state, see API reference */ 48 | }), 49 | }); 50 | ``` 51 | 52 | ### Step 2: 53 | 54 | `App.js` 55 | 56 | ```jsx 57 | import Modal from "react-redux-modal-flex"; 58 | 59 | class App extends React.Component { 60 | render() { 61 | return ( 62 | 63 |
64 | 65 | 66 | 67 | 68 | 69 |
70 |
71 | ); 72 | } 73 | } 74 | ``` 75 | 76 | ### Step 3: 77 | 78 | Any `Container` you want to use 79 | 80 | ```jsx 81 | import { connect } from "react-redux"; 82 | import { actions as ModalActions } from "react-redux-modal-flex"; 83 | 84 | class LoginModal extends React.Component { 85 | render() { 86 | return ( 87 |
88 |
89 | 90 | 91 |
92 |
93 | 94 | 95 |
96 |
97 | ); 98 | } 99 | } 100 | 101 | class Auth extends React.Component { 102 | render() { 103 | return ( 104 |
105 |

Auth

106 | 119 |
120 | ); 121 | } 122 | } 123 | 124 | export default connect(null, { toggleModal: ModalActions.toggleModal })(Auth); 125 | ``` 126 | 127 | ## API 128 | 129 | - initState: you can overwrite default initial state 130 | 131 | ```js 132 | const initState = { 133 | classContent: "modal-content", 134 | animation: "zoomIn", 135 | duration: 300, 136 | mask: true, 137 | closeByMask: true, 138 | component: ModalDefault, 139 | title: "This is a title", 140 | closeBtn: true, 141 | textCancel: "Cancel", 142 | ok: { 143 | text: "OK", 144 | classOk: "modal-btn-ok", 145 | disabled: false, 146 | action: () => console.log("OK clicked"), 147 | }, 148 | }; 149 | ``` 150 | 151 | - API 152 | 153 | ```js 154 | import Modal, { 155 | reducer as modal, 156 | actions as ModalActions, 157 | } from "react-redux-modal-flex"; 158 | const { toggleModal, modifyOkModal } = ModalActions; 159 | ``` 160 | 161 | - `` is component, using in our `App.js` 162 | - `reducer` using in our `rootReducer.js` you can custom default initial state 163 | 164 | ```js 165 | export default combineReducers({ 166 | todos, 167 | modal: modal({ 168 | textCancel: "Close", 169 | title: "My default title", 170 | }), 171 | }); 172 | ``` 173 | 174 | - `toggleModal` and `modifyOkModal` is action 175 | 176 | ## Usage 177 | 178 | - Open Modal by action `toggleModal(options)` 179 | - `options`: is object and look like the `initState` above 180 | - Example: 181 | 182 | ```jsx 183 | ... 184 | render() { 185 | return ( 186 | 195 | ); 196 | } 197 | ... 198 | ``` 199 | 200 | - Close Modal `toggleModal(false)` or any value excepted object 201 | - Modify button `OK`: `modifyOkModal(options)` usage like `toggleModal` 202 | - Example: 203 | 204 | ```js 205 | onClick={() => this.props.modifyOkModal({ 206 | text: 'Sign up', 207 | disabled: true 208 | })} 209 | ``` 210 | 211 | - Hide `Header` if the `title` is null 212 | - Hide `Cancel` button if the `textCancel` is null 213 | - Hide `Ok` button if `ok: {text: null}` 214 | - Hide Footer if the `Cancel` and `Ok` are hidden 215 | 216 | ## License 217 | 218 | MIT 219 | -------------------------------------------------------------------------------- /example/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {pick} from 'ramda'; 4 | 5 | import {actions as ModalActions} from '../src'; 6 | 7 | import ModalSample from './ModalSample'; 8 | 9 | class App extends React.Component { 10 | render() { 11 | return ( 12 |
13 |

REACT-REDUX-MODAL-FLEX

14 |

Make easy a modal/popup with Redux

15 |
16 | 27 | 28 | 40 | 41 | 54 |
55 | 56 |
57 | More animations from{' '} 58 | 59 | Animate.css 60 | 61 |
62 |
63 | ); 64 | } 65 | } 66 | 67 | export default connect( 68 | null, 69 | pick(['toggleModal'], ModalActions), 70 | )(App); 71 | -------------------------------------------------------------------------------- /example/Fork.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 | 8 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /example/ModalSample.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ModalSample extends React.Component { 4 | state = { 5 | style: { 6 | padding: '20px 0px', 7 | width: '500px', 8 | }, 9 | }; 10 | 11 | static defaultProps = { 12 | content: 'My modal popup', 13 | }; 14 | 15 | render() { 16 | return
{this.props.content}
; 17 | } 18 | } 19 | 20 | export default ModalSample; 21 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | REACT-REDUX-MODAL-FLEX 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import createStore from './store'; 6 | import reducers from './store/reducers'; 7 | 8 | import Modal from '../src'; 9 | 10 | import App from './App'; 11 | import Fork from './Fork'; 12 | 13 | import './styles.css'; 14 | 15 | ReactDOM.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | , 23 | document.getElementById('app') 24 | ); 25 | -------------------------------------------------------------------------------- /example/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | 3 | const __DEV__ = process.env.NODE_ENV !== 'production'; 4 | 5 | const configureStore = reducers => { 6 | const middlewares = []; 7 | 8 | if (__DEV__) { 9 | // middlewares.push(); 10 | } 11 | 12 | let composeEnhancers = compose; 13 | 14 | if (__DEV__) { 15 | if (typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ === 'function') { 16 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; 17 | } 18 | } 19 | 20 | return createStore( 21 | reducers, 22 | composeEnhancers(applyMiddleware(...middlewares)) 23 | ); 24 | }; 25 | 26 | export default configureStore; 27 | -------------------------------------------------------------------------------- /example/store/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | export const LOGIN = 'user/LOGIN'; 2 | -------------------------------------------------------------------------------- /example/store/modules/user/index.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | 3 | const initState = { 4 | username: 'username', 5 | password: 'mypassword', 6 | }; 7 | 8 | export default (state = initState, action) => { 9 | switch (action.type) { 10 | case actions.LOGIN: 11 | return state; 12 | 13 | default: 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /example/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import user from './modules/user'; 4 | import { reducer as modal } from '../../src'; 5 | 6 | const reducers = combineReducers({ 7 | user, 8 | modal: modal({ textCancel: 'Close' }), 9 | }); 10 | 11 | export default reducers; 12 | -------------------------------------------------------------------------------- /example/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: 'Roboto', sans-serif; 9 | font-size: 16px; 10 | color: #525252; 11 | background: #f0fbff; 12 | } 13 | 14 | .App { 15 | position: fixed; 16 | top: 50%; 17 | left: 50%; 18 | transform: translate(-50%, -50%); 19 | } 20 | 21 | h1, 22 | h2, 23 | h3 { 24 | background-image: linear-gradient( 25 | 90deg, 26 | #f79533 0%, 27 | #f37055 15%, 28 | #ef4e7b 30%, 29 | #a166ab 44%, 30 | #5073b8 58%, 31 | #1098ad 72%, 32 | #07b39b 86%, 33 | #6dba82 100% 34 | ); 35 | background-size: cover; 36 | -webkit-background-clip: text; 37 | -webkit-text-fill-color: transparent; 38 | background-clip: text; 39 | text-fill-color: transparent; 40 | font-weight: 100; 41 | } 42 | 43 | button { 44 | padding: 10px 20px; 45 | outline: none; 46 | background: #559ef1; 47 | color: #ffffff; 48 | border: none; 49 | font-size: 1.2rem; 50 | cursor: pointer; 51 | } 52 | 53 | .text-center { 54 | text-align: center; 55 | } 56 | 57 | .buttons { 58 | width: 100%; 59 | display: flex; 60 | justify-content: space-around; 61 | margin: 20px 0; 62 | } 63 | 64 | .animated.flipOutX, 65 | .animated.flipOutY, 66 | .animated.bounceIn, 67 | .animated.bounceOut { 68 | animation-duration: 0.75s; 69 | } 70 | 71 | @keyframes fadeIn { 72 | from { 73 | opacity: 0; 74 | } 75 | 76 | to { 77 | opacity: 1; 78 | } 79 | } 80 | 81 | .fadeIn { 82 | animation-name: fadeIn; 83 | } 84 | 85 | @keyframes bounceIn { 86 | from, 87 | 20%, 88 | 40%, 89 | 60%, 90 | 80%, 91 | to { 92 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 93 | } 94 | 95 | 0% { 96 | opacity: 0; 97 | transform: scale3d(0.3, 0.3, 0.3); 98 | } 99 | 100 | 20% { 101 | transform: scale3d(1.1, 1.1, 1.1); 102 | } 103 | 104 | 40% { 105 | transform: scale3d(0.9, 0.9, 0.9); 106 | } 107 | 108 | 60% { 109 | opacity: 1; 110 | transform: scale3d(1.03, 1.03, 1.03); 111 | } 112 | 113 | 80% { 114 | transform: scale3d(0.97, 0.97, 0.97); 115 | } 116 | 117 | to { 118 | opacity: 1; 119 | transform: scale3d(1, 1, 1); 120 | } 121 | } 122 | 123 | .bounceIn { 124 | animation-name: bounceIn; 125 | } 126 | 127 | .github-corner__svg { 128 | fill: #82888a; 129 | color: #2c2d31; 130 | position: absolute; 131 | top: 0; 132 | border: 0; 133 | right: 0; 134 | } 135 | 136 | .github-corner:hover .octo-arm { 137 | animation: octocat-wave 560ms ease-in-out; 138 | } 139 | 140 | @keyframes octocat-wave { 141 | 0%, 142 | 100% { 143 | transform: rotate(0); 144 | } 145 | 20%, 146 | 60% { 147 | transform: rotate(-25deg); 148 | } 149 | 40%, 150 | 80% { 151 | transform: rotate(10deg); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-modal-flex", 3 | "version": "2.0.3", 4 | "description": "Make easy a modal/popup with Redux", 5 | "main": "./libs/index.js", 6 | "files": [ 7 | "libs" 8 | ], 9 | "scripts": { 10 | "clean": "rimraf lib", 11 | "dev": "webpack-dev-server", 12 | "build": "cross-env NODE_ENV=production webpack --progress --profile -p", 13 | "demo": "npm run build && mv bundle.js example/bundle.js", 14 | "lib": "babel src -d libs --source-maps", 15 | "prepublishOnly": "npm run lib" 16 | }, 17 | "devDependencies": { 18 | "babel-cli": "^6.26.0", 19 | "babel-core": "6.26.3", 20 | "babel-loader": "^7.1.2", 21 | "babel-plugin-ramda": "^1.6.3", 22 | "babel-plugin-transform-class-properties": "^6.24.1", 23 | "babel-preset-env": "^1.7.0", 24 | "babel-preset-react": "^6.24.1", 25 | "classnames": "^2.2.6", 26 | "cross-env": "^5.2.0", 27 | "css-loader": "^0.28.7", 28 | "prop-types": "^15.0.0 || ^16.0.0", 29 | "react": "^16.3.0", 30 | "react-dom": "^16.3.0", 31 | "react-redux": "^4.0.0 || ^5.0.0", 32 | "redux": "^3.0.0 || ^4.0.0", 33 | "reselect": "^4.0.0", 34 | "rimraf": "^2.6.2", 35 | "style-loader": "^0.23.1", 36 | "styled-components": "^4.0.0", 37 | "webpack": "^3.10.0", 38 | "webpack-dev-server": "^2.9.7" 39 | }, 40 | "peerDependencies": { 41 | "classnames": "^2.2.6", 42 | "prop-types": "^15.0.0 || ^16.0.0", 43 | "react": "^16.3.0", 44 | "react-dom": "^16.3.0", 45 | "react-redux": "^4.0.0 || ^5.0.0", 46 | "redux": "^3.0.0 || ^4.0.0", 47 | "reselect": "^4.0.0", 48 | "styled-components": "^4.0.0" 49 | }, 50 | "homepage": "https://github.com/nghiepit/react-redux-modal-flex", 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/nghiepit/react-redux-modal-flex.git" 54 | }, 55 | "bugs": { 56 | "url": "https://github.com/nghiepit/react-redux-modal-flex/issues" 57 | }, 58 | "keywords": [ 59 | "react", 60 | "redux", 61 | "modal", 62 | "popup", 63 | "dialog", 64 | "alert" 65 | ], 66 | "author": "Nghiep ", 67 | "license": "MIT" 68 | } 69 | -------------------------------------------------------------------------------- /src/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import cls from 'classnames'; 5 | import {connect} from 'react-redux'; 6 | import {createStructuredSelector} from 'reselect'; 7 | import {pick, is} from 'ramda'; 8 | 9 | import {GlobalStyle} from './styled'; 10 | import {actions as ModalActions, selectors as ModalSelectors} from './modules'; 11 | 12 | import Content from './components/Content'; 13 | 14 | const Wrapper = styled.div` 15 | position: fixed; 16 | width: 100%; 17 | height: 100%; 18 | top: 0px; 19 | left: 0px; 20 | overflow: hidden; 21 | z-index: 9999; 22 | display: ${props => (props.show ? 'block' : 'none')}; 23 | `; 24 | 25 | class Modal extends React.Component { 26 | static propTypes = { 27 | show: PropTypes.bool.isRequired, 28 | Component: PropTypes.func, 29 | classContent: PropTypes.string.isRequired, 30 | animation: PropTypes.string.isRequired, 31 | duration: PropTypes.number.isRequired, 32 | mask: PropTypes.bool.isRequired, 33 | closeByMask: PropTypes.bool.isRequired, 34 | title: PropTypes.string, 35 | closeBtn: PropTypes.bool.isRequired, 36 | textOk: PropTypes.string, 37 | classOk: PropTypes.string.isRequired, 38 | actionOk: PropTypes.func, 39 | disabledOk: PropTypes.bool, 40 | textCancel: PropTypes.string, 41 | isFooter: PropTypes.bool.isRequired, 42 | toggleModal: PropTypes.func.isRequired, 43 | modifyOkModal: PropTypes.func.isRequired, 44 | }; 45 | 46 | render() { 47 | const { 48 | show, 49 | classContent, 50 | animation, 51 | Component, 52 | toggleModal, 53 | modifyOkModal, 54 | } = this.props; 55 | 56 | return ( 57 | 58 | 59 | 60 | 63 | {show && is(Function, Component) && ( 64 | 68 | )} 69 | 70 | 71 | 72 | ); 73 | } 74 | } 75 | 76 | export default connect( 77 | createStructuredSelector( 78 | pick( 79 | [ 80 | 'show', 81 | 'Component', 82 | 'classContent', 83 | 'animation', 84 | 'duration', 85 | 'mask', 86 | 'closeByMask', 87 | 'title', 88 | 'closeBtn', 89 | 'textOk', 90 | 'classOk', 91 | 'actionOk', 92 | 'disabledOk', 93 | 'textCancel', 94 | 'isFooter', 95 | ], 96 | ModalSelectors, 97 | ), 98 | ), 99 | pick(['toggleModal', 'modifyOkModal'], ModalActions), 100 | )(Modal); 101 | -------------------------------------------------------------------------------- /src/components/Content/components/Footer/components/Button/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | import cls from 'classnames'; 5 | 6 | const Wrapper = styled.span` 7 | color: #83878d; 8 | border-radius: 5px; 9 | cursor: pointer; 10 | padding: 8px 12px; 11 | text-transform: uppercase; 12 | transition: all 0.2s; 13 | &:hover { 14 | background: #f2fbff; 15 | color: #6d7379; 16 | } 17 | &.primary { 18 | color: #fc4a4a; 19 | } 20 | &.isDisabled { 21 | pointer-events: none; 22 | opacity: 0.6; 23 | } 24 | `; 25 | 26 | const Button = ({ children, isDisabled, onClick, primary, className }) => ( 27 | 31 | {children} 32 | 33 | ); 34 | 35 | Button.propTypes = { 36 | children: PropTypes.node.isRequired, 37 | primary: PropTypes.bool, 38 | isDisabled: PropTypes.bool, 39 | onClick: PropTypes.func, 40 | className: PropTypes.string, 41 | }; 42 | 43 | export default Button; 44 | -------------------------------------------------------------------------------- /src/components/Content/components/Footer/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | import Button from './components/Button'; 6 | 7 | const Wrapper = styled.footer` 8 | margin-top: auto; 9 | padding: 10px; 10 | border-top: 1px solid #e5e5e9; 11 | display: flex; 12 | justify-content: flex-end; 13 | align-items: center; 14 | flex: 1 0 auto; 15 | `; 16 | 17 | const Footer = ({ 18 | textOk, 19 | classOk, 20 | textCancel, 21 | toggleModal, 22 | disabledOk, 23 | actionOk, 24 | }) => ( 25 | 26 | {textCancel && ( 27 | 28 | )} 29 | {textOk && ( 30 | 38 | )} 39 | 40 | ); 41 | 42 | Footer.propTypes = { 43 | textOk: PropTypes.string, 44 | classOk: PropTypes.string.isRequired, 45 | textCancel: PropTypes.string, 46 | toggleModal: PropTypes.func.isRequired, 47 | actionOk: PropTypes.func, 48 | disabledOk: PropTypes.bool.isRequired, 49 | className: PropTypes.string.isRequired, 50 | }; 51 | 52 | export default Footer; 53 | -------------------------------------------------------------------------------- /src/components/Content/components/Header/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | const Wrapper = styled.h4` 6 | position: relative; 7 | flex: 1 0 auto; 8 | background: #fafafc; 9 | color: #111; 10 | border-bottom: 1px solid #ebebee; 11 | margin: 0px; 12 | padding: 6px 10px; 13 | font-weight: normal; 14 | text-align: left; 15 | `; 16 | 17 | const Close = styled.span` 18 | position: absolute; 19 | top: 50%; 20 | right: 3px; 21 | transform: translateY(-50%); 22 | cursor: pointer; 23 | right: 5px; 24 | width: 16px; 25 | height: 16px; 26 | &:before, 27 | &:after { 28 | position: absolute; 29 | content: ''; 30 | height: 2px; 31 | width: 100%; 32 | top: 50%; 33 | left: 0; 34 | margin-top: -1px; 35 | background: #999; 36 | border-radius: 100%; 37 | transition: background 0.2s; 38 | } 39 | &:before { 40 | transform: rotate(45deg); 41 | } 42 | &:after { 43 | transform: rotate(-45deg); 44 | } 45 | &:hover:before, 46 | &:hover:after { 47 | background: #333; 48 | } 49 | `; 50 | 51 | const Header = ({ title, toggleModal, closeBtn }) => 52 | title && ( 53 | 54 | {title} 55 | {closeBtn && toggleModal(false)} />} 56 | 57 | ); 58 | 59 | Header.propTypes = { 60 | title: PropTypes.string, 61 | toggleModal: PropTypes.func.isRequired, 62 | closeBtn: PropTypes.bool.isRequired, 63 | }; 64 | 65 | export default Header; 66 | -------------------------------------------------------------------------------- /src/components/Content/components/Main/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | const Wrapper = styled.section` 6 | max-width: 100%; 7 | padding: 0px 10px; 8 | overflow: auto; 9 | > * { 10 | box-sizing: border-box; 11 | max-width: 100%; 12 | } 13 | `; 14 | 15 | const Main = ({ children }) => {children}; 16 | 17 | Main.propTypes = { 18 | children: PropTypes.node, 19 | }; 20 | 21 | export default Main; 22 | -------------------------------------------------------------------------------- /src/components/Content/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components'; 4 | 5 | import Header from './components/Header'; 6 | import Footer from './components/Footer'; 7 | import Main from './components/Main'; 8 | 9 | const Wrapper = styled.section` 10 | position: absolute; 11 | width: 100%; 12 | height: 100%; 13 | top: 0px; 14 | left: 0px; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | background: rgba(0, 0, 0, 0.5); 19 | animation-duration: 0.2s; 20 | & > div.animated { 21 | display: flex; 22 | flex-direction: column; 23 | min-width: 320px; 24 | max-width: 98vw; 25 | max-height: 98vh; 26 | background: #fff; 27 | border-radius: 2px; 28 | border: 1px solid #ebebee; 29 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33); 30 | animation-duration: ${props => props.duration}ms; 31 | } 32 | `; 33 | 34 | class Content extends React.Component { 35 | static propTypes = { 36 | mask: PropTypes.bool.isRequired, 37 | className: PropTypes.string.isRequired, 38 | children: PropTypes.node, 39 | isFooter: PropTypes.bool.isRequired, 40 | duration: PropTypes.number.isRequired, 41 | toggleModal: PropTypes.func.isRequired, 42 | }; 43 | 44 | onClick = ({ target }) => { 45 | const { mask, closeByMask, toggleModal } = this.props; 46 | if (mask && closeByMask && /modal-overlay/i.test(target.className)) { 47 | toggleModal(false); 48 | } 49 | }; 50 | 51 | render() { 52 | const { mask, className, children, isFooter, duration } = this.props; 53 | 54 | return ( 55 | 60 |
61 |
62 |
{children}
63 | {isFooter &&
} 64 |
65 |
66 | ); 67 | } 68 | } 69 | 70 | export default Content; 71 | -------------------------------------------------------------------------------- /src/components/ModalDefault/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ModalDefault = () =>
My modal content
; 4 | 5 | export default ModalDefault; 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Modal from './Modal'; 2 | import reducer, {actions, selectors} from './modules'; 3 | 4 | export {reducer, actions, selectors}; 5 | 6 | export default Modal; 7 | -------------------------------------------------------------------------------- /src/modules/actions.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE_MODAL = 'modal/TOGGLE_MODAL'; 2 | export const MODIFY_OK = 'modal/MODIFY_OK'; 3 | 4 | export const toggleModal = payload => ({ 5 | type: TOGGLE_MODAL, 6 | payload, 7 | }); 8 | 9 | export const modifyOkModal = payload => ({ 10 | type: MODIFY_OK, 11 | payload, 12 | }); 13 | -------------------------------------------------------------------------------- /src/modules/index.js: -------------------------------------------------------------------------------- 1 | import { mergeWithKey, merge, is, assoc } from 'ramda'; 2 | 3 | import * as actions from './actions'; 4 | import * as selectors from './selectors'; 5 | import ModalDefault from '../components/ModalDefault'; 6 | 7 | export { actions, selectors }; 8 | 9 | export default (defaultState = {}) => { 10 | const enhancer = (k, l, r) => (k === 'ok' ? merge(l, r) : r); 11 | const initState = mergeWithKey( 12 | enhancer, 13 | { 14 | show: false, 15 | classContent: 'modal-content', 16 | animation: 'zoomIn', 17 | duration: 300, 18 | mask: true, 19 | closeByMask: true, 20 | component: ModalDefault, 21 | title: 'This is a title', 22 | closeBtn: true, 23 | textCancel: 'Cancel', 24 | ok: { 25 | text: 'OK', 26 | classOk: 'modal-btn-ok', 27 | disabled: false, 28 | action: () => console.log('OK clicked'), 29 | }, 30 | }, 31 | defaultState 32 | ); 33 | 34 | return (state = initState, action) => { 35 | switch (action.type) { 36 | case actions.TOGGLE_MODAL: 37 | if (is(Object, action.payload)) { 38 | return mergeWithKey( 39 | enhancer, 40 | initState, 41 | merge({ show: true }, action.payload) 42 | ); 43 | } 44 | return assoc('show', false, state); 45 | case actions.MODIFY_OK: 46 | return assoc('ok', merge(state.ok, action.payload), state); 47 | default: 48 | return state; 49 | } 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/modules/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | export const modal = state => state.modal; 4 | 5 | export const show = createSelector(modal, modal => modal.show); 6 | 7 | export const classContent = createSelector(modal, modal => modal.classContent); 8 | 9 | export const animation = createSelector(modal, modal => modal.animation); 10 | 11 | export const duration = createSelector(modal, modal => modal.duration); 12 | 13 | export const mask = createSelector(modal, modal => modal.mask); 14 | 15 | export const closeByMask = createSelector(modal, modal => modal.closeByMask); 16 | 17 | export const Component = createSelector(modal, modal => modal.component); 18 | 19 | export const title = createSelector(modal, modal => modal.title); 20 | 21 | export const closeBtn = createSelector(modal, modal => modal.closeBtn); 22 | 23 | export const textCancel = createSelector(modal, modal => modal.textCancel); 24 | 25 | export const ok = createSelector(modal, modal => modal.ok); 26 | 27 | export const textOk = createSelector(ok, ok => ok.text); 28 | 29 | export const classOk = createSelector(ok, ok => ok.classOk); 30 | 31 | export const actionOk = createSelector(ok, ok => ok.action); 32 | 33 | export const disabledOk = createSelector(ok, ok => ok.disabled); 34 | 35 | export const isFooter = createSelector( 36 | textCancel, 37 | textOk, 38 | (cancel, ok) => !!(ok || cancel) 39 | ); 40 | -------------------------------------------------------------------------------- /src/styled.js: -------------------------------------------------------------------------------- 1 | import {createGlobalStyle} from 'styled-components'; 2 | 3 | export const GlobalStyle = createGlobalStyle` 4 | .animated { 5 | animation-duration: 1s; 6 | animation-fill-mode: both; 7 | will-change: transform, opacity; 8 | } 9 | 10 | .animated.infinite { 11 | animation-iteration-count: infinite; 12 | } 13 | 14 | @keyframes fadeIn { 15 | from { 16 | opacity: 0; 17 | } 18 | 19 | to { 20 | opacity: 1; 21 | } 22 | } 23 | .fadeIn { 24 | animation-name: fadeIn; 25 | } 26 | 27 | @keyframes zoomIn { 28 | from { 29 | opacity: 0; 30 | transform: scale3d(.3, .3, .3); 31 | } 32 | 33 | 50% { 34 | opacity: 1; 35 | } 36 | } 37 | .zoomIn { 38 | animation-name: zoomIn; 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const webpack = require('webpack'); 4 | const path = require('path'); 5 | 6 | module.exports = { 7 | entry: { 8 | bundle: './example/index.js', 9 | }, 10 | 11 | output: { 12 | filename: '[name].js', 13 | chunkFilename: '[id].chunk.js', 14 | publicPath: '/', 15 | }, 16 | 17 | resolve: { 18 | extensions: ['.js', '.jsx'], 19 | }, 20 | 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 24 | }), 25 | new webpack.optimize.UglifyJsPlugin({ 26 | include: /\.min\.js$/, 27 | minimize: true, 28 | compress: { 29 | warnings: false, 30 | comparisons: false, 31 | }, 32 | output: { 33 | comments: false, 34 | ascii_only: true, 35 | }, 36 | }), 37 | new webpack.optimize.ModuleConcatenationPlugin(), 38 | new webpack.HotModuleReplacementPlugin(), 39 | ], 40 | 41 | module: { 42 | strictExportPresence: true, 43 | rules: [ 44 | { 45 | test: /\.(js|jsx)$/, 46 | loader: require.resolve('babel-loader'), 47 | options: { 48 | compact: true, 49 | }, 50 | }, 51 | { 52 | test: /\.css$/, 53 | use: [ 54 | { 55 | loader: 'style-loader', 56 | }, 57 | { 58 | loader: 'css-loader', 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | 65 | devServer: { 66 | contentBase: './example/', 67 | hot: true, 68 | inline: true, 69 | compress: true, 70 | port: 2345, 71 | stats: { 72 | assets: true, 73 | children: false, 74 | chunks: false, 75 | hash: false, 76 | modules: false, 77 | publicPath: false, 78 | timings: false, 79 | version: false, 80 | warnings: true, 81 | colors: { 82 | green: '\u001b[32m', 83 | }, 84 | }, 85 | }, 86 | }; 87 | --------------------------------------------------------------------------------