emoji-mart-lazyload/src/components/picker/nimble-picker.js

678 lines
17 KiB
JavaScript
Raw Normal View History

2016-05-31 14:36:52 +00:00
import React from 'react'
import PropTypes from 'prop-types'
2016-06-02 17:21:31 +00:00
import * as icons from '../../svgs'
2018-04-26 15:47:30 +00:00
import store from '../../utils/store'
import frequently from '../../utils/frequently'
import { deepMerge, measureScrollbar, getSanitizedData } from '../../utils'
2018-04-30 00:55:13 +00:00
import { uncompress } from '../../utils/data'
import { PickerPropTypes } from '../../utils/shared-props'
2016-07-20 18:45:28 +00:00
2018-11-13 10:19:16 +00:00
import Anchors from '../anchors'
import Category from '../category'
import Preview from '../preview'
import Search from '../search'
import { PickerDefaultProps } from '../../utils/shared-default-props'
2016-06-02 15:26:48 +00:00
2016-10-27 03:22:59 +00:00
const I18N = {
search: 'Search',
clear: 'Clear', // Accessible label on "clear" button
2017-04-18 15:20:17 +00:00
notfound: 'No Emoji Found',
2018-08-10 18:02:33 +00:00
skintext: 'Choose your default skin tone',
2016-10-27 03:22:59 +00:00
categories: {
search: 'Search Results',
recent: 'Frequently Used',
people: 'Smileys & People',
nature: 'Animals & Nature',
foods: 'Food & Drink',
activity: 'Activity',
places: 'Travel & Places',
objects: 'Objects',
symbols: 'Symbols',
flags: 'Flags',
custom: 'Custom',
2016-10-27 03:22:59 +00:00
},
2019-03-10 17:57:43 +00:00
categorieslabel: 'Emoji categories', // Accessible title for the list of categories
skintones: {
1: 'Default Skin Tone',
2: 'Light Skin Tone',
3: 'Medium-Light Skin Tone',
4: 'Medium Skin Tone',
5: 'Medium-Dark Skin Tone',
6: 'Dark Skin Tone',
},
2016-10-27 03:22:59 +00:00
}
export default class NimblePicker extends React.PureComponent {
2016-05-31 18:39:30 +00:00
constructor(props) {
super(props)
this.CUSTOM = []
this.RECENT_CATEGORY = { id: 'recent', name: 'Recent', emojis: null }
this.SEARCH_CATEGORY = {
id: 'search',
name: 'Search',
emojis: null,
anchor: false,
}
2018-04-30 00:55:13 +00:00
if (props.data.compressed) {
uncompress(props.data)
}
this.data = props.data
2016-10-27 03:22:59 +00:00
this.i18n = deepMerge(I18N, props.i18n)
this.icons = deepMerge(icons, props.icons)
2016-05-31 18:39:30 +00:00
this.state = {
skin: props.skin || store.get('skin') || props.defaultSkin,
firstRender: true,
2016-05-31 18:39:30 +00:00
}
this.categories = []
let allCategories = [].concat(this.data.categories)
if (props.custom.length > 0) {
const customCategories = {}
let customCategoriesCreated = 0
props.custom.forEach((emoji) => {
if (!customCategories[emoji.customCategory]) {
customCategories[emoji.customCategory] = {
id: emoji.customCategory
? `custom-${emoji.customCategory}`
: 'custom',
name: emoji.customCategory || 'Custom',
emojis: [],
anchor: customCategoriesCreated === 0,
}
customCategoriesCreated++
}
const category = customCategories[emoji.customCategory]
const customEmoji = {
...emoji,
// `<Category />` expects emoji to have an `id`.
id: emoji.short_names[0],
custom: true,
}
category.emojis.push(customEmoji)
this.CUSTOM.push(customEmoji)
})
2019-12-24 02:03:16 +00:00
allCategories = allCategories.concat(
Object.keys(customCategories).map((key) => customCategories[key]),
)
}
2017-05-29 11:36:49 +00:00
this.hideRecent = true
if (props.include != undefined) {
2017-09-29 23:45:52 +00:00
allCategories.sort((a, b) => {
if (props.include.indexOf(a.id) > props.include.indexOf(b.id)) {
return 1
}
return -1
})
}
2017-10-07 04:02:02 +00:00
for (
let categoryIndex = 0;
categoryIndex < allCategories.length;
categoryIndex++
) {
2017-09-17 08:54:22 +00:00
const category = allCategories[categoryIndex]
2017-10-07 04:02:02 +00:00
let isIncluded =
props.include && props.include.length
? props.include.indexOf(category.id) > -1
2017-10-07 04:02:02 +00:00
: true
let isExcluded =
props.exclude && props.exclude.length
? props.exclude.indexOf(category.id) > -1
2017-10-07 04:02:02 +00:00
: false
if (!isIncluded || isExcluded) {
continue
}
2017-04-04 16:00:32 +00:00
if (props.emojisToShowFilter) {
let newEmojis = []
2017-10-07 04:02:02 +00:00
const { emojis } = category
2017-09-17 08:54:22 +00:00
for (let emojiIndex = 0; emojiIndex < emojis.length; emojiIndex++) {
const emoji = emojis[emojiIndex]
if (props.emojisToShowFilter(this.data.emojis[emoji] || emoji)) {
newEmojis.push(emoji)
}
}
if (newEmojis.length) {
let newCategory = {
emojis: newEmojis,
name: category.name,
2018-01-09 20:20:18 +00:00
id: category.id,
}
2017-04-04 16:00:32 +00:00
this.categories.push(newCategory)
}
} else {
this.categories.push(category)
}
}
2017-04-04 16:00:32 +00:00
2017-10-07 04:02:02 +00:00
let includeRecent =
props.include && props.include.length
? props.include.indexOf(this.RECENT_CATEGORY.id) > -1
2017-10-07 04:02:02 +00:00
: true
let excludeRecent =
props.exclude && props.exclude.length
? props.exclude.indexOf(this.RECENT_CATEGORY.id) > -1
2017-10-07 04:02:02 +00:00
: false
2017-05-29 11:36:49 +00:00
if (includeRecent && !excludeRecent) {
this.hideRecent = false
this.categories.unshift(this.RECENT_CATEGORY)
}
if (this.categories[0]) {
this.categories[0].first = true
}
this.categories.unshift(this.SEARCH_CATEGORY)
2017-10-07 04:02:02 +00:00
this.setAnchorsRef = this.setAnchorsRef.bind(this)
this.handleAnchorClick = this.handleAnchorClick.bind(this)
2017-10-07 04:02:02 +00:00
this.setSearchRef = this.setSearchRef.bind(this)
this.handleSearch = this.handleSearch.bind(this)
this.setScrollRef = this.setScrollRef.bind(this)
this.handleScroll = this.handleScroll.bind(this)
this.handleScrollPaint = this.handleScrollPaint.bind(this)
2017-10-07 04:02:02 +00:00
this.handleEmojiOver = this.handleEmojiOver.bind(this)
this.handleEmojiLeave = this.handleEmojiLeave.bind(this)
this.handleEmojiClick = this.handleEmojiClick.bind(this)
2018-03-15 19:36:07 +00:00
this.handleEmojiSelect = this.handleEmojiSelect.bind(this)
2017-10-07 04:02:02 +00:00
this.setPreviewRef = this.setPreviewRef.bind(this)
this.handleSkinChange = this.handleSkinChange.bind(this)
2018-03-15 19:36:07 +00:00
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleDarkMatchMediaChange = this.handleDarkMatchMediaChange.bind(this)
this.state.theme = this.getPreferredTheme()
2016-05-31 18:39:30 +00:00
}
static getDerivedStateFromProps(props, state) {
if (props.skin) {
return {
...state,
skin: props.skin,
}
} else if (props.defaultSkin && !store.get('skin')) {
return {
...state,
skin: props.defaultSkin,
}
}
return state
2016-06-09 00:22:06 +00:00
}
componentDidMount() {
if (this.state.firstRender) {
this.testStickyPosition()
2016-07-15 16:31:22 +00:00
this.firstRenderTimeout = setTimeout(() => {
this.setState({ firstRender: false })
}, 60)
}
}
2016-05-31 18:39:30 +00:00
componentDidUpdate() {
2016-07-08 17:56:29 +00:00
this.updateCategoriesSize()
2016-05-31 18:39:30 +00:00
this.handleScroll()
}
2016-07-15 16:31:22 +00:00
componentWillUnmount() {
this.SEARCH_CATEGORY.emojis = null
2016-07-15 16:31:22 +00:00
clearTimeout(this.leaveTimeout)
clearTimeout(this.firstRenderTimeout)
if (this.darkMatchMedia) {
this.darkMatchMedia.removeListener(this.handleDarkMatchMediaChange)
}
2016-07-15 16:31:22 +00:00
}
2016-05-31 18:39:30 +00:00
testStickyPosition() {
2017-09-17 08:54:22 +00:00
const stickyTestElement = document.createElement('div')
const prefixes = ['', '-webkit-', '-ms-', '-moz-', '-o-']
2017-10-07 04:02:02 +00:00
prefixes.forEach(
2018-03-27 18:51:26 +00:00
(prefix) => (stickyTestElement.style.position = `${prefix}sticky`),
2017-10-07 04:02:02 +00:00
)
2016-05-31 16:35:08 +00:00
this.hasStickyPosition = !!stickyTestElement.style.position.length
}
getPreferredTheme() {
if (this.props.theme != 'auto') return this.props.theme
if (typeof matchMedia !== 'function') return PickerDefaultProps.theme
if (!this.darkMatchMedia) {
this.darkMatchMedia = matchMedia('(prefers-color-scheme: dark)')
this.darkMatchMedia.addListener(this.handleDarkMatchMediaChange)
}
if (this.darkMatchMedia.media.match(/^not/)) return PickerDefaultProps.theme
return this.darkMatchMedia.matches ? 'dark' : 'light'
}
handleDarkMatchMediaChange() {
this.setState({ theme: this.getPreferredTheme() })
}
2016-05-31 14:36:52 +00:00
handleEmojiOver(emoji) {
var { preview } = this
2017-10-07 04:02:02 +00:00
if (!preview) {
return
}
2017-06-23 09:32:26 +00:00
// Use Array.prototype.find() when it is more widely supported.
const emojiData = this.CUSTOM.filter(
2018-03-27 18:51:26 +00:00
(customEmoji) => customEmoji.id === emoji.id,
2017-10-07 04:02:02 +00:00
)[0]
2017-09-17 08:54:22 +00:00
for (let key in emojiData) {
if (emojiData.hasOwnProperty(key)) {
2017-10-07 04:02:02 +00:00
emoji[key] = emojiData[key]
2017-09-17 08:54:22 +00:00
}
}
2017-09-17 08:54:22 +00:00
preview.setState({ emoji })
clearTimeout(this.leaveTimeout)
}
handleEmojiLeave(emoji) {
var { preview } = this
2017-10-07 04:02:02 +00:00
if (!preview) {
return
}
this.leaveTimeout = setTimeout(() => {
preview.setState({ emoji: null })
}, 16)
2016-05-31 14:36:52 +00:00
}
2016-07-21 19:10:33 +00:00
handleEmojiClick(emoji, e) {
this.props.onClick(emoji, e)
2018-03-15 19:36:07 +00:00
this.handleEmojiSelect(emoji)
}
handleEmojiSelect(emoji) {
this.props.onSelect(emoji)
if (!this.hideRecent && !this.props.recent) frequently.add(emoji)
2016-07-07 18:22:02 +00:00
var component = this.categoryRefs['category-1']
2016-07-08 17:56:29 +00:00
if (component) {
2016-07-07 18:22:02 +00:00
let maxMargin = component.maxMargin
if (this.props.enableFrequentEmojiSort) {
component.forceUpdate()
}
2016-07-07 18:22:02 +00:00
2019-12-29 23:24:18 +00:00
requestAnimationFrame(() => {
if (!this.scroll) return
2016-07-08 17:56:29 +00:00
component.memoizeSize()
2016-07-07 18:22:02 +00:00
if (maxMargin == component.maxMargin) return
2016-07-08 17:56:29 +00:00
this.updateCategoriesSize()
2016-07-07 18:22:02 +00:00
this.handleScrollPaint()
if (this.SEARCH_CATEGORY.emojis) {
component.updateDisplay('none')
}
2016-07-07 18:22:02 +00:00
})
}
}
2016-05-31 16:35:08 +00:00
handleScroll() {
2016-06-02 15:26:48 +00:00
if (!this.waitingForPaint) {
this.waitingForPaint = true
2019-12-29 23:24:18 +00:00
requestAnimationFrame(this.handleScrollPaint)
2016-06-02 15:26:48 +00:00
}
}
handleScrollPaint() {
this.waitingForPaint = false
if (!this.scroll) {
return
}
2017-05-27 17:36:41 +00:00
let activeCategory = null
if (this.SEARCH_CATEGORY.emojis) {
activeCategory = this.SEARCH_CATEGORY
2017-05-27 17:36:41 +00:00
} else {
var target = this.scroll,
2017-10-07 04:02:02 +00:00
scrollTop = target.scrollTop,
scrollingDown = scrollTop > (this.scrollTop || 0),
minTop = 0
2017-05-27 17:36:41 +00:00
for (let i = 0, l = this.categories.length; i < l; i++) {
2017-10-07 04:02:02 +00:00
let ii = scrollingDown ? this.categories.length - 1 - i : i,
category = this.categories[ii],
component = this.categoryRefs[`category-${ii}`]
2017-05-27 17:36:41 +00:00
if (component) {
let active = component.handleScroll(scrollTop)
if (!minTop || component.top < minTop) {
if (component.top > 0) {
minTop = component.top
}
}
2017-05-27 17:36:41 +00:00
if (active && !activeCategory) {
activeCategory = category
}
2016-05-31 16:35:08 +00:00
}
2016-06-02 15:26:48 +00:00
}
2017-05-27 17:36:41 +00:00
if (scrollTop < minTop) {
2017-10-07 04:02:02 +00:00
activeCategory = this.categories.filter(
2018-03-27 18:51:26 +00:00
(category) => !(category.anchor === false),
2017-10-07 04:02:02 +00:00
)[0]
2017-05-27 17:36:41 +00:00
} else if (scrollTop + this.clientHeight >= this.scrollHeight) {
activeCategory = this.categories[this.categories.length - 1]
}
}
2016-06-02 15:26:48 +00:00
if (activeCategory) {
let { anchors } = this,
2017-10-07 04:02:02 +00:00
{ name: categoryName } = activeCategory
2016-06-02 15:26:48 +00:00
if (anchors.state.selected != categoryName) {
anchors.setState({ selected: categoryName })
}
2016-05-31 16:35:08 +00:00
}
2016-06-02 15:26:48 +00:00
this.scrollTop = scrollTop
2016-05-31 16:35:08 +00:00
}
2016-05-31 18:39:30 +00:00
handleSearch(emojis) {
this.SEARCH_CATEGORY.emojis = emojis
2016-07-08 17:56:29 +00:00
for (let i = 0, l = this.categories.length; i < l; i++) {
let component = this.categoryRefs[`category-${i}`]
2016-07-08 17:56:29 +00:00
if (component && component.props.name != 'Search') {
let display = emojis ? 'none' : 'inherit'
component.updateDisplay(display)
2016-07-08 17:56:29 +00:00
}
2016-05-31 18:39:30 +00:00
}
2016-07-08 17:56:29 +00:00
this.forceUpdate()
if (this.scroll) {
this.scroll.scrollTop = 0
}
this.handleScroll()
2016-05-31 18:39:30 +00:00
}
2016-06-02 18:58:19 +00:00
handleAnchorClick(category, i) {
var component = this.categoryRefs[`category-${i}`],
2017-10-07 04:02:02 +00:00
{ scroll, anchors } = this,
scrollToComponent = null
2016-06-02 18:58:19 +00:00
scrollToComponent = () => {
if (component) {
let { top } = component
if (category.first) {
top = 0
} else {
top += 1
}
2016-06-02 18:58:19 +00:00
scroll.scrollTop = top
2016-06-02 18:58:19 +00:00
}
}
if (this.SEARCH_CATEGORY.emojis) {
this.handleSearch(null)
this.search.clear()
2016-06-02 18:58:19 +00:00
2019-12-29 23:24:18 +00:00
requestAnimationFrame(scrollToComponent)
} else {
scrollToComponent()
2016-06-02 18:58:19 +00:00
}
}
2016-06-09 00:22:06 +00:00
handleSkinChange(skin) {
var newState = { skin: skin },
{ onSkinChange } = this.props
this.setState(newState)
store.update(newState)
onSkinChange(skin)
2016-06-09 00:22:06 +00:00
}
2018-03-15 19:36:07 +00:00
handleKeyDown(e) {
let handled = false
2018-03-15 19:36:07 +00:00
switch (e.keyCode) {
case 13:
let emoji
2018-03-27 18:51:26 +00:00
if (
this.SEARCH_CATEGORY.emojis &&
this.SEARCH_CATEGORY.emojis.length &&
(emoji = getSanitizedData(
this.SEARCH_CATEGORY.emojis[0],
this.state.skin,
this.props.set,
this.props.data,
))
2018-03-27 18:51:26 +00:00
) {
this.handleEmojiSelect(emoji)
handled = true
2018-03-15 19:36:07 +00:00
}
2018-03-27 18:51:26 +00:00
break
2018-03-15 19:36:07 +00:00
}
2018-03-15 19:36:07 +00:00
if (handled) {
e.preventDefault()
}
}
2016-07-08 17:56:29 +00:00
updateCategoriesSize() {
for (let i = 0, l = this.categories.length; i < l; i++) {
let component = this.categoryRefs[`category-${i}`]
2016-07-08 17:56:29 +00:00
if (component) component.memoizeSize()
}
if (this.scroll) {
let target = this.scroll
this.scrollHeight = target.scrollHeight
this.clientHeight = target.clientHeight
}
2016-07-08 17:56:29 +00:00
}
getCategories() {
2017-10-07 04:02:02 +00:00
return this.state.firstRender
? this.categories.slice(0, 3)
: this.categories
}
setAnchorsRef(c) {
this.anchors = c
}
setSearchRef(c) {
this.search = c
}
setPreviewRef(c) {
this.preview = c
}
setScrollRef(c) {
this.scroll = c
}
setCategoryRef(name, c) {
if (!this.categoryRefs) {
this.categoryRefs = {}
}
this.categoryRefs[name] = c
}
2016-05-31 14:36:52 +00:00
render() {
2017-10-07 04:02:02 +00:00
var {
perLine,
emojiSize,
set,
sheetSize,
sheetColumns,
sheetRows,
2017-10-07 04:02:02 +00:00
style,
title,
emoji,
color,
native,
backgroundImageFn,
emojisToShowFilter,
showPreview,
showSkinTones,
2017-10-07 04:02:02 +00:00
emojiTooltip,
useButton,
2017-10-07 04:02:02 +00:00
include,
exclude,
recent,
2017-10-07 04:02:02 +00:00
autoFocus,
skinEmoji,
notFound,
2018-07-30 21:06:23 +00:00
notFoundEmoji,
2017-10-07 04:02:02 +00:00
} = this.props,
{ skin, theme } = this.state,
2017-10-07 04:02:02 +00:00
width = perLine * (emojiSize + 12) + 12 + 2 + measureScrollbar()
return (
<section
2018-03-27 18:51:26 +00:00
style={{ width: width, ...style }}
className={`emoji-mart emoji-mart-${theme}`}
aria-label={title}
2018-03-27 18:51:26 +00:00
onKeyDown={this.handleKeyDown}
>
2017-10-07 04:02:02 +00:00
<div className="emoji-mart-bar">
<Anchors
ref={this.setAnchorsRef}
data={this.data}
2016-10-27 03:22:59 +00:00
i18n={this.i18n}
2017-10-07 04:02:02 +00:00
color={color}
categories={this.categories}
onAnchorClick={this.handleAnchorClick}
icons={this.icons}
2016-05-31 14:36:52 +00:00
/>
2017-10-07 04:02:02 +00:00
</div>
2016-05-31 14:36:52 +00:00
2018-04-30 00:55:13 +00:00
<Search
2017-10-07 04:02:02 +00:00
ref={this.setSearchRef}
onSearch={this.handleSearch}
data={this.data}
2017-10-07 04:02:02 +00:00
i18n={this.i18n}
emojisToShowFilter={emojisToShowFilter}
include={include}
exclude={exclude}
custom={this.CUSTOM}
2017-10-07 04:02:02 +00:00
autoFocus={autoFocus}
2016-05-31 14:36:52 +00:00
/>
2017-10-07 04:02:02 +00:00
<div
2017-10-07 04:02:02 +00:00
ref={this.setScrollRef}
className="emoji-mart-scroll"
onScroll={this.handleScroll}
>
{this.getCategories().map((category, i) => {
return (
2018-04-30 00:55:13 +00:00
<Category
2017-10-07 04:02:02 +00:00
ref={this.setCategoryRef.bind(this, `category-${i}`)}
key={category.name}
id={category.id}
2017-10-07 04:02:02 +00:00
name={category.name}
emojis={category.emojis}
perLine={perLine}
native={native}
hasStickyPosition={this.hasStickyPosition}
data={this.data}
2017-10-07 04:02:02 +00:00
i18n={this.i18n}
2018-03-02 20:10:04 +00:00
recent={
category.id == this.RECENT_CATEGORY.id ? recent : undefined
}
2017-10-07 04:02:02 +00:00
custom={
category.id == this.RECENT_CATEGORY.id
? this.CUSTOM
2017-12-15 20:31:49 +00:00
: undefined
2017-10-07 04:02:02 +00:00
}
emojiProps={{
native: native,
skin: skin,
size: emojiSize,
set: set,
sheetSize: sheetSize,
sheetColumns: sheetColumns,
sheetRows: sheetRows,
2017-10-07 04:02:02 +00:00
forceSize: native,
tooltip: emojiTooltip,
backgroundImageFn: backgroundImageFn,
useButton: useButton,
2017-10-07 04:02:02 +00:00
onOver: this.handleEmojiOver,
onLeave: this.handleEmojiLeave,
onClick: this.handleEmojiClick,
}}
notFound={notFound}
2018-07-30 21:06:23 +00:00
notFoundEmoji={notFoundEmoji}
2017-10-07 04:02:02 +00:00
/>
)
})}
</div>
2017-10-07 04:02:02 +00:00
{(showPreview || showSkinTones) && (
2017-10-07 04:02:02 +00:00
<div className="emoji-mart-bar">
2018-04-30 00:55:13 +00:00
<Preview
2017-10-07 04:02:02 +00:00
ref={this.setPreviewRef}
data={this.data}
2017-10-07 04:02:02 +00:00
title={title}
emoji={emoji}
showSkinTones={showSkinTones}
showPreview={showPreview}
2017-10-07 04:02:02 +00:00
emojiProps={{
native: native,
size: 38,
skin: skin,
set: set,
sheetSize: sheetSize,
sheetColumns: sheetColumns,
sheetRows: sheetRows,
2017-10-07 04:02:02 +00:00
backgroundImageFn: backgroundImageFn,
}}
skinsProps={{
skin: skin,
onChange: this.handleSkinChange,
skinEmoji: skinEmoji,
2017-10-07 04:02:02 +00:00
}}
i18n={this.i18n}
2017-10-07 04:02:02 +00:00
/>
</div>
)}
</section>
2017-10-07 04:02:02 +00:00
)
2016-05-31 14:36:52 +00:00
}
}
NimblePicker.propTypes /* remove-proptypes */ = {
2018-04-26 14:36:54 +00:00
...PickerPropTypes,
data: PropTypes.object.isRequired,
}
NimblePicker.defaultProps = { ...PickerDefaultProps }