Add categories anchors

release
Etienne Lemay 2016-06-02 11:26:48 -04:00
parent d9ec7c0352
commit bb70daf89a
9 changed files with 219 additions and 21 deletions

View File

@ -21,6 +21,47 @@
border-bottom-right-radius: 5px; border-bottom-right-radius: 5px;
} }
.emoji-picker-anchors {
display: flex;
justify-content: space-between;
padding: 0 6px;
color: #858585;
}
.emoji-picker-anchor {
position: relative;
flex: 1;
text-align: center;
padding: 12px 4px;
overflow: hidden;
transition: color .2s ease-out;
}
.emoji-picker-anchor-selected {
color: #ae65c5;
}
.emoji-picker-anchor-selected .emoji-picker-anchor-bar {
bottom: 0;
}
.emoji-picker-anchor-bar {
position: absolute;
bottom: -3px; left: 0;
width: 100%; height: 3px;
background-color: #ae65c5;
transition: bottom .2s ease-out;
}
.emoji-picker-anchors i {
display: inline-block;
width: 100%;
max-width: 22px;
}
.emoji-picker-anchors svg {
fill: currentColor;
}
.emoji-picker-scroll { .emoji-picker-scroll {
overflow: scroll; overflow: scroll;
height: 270px; height: 270px;

View File

@ -54,7 +54,7 @@ class Example extends React.Component {
<br /> <br />
<br /><Operator>&lt;</Operator><Variable>Picker</Variable> <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 /> 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 /> 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='6' max='16' value={this.state.perLine} />
<br /> skin<Operator>=</Operator>&#123;<Variable>{this.state.skin}</Variable>&#125; <input type='range' data-key='skin' onChange={this.handleInput.bind(this)} min='1' max='6' value={this.state.skin} /> <br /> skin<Operator>=</Operator>&#123;<Variable>{this.state.skin}</Variable>&#125; <input type='range' data-key='skin' onChange={this.handleInput.bind(this)} min='1' max='6' value={this.state.skin} />
<br /> sheetURL<Operator>=</Operator><String>'images/sheet-{this.state.set}-64.png'</String> <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 /> onClick<Operator>=</Operator>&#123;(<Variable>emoji</Variable>) => console.log(<Variable>emoji</Variable>)&#125;

View File

@ -18,6 +18,13 @@ module.exports = {
path.resolve('example'), path.resolve('example'),
], ],
}, },
{
test: /\.svg$/,
loader: 'svg-inline',
include: [
path.resolve('src/svgs'),
],
},
], ],
}, },

View File

@ -34,6 +34,8 @@
"react": "0.14.7", "react": "0.14.7",
"react-dom": "0.14.7", "react-dom": "0.14.7",
"rimraf": "2.5.2", "rimraf": "2.5.2",
"svg-inline-loader": "0.4.1",
"svg-inline-react": "1.0.1",
"webpack": "1.12.14" "webpack": "1.12.14"
}, },
"scripts": { "scripts": {

58
src/components/anchors.js Normal file
View File

@ -0,0 +1,58 @@
import React from 'react'
import InlineSVG from 'svg-inline-react'
import Activity from '../svgs/activity.svg'
import Flags from '../svgs/flags.svg'
import Foods from '../svgs/foods.svg'
import Nature from '../svgs/nature.svg'
import Objects from '../svgs/objects.svg'
import People from '../svgs/people.svg'
import Places from '../svgs/places.svg'
import Recent from '../svgs/recent.svg'
import Symbols from '../svgs/symbols.svg'
var svgs = {
Activity,
Flags,
Foods,
Nature,
Objects,
People,
Places,
Recent,
Symbols,
}
export default class Anchors extends React.Component {
constructor(props) {
super(props)
this.state = {
selected: props.categories[0].name
}
}
render() {
var { categories } = this.props,
{ selected } = this.state
return <div className='emoji-picker-anchors'>
{categories.map((category, i) => {
var { name } = category
return <span key={name} className={`emoji-picker-anchor ${name == selected ? 'emoji-picker-anchor-selected' : ''}`}>
<InlineSVG src={svgs[name]} />
<span className='emoji-picker-anchor-bar'></span>
</span>
})}
</div>
}
}
Anchors.propTypes = {
categories: React.PropTypes.array,
}
Anchors.defaultProps = {
categories: [],
}

View File

@ -23,7 +23,11 @@ export default class Category extends React.Component {
var { height: labelHeight } = this.label.getBoundingClientRect() var { height: labelHeight } = this.label.getBoundingClientRect()
this.top = top - parentTop + this.parent.scrollTop this.top = top - parentTop + this.parent.scrollTop
if (height > labelHeight) {
this.maxMargin = height - labelHeight this.maxMargin = height - labelHeight
} else {
this.maxMargin = 1
}
} }
handleScroll(scrollTop) { handleScroll(scrollTop) {
@ -32,15 +36,22 @@ export default class Category extends React.Component {
margin = margin > this.maxMargin ? this.maxMargin : margin margin = margin > this.maxMargin ? this.maxMargin : margin
if (margin == this.margin) return if (margin == this.margin) return
var { name } = this.props
if (!this.props.hasStickyPosition) {
this.label.style.top = `${margin}px` this.label.style.top = `${margin}px`
}
this.margin = margin this.margin = margin
return true
} }
render() { render() {
var { name, emojis, hasStickyPosition, emojiProps } = this.props, var { name, emojis, hasStickyPosition, emojiProps } = this.props,
emojis = emojis.slice(0), emojis = emojis ? emojis.slice(0) : null,
labelStyles = {}, labelStyles = {},
labelSpanStyles = {} labelSpanStyles = {},
containerStyles = {}
if (!hasStickyPosition) { if (!hasStickyPosition) {
labelStyles = { labelStyles = {
@ -52,12 +63,19 @@ export default class Category extends React.Component {
} }
} }
return <div ref='container' className='emoji-picker-category'> if (!emojis) {
containerStyles = {
height: 1,
overflow: 'hidden',
}
}
return <div ref='container' className='emoji-picker-category' style={containerStyles}>
<div style={labelStyles} data-name={name} className='emoji-picker-category-label'> <div style={labelStyles} data-name={name} className='emoji-picker-category-label'>
<span style={labelSpanStyles} ref='label'>{name}</span> <span style={labelSpanStyles} ref='label'>{name}</span>
</div> </div>
{emojis.map((emoji) => {emojis && emojis.map((emoji) =>
<Emoji <Emoji
key={emoji} key={emoji}
emoji={emoji} emoji={emoji}
@ -65,7 +83,7 @@ export default class Category extends React.Component {
/> />
)} )}
{!emojis.length && {emojis && !emojis.length &&
<div className='emoji-picker-no-results'> <div className='emoji-picker-no-results'>
<Emoji <Emoji
{...emojiProps} {...emojiProps}

View File

@ -1,9 +1,16 @@
import '../utils/raf-polyfill'
import React from 'react' import React from 'react'
import data from '../../data' import data from '../../data'
import Preview from './preview' import Preview from './preview'
import Category from './category' import Category from './category'
import Search from './search' import Search from './search'
import Anchors from './anchors'
const DEFAULT_CATEGORIES = [
{ name: 'Recent', emojis: null }
].concat(data.categories)
export default class Picker extends React.Component { export default class Picker extends React.Component {
constructor(props) { constructor(props) {
@ -11,7 +18,7 @@ export default class Picker extends React.Component {
this.testStickyPosition() this.testStickyPosition()
this.state = { this.state = {
categories: data.categories, categories: DEFAULT_CATEGORIES,
} }
} }
@ -42,22 +49,46 @@ export default class Picker extends React.Component {
} }
handleScroll() { handleScroll() {
var target = this.refs.scroll, if (!this.waitingForPaint) {
scrollTop = target.scrollTop this.waitingForPaint = true
window.requestAnimationFrame(this.handleScrollPaint.bind(this))
}
}
if (!this.hasStickyPosition) { handleScrollPaint() {
Array(this.state.categories.length).fill().forEach((_, i) => { this.waitingForPaint = false
var category = this.refs[`category-${i}`]
if (category) { var target = this.refs.scroll,
category.handleScroll(scrollTop) scrollTop = target.scrollTop,
activeCategory = null
for (let i = 0, l = this.state.categories.length; i < l; i++) {
let category = this.state.categories[i],
component = this.refs[`category-${i}`]
if (component) {
let active = component.handleScroll(scrollTop)
if (active && !activeCategory) {
activeCategory = category
} }
})
} }
} }
if (activeCategory) {
let { anchors } = this.refs,
{ name: categoryName } = activeCategory
if (anchors.state.selected != categoryName) {
anchors.setState({ selected: categoryName })
}
}
this.scrollTop = scrollTop
}
handleSearch(emojis) { handleSearch(emojis) {
if (emojis == null) { if (emojis == null) {
this.setState({ categories: data.categories }) this.setState({ categories: DEFAULT_CATEGORIES })
} else { } else {
this.setState({ categories: [{ this.setState({ categories: [{
name: 'Search Results', name: 'Search Results',
@ -72,7 +103,10 @@ export default class Picker extends React.Component {
return <div style={{width: width}} className='emoji-picker'> return <div style={{width: width}} className='emoji-picker'>
<div className='emoji-picker-bar'> <div className='emoji-picker-bar'>
Categories <Anchors
ref='anchors'
categories={DEFAULT_CATEGORIES}
/>
</div> </div>
<div ref="scroll" className='emoji-picker-scroll' onScroll={this.handleScroll.bind(this)}> <div ref="scroll" className='emoji-picker-scroll' onScroll={this.handleScroll.bind(this)}>

31
src/utils/raf-polyfill.js Normal file
View File

@ -0,0 +1,31 @@
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
// MIT license
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());

View File

@ -23,6 +23,13 @@ module.exports = {
path.resolve('data'), path.resolve('data'),
], ],
}, },
{
test: /\.svg$/,
loader: 'svg-inline',
include: [
path.resolve('src/svgs'),
],
},
], ],
}, },