├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── demo ├── index.html └── index.js ├── package.json ├── src └── index.js └── webpack.demo.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-2"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | demo/demo.js 5 | demo/demo.js.map 6 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | demo/demo.js 4 | demo/demo.js.map 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Emoji React 2 | 3 | A clone (eventually) of slack emoji reactions as a react component 4 | 5 | Click here for a demo. 6 | 7 | 8 | 9 | ### Install 10 | 11 | `npm install react-emoji-react --save` 12 | 13 | ### Use 14 | 15 | ```js 16 | import EmojiReact from 'react-emoji-react'; 17 | import React, { Component } from 'react'; 18 | import { render } from 'react-dom'; 19 | 20 | const emojis = [ 21 | { 22 | name: 'rage', 23 | count: 2 24 | }, 25 | { 26 | name: 'blush', 27 | count: 1 28 | }, 29 | { 30 | name: 100, 31 | count: 3 32 | }, 33 | { 34 | name: 'grinning', 35 | count: 2 36 | } 37 | ]; 38 | 39 | class ReactingComponent extends Component { 40 | constructor() { 41 | super(); 42 | this.state = { 43 | emojis 44 | }; 45 | } 46 | 47 | onReaction(name) { 48 | const emojis = this.state.emojis.map(emoji => { 49 | if (emoji.name === name) { 50 | emoji.count += 1; 51 | } 52 | return emoji; 53 | }); 54 | this.setState({ emojis }); 55 | } 56 | 57 | onEmojiClick(name) { 58 | console.log(name); 59 | const emojis = this.state.emojis.concat([{name, count: 1}]); 60 | this.setState({ emojis }); 61 | } 62 | 63 | render() { 64 | return ( 65 | this.onReaction(name)} 68 | onEmojiClick={(name) => this.onEmojiClick(name)} 69 | /> 70 | ); 71 | } 72 | } 73 | 74 | 75 | render(, document.getElementById('app')); 76 | ``` 77 | 78 | 79 | ### Args 80 | 81 | * `reactions` - an array of current emoji reactions, reactions are objects containing name and count. 82 | * `onReaction` - fired when a current reaction is clicked. 83 | * `onEmojiClick` - fired when a new emoji is selected. 84 | 85 | 86 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import EmojiReact from '../dist'; 2 | import React, { Component } from 'react'; 3 | import { render } from 'react-dom'; 4 | 5 | const emojis = [ 6 | { 7 | name: 'rage', 8 | count: 2 9 | }, 10 | { 11 | name: 'blush', 12 | count: 1 13 | }, 14 | { 15 | name: 100, 16 | count: 3 17 | }, 18 | { 19 | name: 'grinning', 20 | count: 2 21 | } 22 | ]; 23 | 24 | class Testing extends Component { 25 | constructor() { 26 | super(); 27 | this.state = { 28 | emojis 29 | }; 30 | } 31 | 32 | onReaction(name) { 33 | const emojis = this.state.emojis.map(emoji => { 34 | if (emoji.name === name) { 35 | emoji.count += 1; 36 | } 37 | return emoji; 38 | }); 39 | this.setState({ emojis }); 40 | } 41 | 42 | onEmojiClick(name) { 43 | console.log(name); 44 | const emojis = this.state.emojis.concat([{name, count: 1}]); 45 | this.setState({ emojis }); 46 | } 47 | 48 | render() { 49 | return ( 50 | this.onReaction(name)} 53 | onEmojiClick={(name) => this.onEmojiClick(name)} 54 | /> 55 | ); 56 | } 57 | } 58 | 59 | 60 | render(, document.getElementById('app')); 61 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-emoji-react", 3 | "version": "0.3.0", 4 | "description": "a clone of slack emoji reactions in react", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "demo": "./node_modules/.bin/webpack --config webpack.demo.config.js --watch", 8 | "build": "babel src --out-dir dist", 9 | "watch": "babel src --watch --out-dir dist", 10 | "prepublish": "npm run build" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "slack", 15 | "emoji", 16 | "reactions", 17 | "like", 18 | "button" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/conorhastings/react-emoji-react.git" 23 | }, 24 | "author": "Conor Hastings", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/conorhastings/react-emoji-react/issues" 28 | }, 29 | "devDependencies": { 30 | "babel-cli": "^6.4.5", 31 | "babel-core": "^6.4.5", 32 | "babel-loader": "^6.2.2", 33 | "babel-preset-es2015": "^6.3.13", 34 | "babel-preset-react": "^6.3.13", 35 | "babel-preset-stage-2": "^6.3.13", 36 | "file-loader": "^0.8.5", 37 | "json-loader": "^0.5.4", 38 | "react": "^0.14.7", 39 | "react-dom": "^0.14.7", 40 | "url-loader": "^0.5.7", 41 | "webpack": "^1.12.13" 42 | }, 43 | "peerDependencies": { 44 | "react": ">= 0.14.0", 45 | "react-dom": ">= 0.14.0" 46 | }, 47 | "dependencies": { 48 | "get-emoji": "^2.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { emojiList as emoji } from 'get-emoji'; 3 | import getEmoji from 'get-emoji'; 4 | 5 | 6 | const wrapperStyle = { 7 | display: 'inline-block', 8 | marginTop: '2px', 9 | marginBottom: '2px', 10 | marginRight: '4px', 11 | padding: '1px 3px', 12 | borderRadius: '5px', 13 | backgroundColor: '#fff', 14 | border: '1px solid #E8E8E8', 15 | cursor: 'pointer', 16 | height: '1.4rem', 17 | lineHeight: '23px', 18 | WebkitUserSelect: 'none', 19 | msUserSelect: 'none', 20 | MozUserSelect: 'none' 21 | }; 22 | 23 | const emojiStyle = { 24 | lineHeight: '20px', 25 | verticalAlign: 'middle', 26 | display: 'inline-block' 27 | }; 28 | 29 | const wrapperHover = { 30 | border: '1px solid #4fb0fc' 31 | }; 32 | 33 | const countStyle = { 34 | fontSize: '11px', 35 | fontFamily: 'helvetica, arial', 36 | position: 'relative', 37 | top: '-2px', 38 | padding: '0 1px 3px', 39 | color: '#959595' 40 | }; 41 | 42 | const countHover = { 43 | color: "#4fb0fc" 44 | }; 45 | 46 | const selectorStyle = { 47 | boxShadow: '0 6px 8px 0 rgba(0, 0, 0, 0.24)', 48 | backgroundColor: '#fff', 49 | width: '250px', 50 | height: '220px', 51 | position: 'relative', 52 | left: '10px', 53 | top: '0px' 54 | }; 55 | 56 | const EmojiImage = ({name}) => ; 57 | 58 | class SingleEmoji extends Component { 59 | constructor() { 60 | super(); 61 | this.state = { hovered: false }; 62 | } 63 | 64 | render() { 65 | const { 66 | name, 67 | count = 1, 68 | styles = { 69 | wrapperStyle: wrapperStyle, 70 | emojiStyle: emojiStyle, 71 | countStyle: countStyle, 72 | wrapperHover: wrapperHover, 73 | countHover: countHover 74 | }, 75 | onClick = () => {} 76 | } = this.props; 77 | 78 | const wrapperFinalStyle = this.state.hovered ? {...wrapperStyle, ...wrapperHover} : wrapperStyle; 79 | const countFinalStyle = this.state.hovered ? {...countStyle, ...countHover} : countStyle; 80 | return ( 81 |
onClick(name)} 84 | onMouseEnter={() => this.setState({hovered: true})} 85 | onMouseLeave={() => this.setState({hovered: false})} 86 | > 87 | 88 | {count} 89 |
90 | ); 91 | } 92 | } 93 | 94 | const PickerEmoji = ({onClick, image}) => ( 95 | onClick()}> 96 | {image} 97 | 98 | ); 99 | 100 | const EmojiWrapper = ({reactions, onReaction}) => { 101 | return ( 102 |
103 | {reactions.map(({name, count}) => ( 104 | 105 | ))} 106 |
107 | ); 108 | } 109 | 110 | const SINGLE_EMOJI_HEIGHT = 23; 111 | const LOAD_HEIGHT = 500; 112 | const EMOJIS_ACROSS = 8 113 | 114 | class EmojiSelector extends Component { 115 | constructor() { 116 | super(); 117 | this.state = { 118 | filter: "", 119 | xHovered: false, 120 | scrollPosition: 0 121 | }; 122 | this.onScroll = this.onScroll.bind(this); 123 | } 124 | 125 | onScroll() { 126 | this.setState({ scrollPosition: this.emojiContainer.scrollTop }) 127 | } 128 | 129 | componentDidMount() { 130 | this.emojiContainer.addEventListener('scroll', this.onScroll); 131 | } 132 | 133 | componentWillUnMount() { 134 | this.emojiContainer.removeEventListener('scroll', this.onScroll); 135 | } 136 | 137 | render() { 138 | const { showing, onEmojiClick, close } = this.props; 139 | let xStyle = { 140 | color: '#E8E8E8', 141 | fontSize: '20px', 142 | cursor: 'pointer', 143 | float: 'right', 144 | marginTop: '-32px', 145 | marginRight: '5px' 146 | }; 147 | if (this.state.xHovered) { 148 | xStyle.color = '#4fb0fc'; 149 | } 150 | const searchInput = ( 151 |
152 | this.setState({filter: e.target.value})} 158 | /> 159 |
160 | ); 161 | const x = ( 162 | { 165 | this.setState({ xHovered: false}); 166 | close(); 167 | }} 168 | onMouseEnter={() => this.setState({ xHovered: true})} 169 | onMouseLeave={() => this.setState({ xHovered: false})} 170 | > 171 | x 172 | 173 | ); 174 | const show = emoji.filter(name => name.indexOf(this.state.filter) !== -1); 175 | const emptyStyle = { 176 | height: '16px', 177 | width: '16px', 178 | display: 'inline-block' 179 | }; 180 | const emojis = show.map((em, i) => { 181 | const row = Math.floor((i + 1) / EMOJIS_ACROSS); 182 | const pixelPosition = row * SINGLE_EMOJI_HEIGHT; 183 | const position = this.state.scrollPosition + LOAD_HEIGHT; 184 | const shouldShowImage = pixelPosition < position && (position - pixelPosition) <= LOAD_HEIGHT; 185 | const image = shouldShowImage ? :
; 186 | return ( 187 | { 191 | onEmojiClick(em); 192 | close(); 193 | }} 194 | /> 195 | ); 196 | }); 197 | return ( 198 |
199 | {searchInput} 200 | {x} 201 |
this.emojiContainer = node} 204 | > 205 | {emojis} 206 |
207 |
208 | ); 209 | } 210 | } 211 | 212 | export default class EmojiReact extends Component { 213 | constructor() { 214 | super(); 215 | this.state = { hovered: false, showSelector: false }; 216 | this.onKeyPress = this.onKeyPress.bind(this); 217 | this.closeSelector = this.closeSelector.bind(this); 218 | this.onClick = this.onClick.bind(this); 219 | } 220 | 221 | onKeyPress(e) { 222 | if (e.keyCode === 27) { 223 | this.closeSelector(); 224 | } 225 | } 226 | 227 | onClick({ target }) { 228 | if (!this.node.contains(target) && this.state.showSelector) { 229 | this.closeSelector(); 230 | } 231 | } 232 | 233 | componentDidMount() { 234 | document.addEventListener('click', this.onClick); 235 | document.addEventListener('keydown', this.onKeyPress); 236 | } 237 | 238 | componentWillUnMount() { 239 | document.removeEventListener('click', this.onClick); 240 | document.removeEventListener('keydown', this.onKeyPress); 241 | } 242 | 243 | closeSelector() { 244 | this.setState({ showSelector: false }); 245 | } 246 | 247 | render() { 248 | const { reactions, onReaction, onEmojiClick } = this.props; 249 | const plusButtonStyle = this.state.hovered ? {...wrapperStyle, ...wrapperHover} : wrapperStyle; 250 | const plusStyle = this.state.hovered ? {...countStyle, ...countHover} : countStyle; 251 | const selector = ( 252 |
this.node = node}> 253 |
this.setState({ hovered: true })} 256 | onMouseLeave={() => this.setState({ hovered: false}) } 257 | onClick={() => this.setState({ showSelector: !this.state.showSelector})} 258 | > 259 | + 260 |
261 | 266 |
267 | ); 268 | return ( 269 |
270 | 271 | {selector} 272 |
273 | ); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /webpack.demo.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | context: path.join(__dirname), 6 | devtool: 'source-map', 7 | entry: { 8 | demo: "./demo/index.js" 9 | }, 10 | output: { 11 | path: path.join(__dirname) + '/demo', 12 | filename: "[name].js" 13 | }, 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.js?$/, 18 | loader: 'babel', 19 | query: { 20 | presets: ['react', 'es2015'], 21 | plugins: ['transform-object-rest-spread'] 22 | }, 23 | include: path.join(__dirname) + '/demo' 24 | }, 25 | { test: /\.json$/, loader: require.resolve("json-loader") } 26 | ] 27 | }, 28 | }; --------------------------------------------------------------------------------