commit e108b00768b009540b18738349b049800f58f296 Author: Etienne Lemay Date: Tue May 31 10:36:52 2016 -0400 Initial commit diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..86c445f --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "react"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8050a77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +build/ +data/ +dist/ +lib/ diff --git a/css/emoji-picker.css b/css/emoji-picker.css new file mode 100644 index 0000000..d491201 --- /dev/null +++ b/css/emoji-picker.css @@ -0,0 +1,94 @@ +.emoji-picker, +.emoji-picker * { + box-sizing: border-box; +} + +.emoji-picker { + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; + font-size: 16px; + display: inline-block; + color: #222427; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 5px; +} + +.emoji-picker-bar { + background-color: #f4f4f4; + padding: 12px; +} +.emoji-picker-bar:first-child { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} +.emoji-picker-bar:last-child { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +} + +.emoji-picker-scroll { + overflow: scroll; + max-height: 270px; + padding: 6px; + border: solid rgba(0, 0, 0, .15); + border-width: 1px 0; +} + +.emoji-picker-search { + font-size: 16px; + display: block; + width: 100%; + padding: .2em .6em; + border-radius: 25px; + border: 1px solid rgba(0, 0, 0, .15); + outline: 0; +} + +.emoji-picker-category .emoji-picker-emoji:hover { + background-color: #b7e887; +} + +.emoji-picker-category-label { + font-weight: bold; + padding: 5px 6px; + background-color: rgba(255, 255, 255, .95); +} + +.emoji-picker-emoji { + display: inline-block; + padding: 4px 6px; + border-radius: 5px; +} + +.emoji-picker-preview { + position: relative; + height: 70px; +} + +.emoji-picker-preview-emoji, +.emoji-picker-preview-data { + position: absolute; + top: 12px; +} + +.emoji-picker-preview-emoji { + left: 12px; +} + +.emoji-picker-preview-data { + top: 50%; + left: 68px; right: 12px; + transform: translateY(-50%); + word-break: break-word; +} + +.emoji-picker-preview-name { + font-size: 14px; +} + +.emoji-picker-preview-shortname { + font-size: 12px; + color: #888; +} +.emoji-picker-preview-shortname + .emoji-picker-preview-shortname { + margin-left: .5em; +} diff --git a/example/images/sheet-apple-64.png b/example/images/sheet-apple-64.png new file mode 100644 index 0000000..e17bb57 Binary files /dev/null and b/example/images/sheet-apple-64.png differ diff --git a/example/images/sheet-emojione-64.png b/example/images/sheet-emojione-64.png new file mode 100644 index 0000000..8f53347 Binary files /dev/null and b/example/images/sheet-emojione-64.png differ diff --git a/example/images/sheet-google-64.png b/example/images/sheet-google-64.png new file mode 100644 index 0000000..7670e64 Binary files /dev/null and b/example/images/sheet-google-64.png differ diff --git a/example/images/sheet-twitter-64.png b/example/images/sheet-twitter-64.png new file mode 100644 index 0000000..c4c6e2e Binary files /dev/null and b/example/images/sheet-twitter-64.png differ diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..c16ea12 --- /dev/null +++ b/example/index.html @@ -0,0 +1,10 @@ + + + EmojiPicker + + + +
+ + + diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..697b32f --- /dev/null +++ b/example/index.js @@ -0,0 +1,96 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +import {Picker} from '../src' + +class Example extends React.Component { + constructor(props) { + super(props) + this.state = { + emojiSize: 24, + perLine: 9, + set: 'apple', + } + } + + handleInput(e) { + var { currentTarget } = e, + { value } = currentTarget, + key = currentTarget.getAttribute('data-key'), + state = {} + + state[key] = parseInt(value) + this.setState(state) + } + + render() { + return
+

EmojiPicker

+ +
+ {['Apple', 'Google', 'Twitter', 'EmojiOne'].map((set) => { + var value = set.toLowerCase(), + props = { disabled: value == this.state.set } + + return + })} +
+ +
+import {Picker} from 'emoji-picker'
+
+
<Picker +
emojiSize={{this.state.emojiSize}} +
perLine={{this.state.perLine}} {this.state.perLine < 10 ? ' ' : ' '} +
sheetURL='images/sheet-{this.state.set}-64.png' +
onClick={(emoji) => console.log(emoji)} +
/> +
+ + console.log(emoji)} + /> +
+ } +} + +class Operator extends React.Component { + render() { + return + {this.props.children} + + } +} + +class Variable extends React.Component { + render() { + return + {this.props.children} + + } +} + +class String extends React.Component { + render() { + return + {this.props.children} + + } +} + +ReactDOM.render(, document.querySelector('div')) diff --git a/example/webpack.config.js b/example/webpack.config.js new file mode 100644 index 0000000..9ca94a4 --- /dev/null +++ b/example/webpack.config.js @@ -0,0 +1,27 @@ +var path = require('path') + +module.exports = { + entry: path.resolve('example/index.js'), + output: { + path: path.resolve('build'), + filename: 'example.js', + }, + + module: { + loaders: [ + { + test: /\.js$/, + loader: 'babel-loader', + include: [ + path.resolve('src'), + path.resolve('data'), + path.resolve('example'), + ], + }, + ], + }, + + resolve: { + extensions: ['', '.js'], + }, +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3097cc2 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "emoji-picker", + "version": "0.0.1", + "description": "", + "main": "dist/emoji-picker.js", + "repository": { + "type": "git", + "url": "git@github.com:missive/emoji-picker.git" + }, + "keywords": [ + "react", + "emoji", + "picker" + ], + "author": "Etienne Lemay", + "license": "ISC", + "bugs": { + "url": "https://github.com/missive/emoji-picker/issues" + }, + "homepage": "https://github.com/missive/emoji-picker", + "dependencies": {}, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0" + }, + "devDependencies": { + "babel-core": "6.7.2", + "babel-loader": "6.2.4", + "babel-preset-es2015": "6.6.0", + "babel-preset-react": "6.5.0", + "emoji-data": "git://github.com/missive/emoji-data.git#01aeccb3adf6cdfaab6e13892f7688740047ff32", + "inflection": "1.10.0", + "mkdirp": "0.5.1", + "react": "0.14.7", + "react-dom": "0.14.7", + "rimraf": "2.5.2", + "webpack": "1.12.14" + }, + "scripts": { + "clean": "rimraf build/ data/ dist/", + "build:data": "node scripts/build-data", + "build:example": "node scripts/build-example", + "build:dist": "node scripts/build-dist", + "build": "npm run build:data && npm run build:dist", + "react:clean": "rimraf node_modules/{react,react-dom,react-addons-test-utils}", + "react:14": "npm run react:clean && npm i react@^0.14 react-dom@^0.14 react-addons-test-utils@^0.14", + "react:15": "npm run react:clean && npm i react@^15 react-dom@^15 react-addons-test-utils@^15", + "test": "echo \"Error: no test specified\" && exit 1", + "prepublish": "npm run clean && npm run build" + } +} diff --git a/scripts/build-data.js b/scripts/build-data.js new file mode 100644 index 0000000..63b676c --- /dev/null +++ b/scripts/build-data.js @@ -0,0 +1,55 @@ +var fs = require('fs') +var emojiData = require('emoji-data') +var inflection = require('inflection') +var mkdirp = require('mkdirp') + +var categories = ['People', 'Nature', 'Foods', 'Activity', 'Places', 'Objects', 'Symbols', 'Flags', 'Skins'] +var data = { categories: [], emojis: {} } +var categoriesIndex = {} + +categories.forEach((category, i) => { + data.categories[i] = { name: category, emojis: [] } + categoriesIndex[category] = i +}) + +emojiData.sort((a, b) => { + var aTest = a.sort_order || a.short_name + var bTest = b.sort_order || b.short_name + + return aTest - bTest +}) + +emojiData.forEach((datum) => { + var category = datum.category, + shortName = datum.short_name, + categoryIndex + + if (!category) { + if (/^skin/.test(shortName)) category = 'Skins' + if (/^flag/.test(shortName)) category = 'Flags' + if (/^(left_speech_bubble|keycap_star|eject)$/.test(shortName)) category = 'Symbols' + } + + if (!category) { + throw new Error('“' + datum.short_name + '” doesn’t have a category') + } + + datum.name || (datum.name = datum.short_name.replace(/\-/g, ' ')) + datum.name = inflection.titleize(datum.name || '') + + if (!datum.name) { + throw new Error('“' + datum.short_name + '” doesn’t have a name') + } + + categoryIndex = categoriesIndex[category] + data.categories[categoryIndex].emojis.push(datum.short_name) + data.emojis[datum.short_name] = datum +}) + +mkdirp('data', (err) => { + if (err) throw err + + fs.writeFile('data/index.js', `export default ${JSON.stringify(data)}`, (err) => { + if (err) throw err + }) +}) diff --git a/scripts/build-dist.js b/scripts/build-dist.js new file mode 100644 index 0000000..79a1c84 --- /dev/null +++ b/scripts/build-dist.js @@ -0,0 +1,44 @@ +var path = require('path') +var extend = require('util')._extend +var webpack = require('webpack') + +var configs = ['development', 'production'].map((env) => { + var configPath = '../src/webpack.config.js', + config = require(configPath) + + delete require.cache[require.resolve(configPath)] + + if (env === 'production') { + config.output.filename = config.output.filename.replace(/\.js$/, '.min.js') + config.plugins.push( + new webpack.optimize.UglifyJsPlugin({ + compressor: { warnings: false } + }) + ) + } + + return config +}) + +webpack(configs, (err, stats) => { + if (err) throw err + + console.log( + stats.toString({ + colors: true, + hash: true, + version: false, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + cached: false, + reasons: false, + source: false, + errorDetails: false, + chunkOrigins: false, + }) + ) +}) diff --git a/scripts/build-example.js b/scripts/build-example.js new file mode 100644 index 0000000..0b07f58 --- /dev/null +++ b/scripts/build-example.js @@ -0,0 +1,28 @@ +var path = require('path') +var webpack = require('webpack') + +var config = require('../example/webpack.config.js'), + compiler = webpack(config) + +compiler.watch({}, (err, stats) => { + if (err) throw err + + console.log( + stats.toString({ + colors: true, + hash: true, + version: false, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + cached: false, + reasons: false, + source: false, + errorDetails: false, + chunkOrigins: false, + }) + ) +}) diff --git a/src/components/category.js b/src/components/category.js new file mode 100644 index 0000000..b435eae --- /dev/null +++ b/src/components/category.js @@ -0,0 +1,55 @@ +import React from 'react' +import Emoji from './emoji' + +export default class Category extends React.Component { + shouldComponentUpdate(props) { + if (props.perLine != this.props.perLine) { + return true + } + + for (let k in props.emojiProps) { + if (props.emojiProps[k] != this.props.emojiProps[k]) { + return true + } + } + + return false + } + + render() { + var { name, emojis, perLine, emojiProps } = this.props, + emojis = emojis.slice(0), + lines = [], + linesCount = Math.ceil(emojis.length / perLine) + + Array(linesCount).fill().forEach((_, i) => + lines.push(emojis.splice(0, perLine)) + ) + + return
+
{name}
+ {lines.map((emojis, i) => +
+ {emojis.map((emoji) => + + )} +
+ )} +
+ } +} + +Category.propTypes = { + emojis: React.PropTypes.array, + name: React.PropTypes.string.isRequired, + perLine: React.PropTypes.number.isRequired, + emojiProps: React.PropTypes.object.isRequired, +} + +Category.defaultProps = { + emojis: [], +} diff --git a/src/components/emoji.js b/src/components/emoji.js new file mode 100644 index 0000000..22ee40b --- /dev/null +++ b/src/components/emoji.js @@ -0,0 +1,67 @@ +import React from 'react' +import data from '../../data' + +const SHEET_SIZE = 2624 +const EMOJI_SIZE = 64 + +export default class Emoji extends React.Component { + componentWillMount() { + this.emoji = data.emojis[this.props.emoji] + } + + shouldComponentUpdate(props) { + if (props.size != this.props.size || props.sheetURL != this.props.sheetURL) { + return true + } + + return false + } + + getSheetSize() { + var { size } = this.props, + sheetSize = SHEET_SIZE * size / EMOJI_SIZE + + return `${sheetSize}px ${sheetSize}px` + } + + getEmojiPosition(emoji) { + var { size } = this.props, + { sheet_x, sheet_y } = emoji, + x = sheet_x * size, + y = sheet_y * size + + return `-${x}px -${y}px` + } + + render() { + var { sheetURL, size, onOver, onClick } = this.props + + return onClick(this.emoji)} + onMouseEnter={() => onOver(this.emoji)} + className='emoji-picker-emoji'> + + + + } +} + +Emoji.propTypes = { + onOver: React.PropTypes.func, + onClick: React.PropTypes.func, + size: React.PropTypes.number.isRequired, + emoji: React.PropTypes.string.isRequired, + sheetURL: React.PropTypes.string.isRequired, +} + +Emoji.defaultProps = { + onOver: (() => {}), + onClick: (() => {}), +} diff --git a/src/components/picker.js b/src/components/picker.js new file mode 100644 index 0000000..2472375 --- /dev/null +++ b/src/components/picker.js @@ -0,0 +1,69 @@ +import React from 'react' + +import data from '../../data' +import Preview from './preview' +import Category from './category' + +export default class Picker extends React.Component { + handleEmojiOver(emoji) { + var { preview } = this.refs + preview.setState({ emoji: emoji }) + } + + render() { + var { perLine, emojiSize, sheetURL } = this.props + + return
+
+ Categories +
+ +
+ + + {data.categories.map((category) => { + if (category.name == 'Skins') return null + return + })} +
+ +
+ +
+
+ } +} + +Picker.propTypes = { + onClick: React.PropTypes.func, + perLine: React.PropTypes.number, + emojiSize: React.PropTypes.number, + sheetURL: React.PropTypes.string.isRequired, +} + +Picker.defaultProps = { + onClick: (() => {}), + emojiSize: 32, + perLine: 9, +} diff --git a/src/components/preview.js b/src/components/preview.js new file mode 100644 index 0000000..a1002b6 --- /dev/null +++ b/src/components/preview.js @@ -0,0 +1,43 @@ +import React from 'react' +import Emoji from './emoji' + +export default class Preview extends React.Component { + constructor(props) { + super(props) + this.state = { emoji: null } + } + + render() { + var { emoji } = this.state, + { emojiProps } = this.props + + if (emoji) { + return
+
+ +
+ +
+ {emoji.name}
+ + {emoji.short_names.map((short_name) => + :{short_name}: + )} + +
+
+ } else { + return
+ EmojiPicker +
+ } + } +} + +Preview.propTypes = { + emojiProps: React.PropTypes.object.isRequired, +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..7ca4e62 --- /dev/null +++ b/src/index.js @@ -0,0 +1,4 @@ +export {default as Category} from './components/category' +export {default as Emoji} from './components/emoji' +export {default as Picker} from './components/picker' +export {default as Preview} from './components/preview' diff --git a/src/webpack.config.js b/src/webpack.config.js new file mode 100644 index 0000000..b28c725 --- /dev/null +++ b/src/webpack.config.js @@ -0,0 +1,36 @@ +var path = require('path') + +module.exports = { + entry: path.resolve('src/index.js'), + output: { + path: path.resolve('dist'), + filename: 'emoji-picker.js', + library: 'EmojiPicker', + libraryTarget: 'umd', + }, + + externals: [ + 'react', + ], + + module: { + loaders: [ + { + test: /\.js$/, + loader: 'babel-loader', + include: [ + path.resolve('src'), + path.resolve('data'), + ], + }, + ], + }, + + resolve: { + extensions: ['', '.js'], + }, + + plugins: [], + devtool: 'source-map', + bail: true, +}