├── .babelrc ├── .gitignore ├── .storybook └── config.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── main.bundle.js └── main.bundle.min.js ├── documentation └── api.md ├── package.json ├── src ├── Notification.jsx ├── NotificationActionArea.jsx ├── NotificationContainer.jsx ├── NotificationContentArea.jsx ├── NotificationHeaderArea.jsx ├── index.js └── lib │ ├── NotificationActions.js │ ├── constants.js │ ├── dispatcher.js │ └── store.js ├── stories └── index.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-1" 6 | ], 7 | "plugins": [ 8 | "add-module-exports" 9 | ] 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | bower_components 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | .idea/ 40 | 41 | lib -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | function loadStories() { 4 | require('../stories/index.js'); 5 | // You can require as many stories as you need. 6 | } 7 | 8 | configure(loadStories, module); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.1.0 2 | > Initial Release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Timo Hanisch 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 | # Material-UI Notifications 2 | 3 | [![https://nodei.co/npm/material-ui-notifications.png?downloads=true&downloadRank=true&stars=true](https://nodei.co/npm/material-ui-notifications.png?downloads=true&downloadRank=true&stars=true)](https://www.npmjs.com/package/material-ui-notifications) 4 | 5 | Material-UI Notification offers components and functionality to use a web version of the Material Desing notifications as seen 6 | in the [Documentation](https://material.io/guidelines/patterns/notifications.html#notifications-anatomy-of-a-notification). 7 | 8 | To implement the components we use [Material-UI](https://github.com/mui-org/material-ui) (< v1.0.0) and [React Flip Move](https://github.com/joshwcomeau/react-flip-move). 9 | 10 | # Installation 11 | 12 | To use all components you have to add `material-ui-notifications` to your dependencies. 13 | 14 | **Yarn** 15 | ```bash 16 | > yarn add material-ui-notifications 17 | ``` 18 | 19 | **npm** 20 | ```bash 21 | > npm install -S material-ui-notifications 22 | ``` 23 | # Examples 24 | 25 | **Simple usage of a notification** 26 | 27 | ```jsx 28 | { this.setState({ showNotification: false }); }} 31 | title="Timo Hanisch" 32 | text="Yeah this seems like a pretty good idea!" 33 | /> 34 | ``` 35 | 36 | **Simple usage of a notification container** 37 | 38 | ```jsx 39 | import { NotificationActions, NotificationContainer } from 'material-ui-notifications'; 40 | ... 41 |
42 | 43 | 59 |
60 | ... 61 | ``` 62 | 63 | # Demo 64 | 65 | To run the demo clone the repository and then run following commands. We use [Storybook](https://github.com/storybooks/storybook) to test 66 | 67 | ```bash 68 | > yarn 69 | 70 | > yarn storybook 71 | ``` 72 | 73 | # Documentation 74 | 75 | The documentation for all components and functionalities can be found [here](/documentation/api.md) 76 | 77 | # License 78 | The Project is Licensed under the [MIT License](/LICENSE) 79 | -------------------------------------------------------------------------------- /documentation/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | Material-UI Notifications offers multiple components and classes enabling the usage of material design conform notifications 4 | for the web. 5 | 6 | ## Notification 7 | 8 | **[React-Component]** 9 | 10 | The notification component is the base component used to render notifications. 11 | 12 | ### `headerLabel` 13 | 14 | | **Accepted Types:** | **Default Value** | **Required** | 15 | |---------------------|-------------------|-------------------| 16 | | `String` | `undefined` | `true`| 17 | 18 | The title of the application/notification in the notification header. 19 | 20 | ### `onClose` 21 | 22 | | **Accepted Types:** | **Default Value** | **Required** | 23 | |---------------------|-------------------|-------------------| 24 | | `Function` | `undefined` | `true`| 25 | 26 | Callback function which is called when the user clicks on the closing button. 27 | The created click `Event` is passed to the function. 28 | 29 | ### `title` 30 | 31 | | **Accepted Types:** | **Default Value** | **Required** | 32 | |---------------------|-------------------|-------------------| 33 | | `String` | `undefined` | `true`| 34 | 35 | Main title for the notification which is rendered into the content body of the notification. 36 | 37 | ### `text` 38 | 39 | | **Accepted Types:** | **Default Value** | **Required** | 40 | |---------------------|-------------------|-------------------| 41 | | `String` | `undefined` | `true`| 42 | 43 | Text shown in the content body below the title. 44 | 45 | ### `avatar` 46 | 47 | | **Accepted Types:** | **Default Value** | **Required** | 48 | |---------------------|-------------------|-------------------| 49 | | `Node` | `null` | `false`| 50 | 51 | An avatar which should be added to the notification. May indiciate the creator of the notification to the user. 52 | This should be a Material-UI `Avatar`. 53 | 54 | `} ... />` 55 | 56 | ### `actions` 57 | 58 | | **Accepted Types:** | **Default Value** | **Required** | 59 | |---------------------|-------------------|-------------------| 60 | | `Array` | `undefined` | `false`| 61 | 62 | An array of action objects which are shown as flat buttons at the bottom of the notification. These action 63 | objects should look like the following example. 64 | 65 | ```js 66 | { 67 | label: String, 68 | onClick: Function, 69 | } 70 | ``` 71 | 72 | The `label` is used as the button text and `onClick` is called after clicking on the corresponding button. 73 | 74 | ### `icon` 75 | 76 | | **Accepted Types:** | **Default Value** | **Required** | 77 | |---------------------|-------------------|-------------------| 78 | | `Node` | `null` | `false`| 79 | 80 | An icon to be shown on the most left side of the notification header. 81 | Should be a Material-UI icon as shown [here](http://www.material-ui.com/#/components/font-icon) or [here](http://www.material-ui.com/#/components/svg-icon). 82 | 83 | ### `primaryColor` 84 | 85 | | **Accepted Types:** | **Default Value** | **Required** | 86 | |---------------------|-------------------|-------------------| 87 | | `String` | `''` | `false`| 88 | 89 | By default notifications use the primary1color defined for the Material-UI theme for the header and actions. 90 | 91 | Notifications can use custom primary colors for example to create unique impressions for different notification types. 92 | 93 | ### `secondaryHeaderLabel` 94 | 95 | | **Accepted Types:** | **Default Value** | **Required** | 96 | |---------------------|-------------------|-------------------| 97 | | `String` | `''` | `false`| 98 | 99 | A string which is additionally drawn besides the [`headerLabel`](#headerlabel). May be used to deliver additional meta information to 100 | the user. 101 | 102 | ### `timestamp` 103 | 104 | | **Accepted Types:** | **Default Value** | **Required** | 105 | |---------------------|-------------------|-------------------| 106 | | `String` | `''` | `false`| 107 | 108 | Indiciating the time when the notification was created. 109 | 110 | ### `style` 111 | 112 | | **Accepted Types:** | **Default Value** | **Required** | 113 | |---------------------|-------------------|-------------------| 114 | | `Object` | `{}` | `false`| 115 | 116 | Inline styles which are applied to the notification container. May contain the styles supported by React. 117 | 118 | ## NotificationContainer 119 | 120 | **[React-Component]** 121 | 122 | ### `appearAnimation` 123 | 124 | | **Accepted Types:** | **Default Value** | **Required** | 125 | |---------------------|-------------------|-------------------| 126 | | `Object`, `String`, `Boolean` | `{from:{opacity: 0.25}, to: {opacity: 1}}` | `false`| 127 | 128 | Control the appear animation that runs when the notification mounts. Works identically to [`enterAnimation`](#enteranimation) below, but only fires on the initial notification. 129 | 130 | ### `duration` 131 | 132 | | **Accepted Types:** | **Default Value** | **Required** | 133 | |---------------------|-------------------|-------------------| 134 | | `Number` | `350` | `false`| 135 | 136 | The length, in milliseconds, that the transition of notification ought to take. 137 | 138 | ### `easing` 139 | 140 | | **Accepted Types:** | **Default Value** | **Required** | 141 | |---------------------|-------------------|-------------------| 142 | | `String` | `cubic-bezier(0.23, 1, 0.32, 1)` | `false`| 143 | 144 | Any valid CSS3 timing function (eg. `linear`, `ease-in`, `cubic-bezier(1, 0, 0, 1)`). 145 | 146 | ### `enterAnimation` 147 | 148 | | **Accepted Types:** | **Default Value** | **Required** | 149 | |---------------------|-------------------|-------------------| 150 | | `Object`, `String`, `Boolean` | `{from:{opacity: 0.25}, to: {opacity: 1}}` | `false`| 151 | 152 | Control the onEnter animation that runs when new notifications are added to the `NotifcationContainer`. 153 | 154 | Accepts several types: 155 | 156 | **String:** You can enter one of the following presets to select that as your enter animation: 157 | * `elevator` 158 | * `fade` 159 | * `accordionVertical` 160 | * `accordionHorizontal` 161 | * `none` 162 | 163 | **Boolean:** You can enter `false` to disable the enter animation, or `true` to select the default enter animation. 164 | 165 | **Object: (default)** For fully granular control, you can pass in an object that contains the styles you'd like to animate. 166 | It requires two keys: `from` and `to`. Each key holds an object of CSS properties. You can supply any valid camelCase CSS properties, and the notification will transition between the two, over the course of the specified `duration`. 167 | 168 | Example: 169 | 170 | ```jsx 171 | const customEnterAnimation = { 172 | from: { transform: 'scale(0.5, 1)' }, 173 | to: { transform: 'scale(1, 1)' } 174 | }; 175 | 176 | 181 | ``` 182 | 183 | It is recommended that you stick to hardware-accelerated CSS properties for optimal performance: transform and opacity. 184 | 185 | --- 186 | 187 | ### `leaveAnimation` 188 | 189 | | **Accepted Types:** | **Default Value** | **Required** | 190 | |---------------------|-------------------|-------------------| 191 | | `Object`, `String`, `Boolean` | `{from:{opacity: 1}, to: {opacity: 0}}` | `false`| 192 | 193 | Control the onLeave animation that runs when notifications are removed from the `NotificationContainer`. 194 | 195 | See [`enterAnimation`](#enteranimation) for how to implement a leave animation. 196 | 197 | ### `position` 198 | 199 | | **Accepted Types:** | **Default Value** | **Required** | 200 | |---------------------|-------------------|-------------------| 201 | | `String` | `'top-right'` | `false`| 202 | 203 | The position of the container relativly to the browser view. 204 | 205 | **String:** You can enter one of the following presets to select that as your position: 206 | * `top-left` 207 | * `top-right` (default) 208 | * `bottom-left` 209 | * `bottom-right` 210 | 211 | 212 | ## NotificationActions 213 | 214 | **[Class]** 215 | 216 | The notification actions offer some functionalities to interact with the mounted [`NotificationContainer`](#notificationcontainer). 217 | 218 | ### `addNotification(notification: Object)` 219 | 220 | Adds the given notifcation to the system so it then can be rendered in the mounted [`NotificationContainer`](#notificationcontainer). 221 | 222 | The passed notification can have the following form. All properties are passed through to a created [`Notification`](#notification) object 223 | with the exception of `autoHide`. 224 | 225 | ```js 226 | { 227 | actions: Array, 228 | autoHide: Number, // Time in milliseconds after which the notification should automatically be hidden 229 | avatar: JSX.Node, 230 | headerLabel: String, 231 | icon: JSX.Node, 232 | onClose: Function, 233 | primaryColor: String, 234 | secondaryHeaderLabel: String, 235 | text: String, 236 | timestamp: String, 237 | title: String, 238 | } 239 | ``` 240 | 241 | 242 | ### `removeNotification(id: Number)` 243 | 244 | Removes the notifcation with the given id from the store. 245 | 246 | ### `resetNotifications()` 247 | 248 | Removes all notifications from the internal store. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-ui-notifications", 3 | "version": "0.1.4", 4 | "description": "Material Design spec compliant notifciation implementation for the web. Inspired by https://github.com/puranjayjain/react-materialui-notifications", 5 | "keywords": [ 6 | "react", 7 | "react-dom", 8 | "material", 9 | "design", 10 | "web", 11 | "notification", 12 | "material-ui", 13 | "component" 14 | ], 15 | "files": [ 16 | "lib", 17 | "src" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/TimoHanisch/material-ui-notifications" 22 | }, 23 | "author": "Timo Hanisch ", 24 | "main": "lib/index.js", 25 | "scripts": { 26 | "start": "webpack-dev-server --open --config webpack.dev.js", 27 | "build": "babel src --out-dir lib", 28 | "storybook": "start-storybook -p 9001 -c .storybook" 29 | }, 30 | "license": "MIT ", 31 | "dependencies": { 32 | "flux": "3.1.3", 33 | "material-ui": "0.20.0", 34 | "prop-types": "15.6.0", 35 | "react-flip-move": "2.10.0", 36 | "react": "16.x.x", 37 | "react-dom": "16.x.x" 38 | }, 39 | "peerDependencies": { 40 | "flux": "3.x.x", 41 | "material-ui": ">= 0.20.0 < 1.0.0", 42 | "prop-types": "15.x.x", 43 | "react": "16.x.x", 44 | "react-dom": "16.x.x" 45 | }, 46 | "devDependencies": { 47 | "@storybook/react": "3.2.17", 48 | "babel": "6.23.0", 49 | "babel-cli": "6.26.0", 50 | "babel-core": "6.26.0", 51 | "babel-loader": "7.1.2", 52 | "babel-plugin-add-module-exports": "0.2.1", 53 | "babel-preset-env": "1.6.1", 54 | "babel-preset-react": "6.24.1", 55 | "babel-preset-stage-1": "6.24.1", 56 | "uglifyjs-webpack-plugin": "1.1.2", 57 | "webpack": "3.8.1", 58 | "webpack-dev-server": "2.9.7" 59 | } 60 | } -------------------------------------------------------------------------------- /src/Notification.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import NotificationHeaderArea from './NotificationHeaderArea'; 5 | import NotificationContentArea from './NotificationContentArea'; 6 | import NotificationActionArea from './NotificationActionArea'; 7 | import Close from 'material-ui/svg-icons/navigation/close'; 8 | import { Transition } from 'react-transition-group'; 9 | import { Avatar, IconButton, Paper, List, ListItem } from 'material-ui'; 10 | 11 | /** 12 | * The notificaiton implemenation for the web based on material design 13 | * defined at https://material.io/guidelines/patterns/notifications.html. 14 | * 15 | * @author Timo Hanisch 16 | * @since 0.1.0 17 | */ 18 | export default class Notification extends React.Component { 19 | 20 | static propTypes = { 21 | /* The title of the application/notification in the notification header */ 22 | headerLabel: PropTypes.string.isRequired, 23 | 24 | /* The callback function which is called when the user clicks on the closing button */ 25 | onClose: PropTypes.func.isRequired, 26 | 27 | /* Main title for the notification which is rendered into the content body of the notification */ 28 | title: PropTypes.string.isRequired, 29 | 30 | /* Text shown in the content body below the title */ 31 | text: PropTypes.string.isRequired, 32 | 33 | /* An avatar which should be added to the notification. May indiciate the creator of the notification to the user */ 34 | avatar: PropTypes.node, 35 | 36 | /* An array of action objects which are shown as flat buttons at the bottom of the notification */ 37 | actions: PropTypes.arrayOf(PropTypes.shape({ 38 | label: PropTypes.string.isRequired, 39 | onClick: PropTypes.func.isRequired, 40 | })), 41 | 42 | /* An icon to be shown on the most left side of the notification header */ 43 | icon: PropTypes.node, 44 | 45 | /* By default notifications use the primary1color defined for the material-ui theme for the header and actions */ 46 | primaryColor: PropTypes.string, 47 | 48 | /* A string which is additionally drawn besides the headerLabel */ 49 | secondaryHeaderLabel: PropTypes.string, 50 | 51 | /* Indiciating the time when the notification was created */ 52 | timestamp: PropTypes.string, 53 | 54 | /* Inline styles which are applied to the notification container */ 55 | style: PropTypes.object, 56 | }; 57 | 58 | static defaultProps = { 59 | avatar: null, 60 | actionArea: null, 61 | icon: null, 62 | secondaryHeader: '', 63 | timestamp: '', 64 | primaryColor: '', 65 | style: {}, 66 | }; 67 | 68 | static STYLE = { 69 | container: { 70 | minWidth: 320, 71 | display: 'flex', 72 | flexDirection: 'column', 73 | }, 74 | }; 75 | 76 | render() { 77 | const { 78 | actions, 79 | actionContent, 80 | avatar, 81 | icon, 82 | headerLabel, 83 | onClose, 84 | primaryColor, 85 | secondaryHeaderLabel, 86 | timestamp, 87 | style, 88 | title, 89 | text, 90 | } = this.props; 91 | return ( 92 | 93 | 101 | 106 | { 107 | // Only render actions if the actions were passed to the component 108 | !!actions && ( 109 | 113 | ) 114 | } 115 | 116 | ); 117 | } 118 | } -------------------------------------------------------------------------------- /src/NotificationActionArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import muiThemeable from 'material-ui/styles/muiThemeable'; 4 | import { FlatButton } from 'material-ui'; 5 | import { grey200 } from 'material-ui/styles/colors'; 6 | 7 | /** 8 | * The action area of a notifaction as shown in https://material.io/guidelines/patterns/notifications.html#notifications-anatomy-of-a-notification 9 | * and adjusted for the web. 10 | * 11 | * @author Timo Hanisch 12 | * @since 0.1.0 13 | */ 14 | class NotificationActionArea extends React.PureComponent { 15 | 16 | static propTypes = { 17 | /* An array of action objects which are shown as flat buttons at the bottom of the notification */ 18 | actions: PropTypes.arrayOf(PropTypes.shape({ 19 | label: PropTypes.string.isRequired, 20 | onClick: PropTypes.func.isRequired, 21 | })).isRequired, 22 | 23 | /* By default notifications use the primary1color defined for the material-ui theme for the header and actions. */ 24 | primaryColor: PropTypes.string, 25 | }; 26 | 27 | static defaultProps = { 28 | primaryColor: '', 29 | }; 30 | 31 | static STYLES = { 32 | container: { 33 | display: 'flex', 34 | flexDirection: 'row', 35 | padding: 8, 36 | backgroundColor: grey200, 37 | }, 38 | button: { 39 | minWidth: null, // We do not want the default width of 88px 40 | }, 41 | buttonLabel: { 42 | padding: '0 8px 0 8px', 43 | }, 44 | }; 45 | 46 | render() { 47 | const { actions, muiTheme, primaryColor } = this.props; 48 | const styles = { 49 | // Inline style which is relies on dynamic information 50 | buttonLabel: { 51 | color: primaryColor || muiTheme.palette.primary1Color, 52 | ...NotificationActionArea.STYLES.buttonLabel, 53 | }, 54 | }; 55 | return ( 56 |
57 | { 58 | actions.map((action, index) => ( 59 | 0 ? { ...NotificationActionArea.STYLES.button, marginLeft: 8 } : NotificationActionArea.STYLES.button} 63 | labelStyle={styles.buttonLabel} 64 | onClick={action.onClick} 65 | /> 66 | )) 67 | } 68 |
69 | ); 70 | } 71 | } 72 | // Use Material-UI HOC to get access to the used primary color 73 | export default muiThemeable()(NotificationActionArea); -------------------------------------------------------------------------------- /src/NotificationContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Notification from './Notification'; 4 | import Store from './lib/store'; 5 | import NotificationActions from './lib/NotificationActions'; 6 | import FlipMove from 'react-flip-move'; 7 | import { DISPATCH_UPDATE } from './lib/constants'; 8 | 9 | /** 10 | * The notification container is used for rendering notifications in your web 11 | * application. All notifications added to the system will be children of this container. 12 | * Additionally it handles all notification animations with the help 13 | * of react-flip-move (https://github.com/joshwcomeau/react-flip-move). Documentation for 14 | * prop types is taken from react-flip-move, since they are only passed to the flip move component. 15 | * 16 | * The container itself is a fixed div container with a z-index of 1200. 17 | * 18 | * @author Timo Hanisch 19 | * @since 0.1.0 20 | */ 21 | export default class NotificationContainer extends React.Component { 22 | 23 | /** 24 | * The default enter/appear animation used by notifications added to 25 | * the notification container. 26 | * 27 | * @private 28 | */ 29 | static _enterAnimation = { 30 | from: { 31 | opacity: 0.25, 32 | }, 33 | to: { 34 | opacity: 1, 35 | }, 36 | }; 37 | 38 | /** 39 | * The default leave animation used by notifications added to 40 | * the notification container. 41 | * 42 | * @private 43 | */ 44 | static _leaveAnimation = { 45 | from: { 46 | opacity: 1, 47 | }, 48 | to: { 49 | opacity: 0, 50 | }, 51 | }; 52 | 53 | static propTypes = { 54 | /** 55 | * Control the appear animation that runs when the component mounts. Control the appear 56 | * animation that runs when the component mounts. Works identically to enterAnimation below, 57 | * but only fires on the initial children. 58 | */ 59 | appearAnimation: PropTypes.oneOfType([ 60 | PropTypes.shape({ 61 | from: PropTypes.object, 62 | to: PropTypes.object, 63 | }), 64 | PropTypes.oneOf([ 65 | 'elevator', 66 | 'fade', 67 | 'accordionVertical', 68 | 'accordionHorizontal', 69 | 'none', 70 | ]), 71 | PropTypes.bool, 72 | ]), 73 | 74 | /** 75 | * The length, in milliseconds, that the transition ought to take. 76 | */ 77 | duration: PropTypes.number, 78 | 79 | /** 80 | * Any valid CSS3 timing function (eg. "linear", "ease-in", "cubic-bezier(1, 0, 0, 1)"). 81 | */ 82 | easing: PropTypes.string, 83 | 84 | /** 85 | * Control the onEnter animation that runs when new notifcations are added to the container. 86 | */ 87 | enterAnimation: PropTypes.oneOfType([ 88 | PropTypes.shape({ 89 | from: PropTypes.object, 90 | to: PropTypes.object, 91 | }), 92 | PropTypes.oneOf([ 93 | 'elevator', 94 | 'fade', 95 | 'accordionVertical', 96 | 'accordionHorizontal', 97 | 'none', 98 | ]), 99 | PropTypes.bool, 100 | ]), 101 | 102 | /** 103 | * Control the onLeave animation that runs when new notifications are removed from the container. 104 | */ 105 | leaveAnimation: PropTypes.oneOfType([ 106 | PropTypes.shape({ 107 | from: PropTypes.object, 108 | to: PropTypes.object, 109 | }), 110 | PropTypes.oneOf([ 111 | 'elevator', 112 | 'fade', 113 | 'accordionVertical', 114 | 'accordionHorizontal', 115 | 'none', 116 | ]), 117 | PropTypes.bool, 118 | ]), 119 | 120 | /* The position of the container relativly to the browser view */ 121 | position: PropTypes.oneOf([ 122 | 'top-left', 123 | 'top-right', 124 | 'bottom-right', 125 | 'bottom-left', 126 | ]), 127 | } 128 | 129 | static defaultProps = { 130 | appearAnimation: NotificationContainer._enterAnimation, 131 | duration: 350, 132 | easing: 'cubic-bezier(0.23, 1, 0.32, 1)', 133 | enterAnimation: NotificationContainer._enterAnimation, 134 | leaveAnimation: NotificationContainer._leaveAnimation, 135 | position: 'top-right', 136 | }; 137 | 138 | static STYLE = { 139 | container: { 140 | position: 'fixed', 141 | zIndex: 1200, 142 | minWidth: 320, 143 | }, 144 | }; 145 | 146 | componentWillMount() { 147 | Store.on(DISPATCH_UPDATE, this._onStoreUpdate); 148 | } 149 | 150 | componentWillUnmount() { 151 | // When the container unmounts we want all old notifications to be cleared. 152 | // Currently only one NotificationContainer at a time is supported. 153 | NotificationActions.resetNotifications(); 154 | Store.removeListener(DISPATCH_UPDATE, this._onStoreUpdate); 155 | } 156 | 157 | /** 158 | * Callback for the close button. 159 | * 160 | * @param {Number} notificationId Id of the notification to be removed 161 | * @private 162 | */ 163 | _onNotificationClose = (notificationId) => { 164 | NotificationActions.removeNotification(notificationId); 165 | }; 166 | 167 | 168 | /** 169 | * Called when the internal notification store is updated. I.e. a notifcation is added 170 | * or removed. 171 | * 172 | * @private 173 | */ 174 | _onStoreUpdate = () => { 175 | this.forceUpdate(); 176 | }; 177 | 178 | /** 179 | * Builds the notficiaton container style with help of the passed position property. 180 | * 181 | * @private 182 | * @returns a style object used for the notification container itself. 183 | */ 184 | _builContainerStyle() { 185 | const { position } = this.props; 186 | // Depending on the chosen position we use a margin of 32 pixels from the 187 | // corresponding side. 188 | return { 189 | ...NotificationContainer.STYLE.container, 190 | bottom: position.indexOf('bottom') !== -1 && 32, 191 | left: position.indexOf('left') !== -1 && 32, 192 | right: position.indexOf('right') !== -1 && 32, 193 | top: position.indexOf('top') !== -1 && 32, 194 | }; 195 | } 196 | 197 | render() { 198 | const { appearAnimation, duration, easing, enterAnimation, leaveAnimation } = this.props; 199 | // Retrieve the notifcations from the internal store 200 | const notifications = Store.notifications; 201 | return ( 202 | 210 | { 211 | notifications.map((notification, index) => ( 212 | this._onNotificationClose(notification.id)} 216 | title={notification.title} 217 | text={notification.text} 218 | avatar={notification.avatar} 219 | actions={notification.actions} 220 | icon={notification.icon} 221 | primaryColor={notification.primaryColor} 222 | secondaryHeaderLabel={notification.secondaryHeaderLabel} 223 | timestamp={notification.timestamp} 224 | style={{ marginTop: index > 0 ? 16 : 0, ...notification.style }} 225 | /> 226 | )) 227 | } 228 | 229 | ); 230 | } 231 | } -------------------------------------------------------------------------------- /src/NotificationContentArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { grey600 } from 'material-ui/styles/colors'; 4 | 5 | /** 6 | * The content area of a notification as shown in https://material.io/guidelines/patterns/notifications.html#notifications-anatomy-of-a-notification. 7 | * 8 | * @author Timo Hanisch 9 | * @since 0.1.0 10 | */ 11 | export default class NotificationContentArea extends React.PureComponent { 12 | 13 | static propTypes = { 14 | /* Content text to be rendered in the notification */ 15 | text: PropTypes.string.isRequired, 16 | 17 | /* Title of the content rendered in the notificaton */ 18 | title: PropTypes.string.isRequired, 19 | 20 | /* An avatar (or large icon) to be shown on the right side of a notification */ 21 | avatar: PropTypes.node, 22 | }; 23 | 24 | static defaultProps = { 25 | avatar: null, 26 | }; 27 | 28 | static STYLES = { 29 | avatar: { 30 | marginLeft: 8, 31 | }, 32 | container: { 33 | padding: '8px 16px 16px 16px', 34 | display: 'flex', 35 | flexDirection: 'row', 36 | justifyContent: 'space-between', 37 | }, 38 | text: { 39 | fontSize: 14, 40 | color: grey600, 41 | marginTop: 2, 42 | }, 43 | textContainer: { 44 | display: 'flex', 45 | flexDirection: 'column', 46 | maxWidth: 392, 47 | }, 48 | title: { 49 | fontSize: 15, 50 | fontWeight: 600, 51 | }, 52 | }; 53 | 54 | render() { 55 | const { avatar, text, title } = this.props; 56 | return ( 57 |
58 |
59 | {title} 60 | {text} 61 |
62 | { 63 | // If no avatar was passed we do not render anything otherwise we render the avatar 64 | // and apply our styles to it 65 | !!avatar && React.cloneElement(avatar, { style: NotificationContentArea.STYLES.avatar }) 66 | } 67 |
68 | ); 69 | } 70 | } -------------------------------------------------------------------------------- /src/NotificationHeaderArea.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import muiThemeable from 'material-ui/styles/muiThemeable'; 4 | import { grey600 } from 'material-ui/styles/colors'; 5 | import { IconButton } from 'material-ui'; 6 | import Close from 'material-ui/svg-icons/navigation/close'; 7 | 8 | /** 9 | * Implementation of the web adjusted notification header as described at https://material.io/guidelines/patterns/notifications.html#notifications-anatomy-of-a-notification. 10 | * 11 | * @author Timo Hanisch 12 | * @since 0.1.0 13 | */ 14 | class NotificationHeaderArea extends React.PureComponent { 15 | 16 | static propTypes = { 17 | /* The title of the application/notification */ 18 | headerLabel: PropTypes.string.isRequired, 19 | 20 | /* Function which is called when the close icon button is clicked */ 21 | onClose: PropTypes.func.isRequired, 22 | 23 | /** 24 | * Icon shown on the left of the header. May be used to deliver unique 25 | * experiences with notificaitons for the user 26 | */ 27 | icon: PropTypes.node, 28 | 29 | /* A timestamp which is rendered as a secondary text besides the header */ 30 | timestamp: PropTypes.string, 31 | 32 | /* A secondary text which can be used to indicate additional meta information */ 33 | secondaryHeaderLabel: PropTypes.string, 34 | 35 | /* By default notifications use the primary1color defined for the material-ui theme for the header and actions */ 36 | primaryColor: PropTypes.string, 37 | }; 38 | 39 | static defaultProps = { 40 | icon: null, 41 | primaryColor: '', 42 | secondaryHeaderLabel: '', 43 | timestamp: '', 44 | }; 45 | 46 | static STYLES = { 47 | closeButton: { 48 | width: 22, 49 | height: 22, 50 | padding: 2, 51 | }, 52 | closeButtonIcon: { 53 | width: 18, 54 | height: 18, 55 | }, 56 | container: { 57 | padding: '16px 16px 0 16px', 58 | display: 'flex', 59 | flexDirection: 'row', 60 | alignItems: 'center', 61 | justifyContent: 'space-between', 62 | }, 63 | icon: { 64 | width: 18, 65 | height: 18, 66 | }, 67 | information: { 68 | display: 'flex', 69 | flexDirection: 'row', 70 | alignItems: 'center' 71 | }, 72 | header: { 73 | fontSize: 14, 74 | }, 75 | secondary: { 76 | color: grey600, 77 | fontSize: 14, 78 | }, 79 | separator: { 80 | color: grey600, 81 | fontSize: 14, 82 | fontWeight: 600, 83 | margin: '0 4px 0 4px' 84 | }, 85 | timestamp: { 86 | color: grey600, 87 | fontSize: 12, 88 | marginRight: 8, 89 | } 90 | }; 91 | 92 | render() { 93 | const { icon, headerLabel, timestamp, secondaryHeaderLabel, muiTheme, primaryColor, onClose } = this.props; 94 | // Since the header relies on dynamic information for styling we create the style object inside the render 95 | const styles = { 96 | headerLabel: { 97 | color: primaryColor || muiTheme.palette.primary1Color, 98 | marginLeft: icon ? 8 : 0, 99 | }, 100 | }; 101 | // Check if the notificaton should have another primary color 102 | const color = primaryColor || muiTheme.palette.primary1Color; 103 | // For secondary and meta text tags we use React.Fragment which is part of React 104 | // since 16.2. These tags are not part of the final DOM, but allow us to 105 | // return multiple tags from a since statement without cluttering the DOM 106 | // by using div containers. 107 | return ( 108 |
109 |
110 | { 111 | // Check if the icon should be rendered and set color and styles 112 | !!icon && React.cloneElement(icon, { color, style: NotificationHeaderArea.STYLES.icon }) 113 | } 114 | {headerLabel} 115 | { 116 | // Check if the secondary header should be rendered and add a separator if it 117 | // should be rendered 118 | !!secondaryHeaderLabel && ( 119 | 120 | · 121 | {secondaryHeaderLabel} 122 | 123 | ) 124 | } 125 | { 126 | // Same as for the secondary header 127 | !!timestamp && ( 128 | 129 | · 130 | {timestamp} 131 | 132 | ) 133 | } 134 |
135 | {/* Button used to close the notifcation */} 136 | 141 | 142 | 143 |
144 | ); 145 | } 146 | } 147 | export default muiThemeable()(NotificationHeaderArea); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Notification } from './Notification'; 2 | export { default as NotificationActions } from './lib/NotificationActions'; 3 | export { default as NotificationContainer } from './NotificationContainer'; -------------------------------------------------------------------------------- /src/lib/NotificationActions.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from './dispatcher'; 2 | import { ADD_NOTIFICATION, REMOVE_NOTIFICATION, RESET_NOTIFICATIONS } from './constants'; 3 | 4 | /** 5 | * Actions, which are exposed to the developer, to interact with the NotificationContainer. 6 | * 7 | * @author Timo Hanisch 8 | * @since 0.1.0 9 | */ 10 | class NotificationActions { 11 | 12 | /** 13 | * Adds the given notifcation to the system so it then can be rendered 14 | * in a NotifcationContainer. 15 | * 16 | * @param {*} notification Notificaton which should be added and shown 17 | */ 18 | addNotification(notification) { 19 | Dispatcher.dispatch({ 20 | type: ADD_NOTIFICATION, 21 | data: notification, 22 | }); 23 | } 24 | 25 | /** 26 | * Removes the notifcation with the given id from the store. 27 | * 28 | * @param {Number} notificationId Id of the notification to be removed 29 | */ 30 | removeNotification(notificationId) { 31 | Dispatcher.dispatch({ 32 | type: REMOVE_NOTIFICATION, 33 | data: notificationId, 34 | }); 35 | } 36 | 37 | /** 38 | * Removes all notifications from the internal store. 39 | */ 40 | resetNotifications() { 41 | Dispatcher.dispatch({ 42 | type: RESET_NOTIFICATIONS 43 | }); 44 | } 45 | } 46 | 47 | export default new NotificationActions(); -------------------------------------------------------------------------------- /src/lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants used for the dispatcher (actions) and flux store. 3 | */ 4 | export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; 5 | export const REMOVE_NOTIFICATION = 'REMOVE_NOTIFICATION'; 6 | export const RESET_NOTIFICATIONS = 'RESET_NOTIFICATIONS'; 7 | 8 | /** 9 | * Constants used to emit change events which are handled 10 | * by components. 11 | */ 12 | export const DISPATCH_UPDATE = 'notification_update'; -------------------------------------------------------------------------------- /src/lib/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from 'flux'; 2 | 3 | /** 4 | * An implementation of the flux dispatcher used 5 | * as a singleton for the notifications, so that 6 | * we do not have to use the applications own 7 | * dispatcher. 8 | */ 9 | class NotificationDispatcher extends Dispatcher { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | dispatch(data) { 15 | super.dispatch(data); 16 | } 17 | } 18 | 19 | export default new NotificationDispatcher(); -------------------------------------------------------------------------------- /src/lib/store.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from './dispatcher'; 2 | import { EventEmitter } from 'events'; 3 | import { ADD_NOTIFICATION, REMOVE_NOTIFICATION, RESET_NOTIFICATIONS, DISPATCH_UPDATE } from './constants'; 4 | 5 | /** 6 | * Flux store internally used to manage notifications for material-ui-notifications. 7 | * 8 | * @author Timo Hanisch 9 | * @since 0.1.0 10 | */ 11 | class Store extends EventEmitter { 12 | 13 | constructor() { 14 | super(); 15 | // Ref token returned by Flux 16 | this._token = Dispatcher.register(this._handleActions); 17 | // List of all active notifications 18 | this._notifications = []; 19 | // List of all timer refs used for auto hide notifcations 20 | this._timerRefs = []; 21 | // Auto increasing id used for added notifications 22 | this._uniqueId = 0; 23 | } 24 | 25 | /** 26 | * Returns the flux token for this store. 27 | * Note: Since material-ui-notifications uses its own dispatcher 28 | * this token might not be usefull at all outside of this library. 29 | * 30 | * @public 31 | * @returns {String} the token created by flux 32 | */ 33 | get token() { 34 | return this._token; 35 | } 36 | 37 | /** 38 | * Returns the list of all active notifications. 39 | * 40 | * @public 41 | * @returns {Array<*>} 42 | */ 43 | get notifications() { 44 | return this._notifications; 45 | } 46 | 47 | /** 48 | * Generates a unique id for a notification. Unique in this store 49 | * not globally unique. 50 | * 51 | * @private 52 | */ 53 | _generateId() { 54 | return this._uniqueId++; 55 | } 56 | 57 | /** 58 | * Adds the given notification to the internal notification list and creates 59 | * a unique id for it. If the notification is an auto hide notification 60 | * a timer for it is created and added to the internal ref list. 61 | * Finally a dispatch update is emitted. 62 | * 63 | * @param {*} notification 64 | * @private 65 | */ 66 | _addNotification(notification) { 67 | const { autoHide, ...rest } = notification; 68 | const tmpNotification = { 69 | ...rest, 70 | id: this._generateId(), 71 | }; 72 | this._notifications = [ 73 | tmpNotification, 74 | ...this._notifications, 75 | ]; 76 | if (autoHide) { 77 | // Add an object with the notification id and timer ref to the the timer refs 78 | // list, so we are later properly able to remove it from the timer ref list 79 | this._timerRefs = [ 80 | ...this._timerRefs, 81 | { 82 | id: tmpNotification.id, 83 | ref: setTimeout(() => { this._removeNotification(tmpNotification.id) }, autoHide), 84 | }, 85 | ]; 86 | } 87 | this.emit(DISPATCH_UPDATE); 88 | } 89 | 90 | /** 91 | * Removes the notification with the given id from the internal notification list. 92 | * 93 | * Finally a dispatch update is emitted. 94 | * 95 | * @param {Number} notificationId 96 | * @private 97 | */ 98 | _removeNotification(notificationId) { 99 | this._notifications = this._notifications.filter(notification => notification.id !== notificationId); 100 | this._timerRefs = this._timerRefs.filter(refObj => refObj.id !== notificationId); 101 | this.emit(DISPATCH_UPDATE); 102 | } 103 | 104 | /** 105 | * Stops all timers, removes all notifications from the internal notification list 106 | * and emits an update event. 107 | * 108 | * @private 109 | */ 110 | _resetNotifications() { 111 | this._timerRefs.forEach(refObj => { 112 | clearTimeout(refObj.ref); 113 | }); 114 | this._timerRefs = []; 115 | this._notifications = []; 116 | this.emit(DISPATCH_UPDATE); 117 | } 118 | 119 | /** 120 | * Handle all incoming flux actions dispatched by our dispatcher implementation. 121 | * 122 | * @param {*} action 123 | * @private 124 | */ 125 | _handleActions = (action) => { 126 | switch (action.type) { 127 | case ADD_NOTIFICATION: { 128 | this._addNotification(action.data); 129 | break; 130 | } 131 | case REMOVE_NOTIFICATION: { 132 | this._removeNotification(action.data); 133 | break; 134 | } 135 | case RESET_NOTIFICATIONS: { 136 | this._resetNotifications(); 137 | break; 138 | } 139 | } 140 | }; 141 | } 142 | 143 | export default new Store(); -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | import { NotificationContainer, Notification, NotificationActions } from '../src/index'; 5 | import { Avatar, RaisedButton, FlatButton } from 'material-ui'; 6 | import { red500 } from 'material-ui/styles/colors'; 7 | import Close from 'material-ui/svg-icons/navigation/close'; 8 | import Email from 'material-ui/svg-icons/communication/email'; 9 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 10 | 11 | storiesOf('Notification', module) 12 | .add('Simple Notification', () => ( 13 |
14 | 15 | { }} 18 | title="Timo Hanisch" 19 | text="Yeah this seems like a pretty good idea!" 20 | /> 21 | 22 |
23 | )) 24 | .add('Extended Notification', () => ( 25 |
26 | 27 | } 29 | icon={} 30 | headerLabel="Mail" 31 | secondaryHeaderLabel="timohanisch@googlemail.com" 32 | timestamp="Now" 33 | primaryColor={red500} 34 | onClose={() => { }} 35 | title="Timo Hanisch" 36 | text="Yeah this seems like a pretty good idea!" 37 | /> 38 | 39 |
40 | )) 41 | .add('Notification with Actions', () => ( 42 |
43 | 44 | console.info("Lets reply"), 49 | }, 50 | { 51 | label: "Archive", 52 | onClick: e => console.info("Lets archive"), 53 | }, 54 | ]} 55 | avatar={} 56 | icon={} 57 | headerLabel="Mail" 58 | secondaryHeaderLabel="timohanisch@googlemail.com" 59 | timestamp="11.12.2017" 60 | primaryColor={red500} 61 | onClose={() => { }} 62 | title="Timo Hanisch" 63 | text="Yeah this seems like a pretty good idea!" 64 | /> 65 | 66 |
67 | )); 68 | 69 | storiesOf('NotificationContainer', module) 70 | .add('Simple Notification', () => ( 71 | 72 |
73 | 74 | { 77 | NotificationActions.addNotification({ 78 | headerLabel: 'Mail', 79 | title: `Timo Hanisch`, 80 | text: 'Yeah this seems like a pretty good idea!', 81 | }); 82 | }} 83 | /> 84 |
85 |
86 | )) 87 | .add('Extended Notification', () => ( 88 | 89 |
90 | 91 | { 94 | NotificationActions.addNotification({ 95 | avatar: , 96 | icon: , 97 | headerLabel: "Mail", 98 | secondaryHeaderLabel: "timohanisch@googlemail.com", 99 | timestamp: "Now", 100 | primaryColor: red500, 101 | title: "Timo Hanisch", 102 | text: "Yeah this seems like a pretty good idea!", 103 | }); 104 | }} 105 | /> 106 |
107 |
108 | )) 109 | .add('Extended Notification with auto hide', () => ( 110 | 111 |
112 | 113 | { 116 | NotificationActions.addNotification({ 117 | autoHide: 5000, 118 | avatar: , 119 | icon: , 120 | headerLabel: "Mail", 121 | secondaryHeaderLabel: "timohanisch@googlemail.com", 122 | timestamp: "Now", 123 | primaryColor: red500, 124 | title: "Timo Hanisch", 125 | text: "Yeah this seems like a pretty good idea!", 126 | }); 127 | }} 128 | /> 129 |
130 |
131 | )); -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: [ 6 | path.join(__dirname, './src/index.js') 7 | ], 8 | output: { 9 | path: path.join(__dirname, './dist'), 10 | filename: '[name].bundle.js' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.jsx?$/, 16 | include: path.join(__dirname, 'src'), 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['env', 'react', 'stage-1'], 22 | } 23 | } 24 | } 25 | ] 26 | }, 27 | resolve: { 28 | extensions: ['.js', '.jsx'], 29 | }, 30 | plugins: [ 31 | new webpack.optimize.OccurrenceOrderPlugin(), 32 | new webpack.DefinePlugin({ 33 | 'process.env': { 34 | 'NODE_ENV': JSON.stringify('development') 35 | } 36 | }) 37 | ], 38 | devServer: { 39 | contentBase: './dist' 40 | } 41 | }; -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | entry: [ 7 | path.join(__dirname, './src/index.js') 8 | ], 9 | output: { 10 | path: path.join(__dirname, './dist'), 11 | filename: '[name].bundle.js' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | include: path.join(__dirname, 'src'), 18 | exclude: /node_modules/, 19 | use: { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: ['env', 'react', 'stage-1'], 23 | } 24 | } 25 | } 26 | ] 27 | }, 28 | resolve: { 29 | extensions: ['.js', '.jsx'], 30 | }, 31 | plugins: [ 32 | new webpack.optimize.OccurrenceOrderPlugin(), 33 | new webpack.DefinePlugin({ 34 | 'process.env': { 35 | 'NODE_ENV': JSON.stringify('production') 36 | } 37 | }), 38 | new UglifyJSPlugin() 39 | ], 40 | devtool: 'source-map' 41 | }; --------------------------------------------------------------------------------