Initial commit

exclude-unsupported-native-emojis
Etienne Lemay 2016-05-31 10:36:52 -04:00
commit e108b00768
20 changed files with 686 additions and 0 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["es2015", "react"]
}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
build/
data/
dist/
lib/

94
css/emoji-picker.css Normal file
View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

10
example/index.html Normal file
View File

@ -0,0 +1,10 @@
<html>
<head>
<title>EmojiPicker</title>
<link rel="stylesheet" href="../css/emoji-picker.css">
</head>
<body>
<div></div>
<script src="../build/example.js"></script>
</body>
</html>

96
example/index.js Normal file
View File

@ -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 <div>
<h1>EmojiPicker</h1>
<div>
{['Apple', 'Google', 'Twitter', 'EmojiOne'].map((set) => {
var value = set.toLowerCase(),
props = { disabled: value == this.state.set }
return <button
key={value}
value={value}
onClick={() => this.setState({ set: value })}
{...props}>
{set}
</button>
})}
</div>
<pre style={{
fontSize: 18,
display: 'inline-block',
verticalAlign: 'top',
margin: '1em',
width: '460px',
}}>
<Operator>import</Operator> &#123;Picker&#125; <Operator>from</Operator> <String>'emoji-picker'</String>
<br />
<br /><Operator>&lt;</Operator><Variable>Picker</Variable>
<br /> emojiSize<Operator>=</Operator>&#123;<Variable>{this.state.emojiSize}</Variable>&#125; <input type='range' data-key='emojiSize' onChange={this.handleInput.bind(this)} min='16' max='64' value={this.state.emojiSize} />
<br /> perLine<Operator>=</Operator>&#123;<Variable>{this.state.perLine}</Variable>&#125; {this.state.perLine < 10 ? ' ' : ' '} <input type='range' data-key='perLine' onChange={this.handleInput.bind(this)} min='5' max='16' value={this.state.perLine} />
<br /> sheetURL<Operator>=</Operator><String>'images/sheet-{this.state.set}-64.png'</String>
<br /> onClick<Operator>=</Operator>&#123;(<Variable>emoji</Variable>) => console.log(<Variable>emoji</Variable>)&#125;
<br /><Operator>/&gt;</Operator>
</pre>
<Picker
emojiSize={this.state.emojiSize}
perLine={this.state.perLine}
sheetURL={`images/sheet-${this.state.set}-64.png`}
onClick={(emoji) => console.log(emoji)}
/>
</div>
}
}
class Operator extends React.Component {
render() {
return <span style={{color: '#a71d5d'}}>
{this.props.children}
</span>
}
}
class Variable extends React.Component {
render() {
return <span style={{color: '#0086b3'}}>
{this.props.children}
</span>
}
}
class String extends React.Component {
render() {
return <span style={{color: '#183691'}}>
{this.props.children}
</span>
}
}
ReactDOM.render(<Example />, document.querySelector('div'))

27
example/webpack.config.js Normal file
View File

@ -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'],
},
}

50
package.json Normal file
View File

@ -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"
}
}

55
scripts/build-data.js Normal file
View File

@ -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 + '” doesnt 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 + '” doesnt 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
})
})

44
scripts/build-dist.js Normal file
View File

@ -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,
})
)
})

28
scripts/build-example.js Normal file
View File

@ -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,
})
)
})

View File

@ -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 <div className='emoji-picker-category'>
<div className='emoji-picker-category-label'>{name}</div>
{lines.map((emojis, i) =>
<div key={`line-${i}`}>
{emojis.map((emoji) =>
<Emoji
key={emoji}
emoji={emoji}
{...emojiProps}
/>
)}
</div>
)}
</div>
}
}
Category.propTypes = {
emojis: React.PropTypes.array,
name: React.PropTypes.string.isRequired,
perLine: React.PropTypes.number.isRequired,
emojiProps: React.PropTypes.object.isRequired,
}
Category.defaultProps = {
emojis: [],
}

67
src/components/emoji.js Normal file
View File

@ -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 <span
onClick={() => onClick(this.emoji)}
onMouseEnter={() => onOver(this.emoji)}
className='emoji-picker-emoji'>
<span style={{
width: size,
height: size,
display: 'inline-block',
backgroundImage: `url(${sheetURL})`,
backgroundSize: this.getSheetSize(),
backgroundPosition: this.getEmojiPosition(this.emoji),
}}>
</span>
</span>
}
}
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: (() => {}),
}

69
src/components/picker.js Normal file
View File

@ -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 <div className='emoji-picker'>
<div className='emoji-picker-bar'>
Categories
</div>
<div ref="scroll" className='emoji-picker-scroll'>
<input
type='text'
placeholder='Search'
className='emoji-picker-search'
/>
{data.categories.map((category) => {
if (category.name == 'Skins') return null
return <Category
key={category.name}
name={category.name}
emojis={category.emojis}
perLine={perLine}
emojiProps={{
size: emojiSize,
sheetURL: sheetURL,
onOver: this.handleEmojiOver.bind(this),
onClick: this.props.onClick,
}}
/>
})}
</div>
<div className='emoji-picker-bar emoji-picker-preview'>
<Preview
ref='preview'
emojiProps={{
size: 38,
sheetURL: sheetURL,
}}
/>
</div>
</div>
}
}
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,
}

43
src/components/preview.js Normal file
View File

@ -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 <div>
<div className='emoji-picker-preview-emoji'>
<Emoji
key={emoji.short_name}
emoji={emoji.short_name}
{...emojiProps}
/>
</div>
<div className='emoji-picker-preview-data'>
<span className='emoji-picker-preview-name'>{emoji.name}</span><br />
<span className='emoji-picker-preview-shortnames'>
{emoji.short_names.map((short_name) =>
<span key={short_name} className='emoji-picker-preview-shortname'>:{short_name}:</span>
)}
</span>
</div>
</div>
} else {
return <div>
EmojiPicker
</div>
}
}
}
Preview.propTypes = {
emojiProps: React.PropTypes.object.isRequired,
}

4
src/index.js Normal file
View File

@ -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'

36
src/webpack.config.js Normal file
View File

@ -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,
}