UNPKG

emoji-mart

Version:

Customizable Slack-like emoji picker for React

633 lines (546 loc) 17.7 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } import React from 'react'; import PropTypes from 'prop-types'; import * as icons from '../../svgs'; import store from '../../utils/store'; import frequently from '../../utils/frequently'; import { deepMerge, measureScrollbar, getSanitizedData } from '../../utils'; import { uncompress } from '../../utils/data'; import { PickerPropTypes } from '../../utils/shared-props'; import Anchors from '../anchors'; import Category from '../category'; import Preview from '../preview'; import Search from '../search'; import { PickerDefaultProps } from '../../utils/shared-default-props'; const I18N = { search: 'Search', clear: 'Clear', // Accessible label on "clear" button notfound: 'No Emoji Found', skintext: 'Choose your default skin tone', 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' }, 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' } }; export default class NimblePicker extends React.PureComponent { 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 }; if (props.data.compressed) { uncompress(props.data); } this.data = props.data; this.i18n = deepMerge(I18N, props.i18n); this.icons = deepMerge(icons, props.icons); this.state = { firstRender: true }; 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 = _objectSpread({}, emoji, { // `<Category />` expects emoji to have an `id`. id: emoji.short_names[0], custom: true }); category.emojis.push(customEmoji); this.CUSTOM.push(customEmoji); }); allCategories = allCategories.concat(Object.keys(customCategories).map(key => customCategories[key])); } this.hideRecent = true; if (props.include != undefined) { allCategories.sort((a, b) => { if (props.include.indexOf(a.id) > props.include.indexOf(b.id)) { return 1; } return -1; }); } for (let categoryIndex = 0; categoryIndex < allCategories.length; categoryIndex++) { const category = allCategories[categoryIndex]; let isIncluded = props.include && props.include.length ? props.include.indexOf(category.id) > -1 : true; let isExcluded = props.exclude && props.exclude.length ? props.exclude.indexOf(category.id) > -1 : false; if (!isIncluded || isExcluded) { continue; } if (props.emojisToShowFilter) { let newEmojis = []; const { emojis } = category; 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, id: category.id }; this.categories.push(newCategory); } } else { this.categories.push(category); } } let includeRecent = props.include && props.include.length ? props.include.indexOf(this.RECENT_CATEGORY.id) > -1 : true; let excludeRecent = props.exclude && props.exclude.length ? props.exclude.indexOf(this.RECENT_CATEGORY.id) > -1 : false; 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); this.setAnchorsRef = this.setAnchorsRef.bind(this); this.handleAnchorClick = this.handleAnchorClick.bind(this); 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); this.handleEmojiOver = this.handleEmojiOver.bind(this); this.handleEmojiLeave = this.handleEmojiLeave.bind(this); this.handleEmojiClick = this.handleEmojiClick.bind(this); this.handleEmojiSelect = this.handleEmojiSelect.bind(this); this.setPreviewRef = this.setPreviewRef.bind(this); this.handleSkinChange = this.handleSkinChange.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleDarkMatchMediaChange = this.handleDarkMatchMediaChange.bind(this); } componentDidMount() { if (this.state.firstRender) { this.testStickyPosition(); this.firstRenderTimeout = setTimeout(() => { this.setState({ firstRender: false }); }, 60); } } componentDidUpdate() { this.updateCategoriesSize(); this.handleScroll(); } componentWillUnmount() { this.SEARCH_CATEGORY.emojis = null; clearTimeout(this.leaveTimeout); clearTimeout(this.firstRenderTimeout); if (this.darkMatchMedia) { this.darkMatchMedia.removeListener(this.handleDarkMatchMediaChange); } } testStickyPosition() { const stickyTestElement = document.createElement('div'); const prefixes = ['', '-webkit-', '-ms-', '-moz-', '-o-']; prefixes.forEach(prefix => stickyTestElement.style.position = `${prefix}sticky`); this.hasStickyPosition = !!stickyTestElement.style.position.length; } getPreferredTheme() { if (this.props.theme != 'auto') return this.props.theme; if (this.state.theme) return this.state.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.darkMatchMedia.matches ? 'dark' : 'light' }); } handleEmojiOver(emoji) { var { preview } = this; if (!preview) { return; } // Use Array.prototype.find() when it is more widely supported. const emojiData = this.CUSTOM.filter(customEmoji => customEmoji.id === emoji.id)[0]; for (let key in emojiData) { if (emojiData.hasOwnProperty(key)) { emoji[key] = emojiData[key]; } } preview.setState({ emoji }); clearTimeout(this.leaveTimeout); } handleEmojiLeave(emoji) { var { preview } = this; if (!preview) { return; } this.leaveTimeout = setTimeout(() => { preview.setState({ emoji: null }); }, 16); } handleEmojiClick(emoji, e) { this.props.onClick(emoji, e); this.handleEmojiSelect(emoji); } handleEmojiSelect(emoji) { this.props.onSelect(emoji); if (!this.hideRecent && !this.props.recent) frequently.add(emoji); var component = this.categoryRefs['category-1']; if (component) { let maxMargin = component.maxMargin; if (this.props.enableFrequentEmojiSort) { component.forceUpdate(); } requestAnimationFrame(() => { if (!this.scroll) return; component.memoizeSize(); if (maxMargin == component.maxMargin) return; this.updateCategoriesSize(); this.handleScrollPaint(); if (this.SEARCH_CATEGORY.emojis) { component.updateDisplay('none'); } }); } } handleScroll() { if (!this.waitingForPaint) { this.waitingForPaint = true; requestAnimationFrame(this.handleScrollPaint); } } handleScrollPaint() { this.waitingForPaint = false; if (!this.scroll) { return; } let activeCategory = null; if (this.SEARCH_CATEGORY.emojis) { activeCategory = this.SEARCH_CATEGORY; } else { var target = this.scroll, scrollTop = target.scrollTop, scrollingDown = scrollTop > (this.scrollTop || 0), minTop = 0; for (let i = 0, l = this.categories.length; i < l; i++) { let ii = scrollingDown ? this.categories.length - 1 - i : i, category = this.categories[ii], component = this.categoryRefs[`category-${ii}`]; if (component) { let active = component.handleScroll(scrollTop); if (!minTop || component.top < minTop) { if (component.top > 0) { minTop = component.top; } } if (active && !activeCategory) { activeCategory = category; } } } if (scrollTop < minTop) { activeCategory = this.categories.filter(category => !(category.anchor === false))[0]; } else if (scrollTop + this.clientHeight >= this.scrollHeight) { activeCategory = this.categories[this.categories.length - 1]; } } if (activeCategory) { let { anchors } = this, { name: categoryName } = activeCategory; if (anchors.state.selected != categoryName) { anchors.setState({ selected: categoryName }); } } this.scrollTop = scrollTop; } handleSearch(emojis) { this.SEARCH_CATEGORY.emojis = emojis; for (let i = 0, l = this.categories.length; i < l; i++) { let component = this.categoryRefs[`category-${i}`]; if (component && component.props.name != 'Search') { let display = emojis ? 'none' : 'inherit'; component.updateDisplay(display); } } this.forceUpdate(); if (this.scroll) { this.scroll.scrollTop = 0; } this.handleScroll(); } handleAnchorClick(category, i) { var component = this.categoryRefs[`category-${i}`], { scroll, anchors } = this, scrollToComponent = null; scrollToComponent = () => { if (component) { let { top } = component; if (category.first) { top = 0; } else { top += 1; } scroll.scrollTop = top; } }; if (this.SEARCH_CATEGORY.emojis) { this.handleSearch(null); this.search.clear(); requestAnimationFrame(scrollToComponent); } else { scrollToComponent(); } } handleSkinChange(skin) { var newState = { skin: skin }, { onSkinChange } = this.props; this.setState(newState); store.update(newState); onSkinChange(skin); } handleKeyDown(e) { let handled = false; switch (e.keyCode) { case 13: let emoji; 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))) { this.handleEmojiSelect(emoji); handled = true; } break; } if (handled) { e.preventDefault(); } } updateCategoriesSize() { for (let i = 0, l = this.categories.length; i < l; i++) { let component = this.categoryRefs[`category-${i}`]; if (component) component.memoizeSize(); } if (this.scroll) { let target = this.scroll; this.scrollHeight = target.scrollHeight; this.clientHeight = target.clientHeight; } } getCategories() { 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; } render() { var { perLine, emojiSize, set, sheetSize, sheetColumns, sheetRows, style, title, emoji, color, native, backgroundImageFn, emojisToShowFilter, showPreview, showSkinTones, emojiTooltip, useButton, include, exclude, recent, autoFocus, skinEmoji, notFound, notFoundEmoji } = this.props; var width = perLine * (emojiSize + 12) + 12 + 2 + measureScrollbar(); var theme = this.getPreferredTheme(); var skin = this.props.skin || this.state.skin || store.get('skin') || this.props.defaultSkin; return React.createElement("section", { style: _objectSpread({ width: width }, style), className: `emoji-mart emoji-mart-${theme}`, "aria-label": title, onKeyDown: this.handleKeyDown }, React.createElement("div", { className: "emoji-mart-bar" }, React.createElement(Anchors, { ref: this.setAnchorsRef, data: this.data, i18n: this.i18n, color: color, categories: this.categories, onAnchorClick: this.handleAnchorClick, icons: this.icons })), React.createElement(Search, { ref: this.setSearchRef, onSearch: this.handleSearch, data: this.data, i18n: this.i18n, emojisToShowFilter: emojisToShowFilter, include: include, exclude: exclude, custom: this.CUSTOM, autoFocus: autoFocus }), React.createElement("div", { ref: this.setScrollRef, className: "emoji-mart-scroll", onScroll: this.handleScroll }, this.getCategories().map((category, i) => { return React.createElement(Category, { ref: this.setCategoryRef.bind(this, `category-${i}`), key: category.name, id: category.id, name: category.name, emojis: category.emojis, perLine: perLine, native: native, hasStickyPosition: this.hasStickyPosition, data: this.data, i18n: this.i18n, recent: category.id == this.RECENT_CATEGORY.id ? recent : undefined, custom: category.id == this.RECENT_CATEGORY.id ? this.CUSTOM : undefined, emojiProps: { native: native, skin: skin, size: emojiSize, set: set, sheetSize: sheetSize, sheetColumns: sheetColumns, sheetRows: sheetRows, forceSize: native, tooltip: emojiTooltip, backgroundImageFn: backgroundImageFn, useButton: useButton, onOver: this.handleEmojiOver, onLeave: this.handleEmojiLeave, onClick: this.handleEmojiClick }, notFound: notFound, notFoundEmoji: notFoundEmoji }); })), (showPreview || showSkinTones) && React.createElement("div", { className: "emoji-mart-bar" }, React.createElement(Preview, { ref: this.setPreviewRef, data: this.data, title: title, emoji: emoji, showSkinTones: showSkinTones, showPreview: showPreview, emojiProps: { native: native, size: 38, skin: skin, set: set, sheetSize: sheetSize, sheetColumns: sheetColumns, sheetRows: sheetRows, backgroundImageFn: backgroundImageFn }, skinsProps: { skin: skin, onChange: this.handleSkinChange, skinEmoji: skinEmoji }, i18n: this.i18n }))); } } NimblePicker.propTypes /* remove-proptypes */ = _objectSpread({}, PickerPropTypes, { data: PropTypes.object.isRequired }); NimblePicker.defaultProps = _objectSpread({}, PickerDefaultProps);