interweave-emoji-picker
Version:
React based emoji picker powered by Interweave and Emojibase.
1,345 lines (1,162 loc) β’ 38.6 kB
JavaScript
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
// Bundled with Packemon: https://packemon.dev
// Platform: browser, Support: stable, Format: esm
import React, { useContext, useCallback, useState, useRef, useEffect } from 'react';
import { LATEST_DATASET_VERSION, EmojiDataManager, Emoji as Emoji$1, MAX_EMOJI_VERSION, useEmojiData } from 'interweave-emoji';
import debounce from 'lodash/debounce';
import { GROUP_KEY_SMILEYS_EMOTION, GROUP_KEY_PEOPLE_BODY, GROUP_KEY_COMPONENT, GROUP_KEY_ANIMALS_NATURE, GROUP_KEY_FOOD_DRINK, GROUP_KEY_TRAVEL_PLACES, GROUP_KEY_ACTIVITIES, GROUP_KEY_OBJECTS, GROUP_KEY_SYMBOLS, GROUP_KEY_FLAGS, SKIN_KEY_LIGHT, SKIN_KEY_MEDIUM_LIGHT, SKIN_KEY_MEDIUM, SKIN_KEY_MEDIUM_DARK, SKIN_KEY_DARK } from 'emojibase';
export { GROUP_KEY_ACTIVITIES, GROUP_KEY_ANIMALS_NATURE, GROUP_KEY_COMPONENT, GROUP_KEY_FLAGS, GROUP_KEY_FOOD_DRINK, GROUP_KEY_OBJECTS, GROUP_KEY_PEOPLE_BODY, GROUP_KEY_SMILEYS_EMOTION, GROUP_KEY_SYMBOLS, GROUP_KEY_TRAVEL_PLACES, SKIN_KEY_DARK, SKIN_KEY_LIGHT, SKIN_KEY_MEDIUM, SKIN_KEY_MEDIUM_DARK, SKIN_KEY_MEDIUM_LIGHT } from 'emojibase';
import { FixedSizeList } from 'react-window';
import chunk from 'lodash/chunk';
import camelCase from 'lodash/camelCase';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
/* eslint-disable sort-keys */
const GROUP_KEY_COMMONLY_USED = 'commonly-used';
const GROUP_KEY_SEARCH_RESULTS = 'search-results';
const GROUP_KEY_VARIATIONS = 'variations';
const GROUP_KEY_NONE = 'none';
const GROUPS = [GROUP_KEY_SMILEYS_EMOTION, GROUP_KEY_PEOPLE_BODY, GROUP_KEY_COMPONENT, // Unused but required for order
GROUP_KEY_ANIMALS_NATURE, GROUP_KEY_FOOD_DRINK, GROUP_KEY_TRAVEL_PLACES, GROUP_KEY_ACTIVITIES, GROUP_KEY_OBJECTS, GROUP_KEY_SYMBOLS, GROUP_KEY_FLAGS];
const GROUP_ICONS = {
[GROUP_KEY_COMMONLY_USED]: 'π',
[GROUP_KEY_SMILEYS_EMOTION]: 'π',
[GROUP_KEY_PEOPLE_BODY]: 'π',
[GROUP_KEY_ANIMALS_NATURE]: 'πΏ',
[GROUP_KEY_FOOD_DRINK]: 'π',
[GROUP_KEY_TRAVEL_PLACES]: 'πΊοΈ',
[GROUP_KEY_ACTIVITIES]: 'β½οΈ',
[GROUP_KEY_OBJECTS]: 'π',
[GROUP_KEY_SYMBOLS]: 'βοΈ',
[GROUP_KEY_FLAGS]: 'π΄'
};
const SKIN_KEY_NONE = 'none';
const SKIN_TONES = [SKIN_KEY_NONE, SKIN_KEY_LIGHT, SKIN_KEY_MEDIUM_LIGHT, SKIN_KEY_MEDIUM, SKIN_KEY_MEDIUM_DARK, SKIN_KEY_DARK];
const SKIN_COLORS = {
[SKIN_KEY_NONE]: '#FFCC22',
[SKIN_KEY_LIGHT]: '#FADCBC',
[SKIN_KEY_MEDIUM_LIGHT]: '#E0BB95',
[SKIN_KEY_MEDIUM]: '#BF8F68',
[SKIN_KEY_MEDIUM_DARK]: '#9B643D',
[SKIN_KEY_DARK]: '#5A463A'
};
const SCROLL_BUFFER = 150;
const SCROLL_DEBOUNCE = 100;
const SEARCH_THROTTLE = 300;
const KEY_COMMONLY_USED = 'interweave/emoji/commonlyUsed';
const KEY_SKIN_TONE = 'interweave/emoji/skinTone';
const COMMON_MODE_RECENT = 'recently-used';
const COMMON_MODE_FREQUENT = 'frequently-used';
const CONTEXT_CLASSNAMES = {
picker: 'interweave-picker__picker',
emoji: 'interweave-picker__emoji',
emojiActive: 'interweave-picker__emoji--active',
emojis: 'interweave-picker__emojis',
emojisList: 'interweave-picker__emojis-list',
emojisRow: 'interweave-picker__emojis-row',
emojisHeader: 'interweave-picker__emojis-header',
emojisHeaderSticky: 'interweave-picker__emojis-header--sticky',
emojisBody: 'interweave-picker__emojis-body',
group: 'interweave-picker__group',
groupActive: 'interweave-picker__group--active',
groups: 'interweave-picker__groups',
groupsList: 'interweave-picker__groups-list',
skinTone: 'interweave-picker__skin-tone',
skinToneActive: 'interweave-picker__skin-tone--active',
skinTones: 'interweave-picker__skin-tones',
skinTonesList: 'interweave-picker__skin-tones-list',
noPreview: 'interweave-picker__no-preview',
noResults: 'interweave-picker__no-results',
preview: 'interweave-picker__preview',
previewEmoji: 'interweave-picker__preview-emoji',
previewContent: 'interweave-picker__preview-content',
previewTitle: 'interweave-picker__preview-title',
previewSubtitle: 'interweave-picker__preview-subtitle',
previewShiftMore: 'interweave-picker__preview-more',
search: 'interweave-picker__search',
searchInput: 'interweave-picker__search-input',
clear: 'interweave-picker__clear'
};
const CONTEXT_MESSAGES = {
frequentlyUsed: 'Frequently used',
recentlyUsed: 'Recently used',
variations: 'Variations',
none: 'All emojis',
skinNone: 'No skin tone',
search: 'Search',
searchA11y: 'Search for emojis by keyword',
searchResults: 'Search results',
noPreview: '',
noResults: 'No results',
clearUsed: 'Clear used'
};
const version = process.env.NODE_ENV === 'test' ? '0.0.0' : LATEST_DATASET_VERSION;
const Context = /*#__PURE__*/React.createContext({
classNames: CONTEXT_CLASSNAMES,
emojiData: EmojiDataManager.getInstance('en', version),
emojiLargeSize: 0,
emojiPadding: 0,
emojiPath: '{{hexcode}}',
emojiSize: 0,
emojiSource: {
compact: false,
locale: 'en',
version
},
messages: CONTEXT_MESSAGES
});
const TITLE_REGEX = /(^|:|\.)\s?[a-z]/g;
function useTitleFormat(title) {
return title.replace(TITLE_REGEX, token => token.toUpperCase());
}
function useGroupMessage(group, commonMode) {
const {
emojiData,
messages
} = useContext(Context);
let title = '';
if (group === GROUP_KEY_COMMONLY_USED) {
title = messages[camelCase(commonMode)];
} else {
var _emojiData$GROUPS_BY_;
const key = camelCase(String(group) === GROUP_KEY_COMMONLY_USED ? commonMode : group);
title = (_emojiData$GROUPS_BY_ = emojiData.GROUPS_BY_KEY[group]) !== null && _emojiData$GROUPS_BY_ !== void 0 ? _emojiData$GROUPS_BY_ : messages[key];
}
return useTitleFormat(title);
}
function EmojiListHeader({
clearIcon,
commonMode,
group,
skinTonePalette,
sticky,
onClear
}) {
const {
classNames,
messages
} = useContext(Context);
const showClear = clearIcon && (group === GROUP_KEY_COMMONLY_USED || group === GROUP_KEY_VARIATIONS);
const showPalette = skinTonePalette && (group === GROUP_KEY_PEOPLE_BODY || group === GROUP_KEY_SEARCH_RESULTS || group === GROUP_KEY_NONE);
const className = [classNames.emojisHeader];
const title = useGroupMessage(group, commonMode);
if (sticky) {
className.push(classNames.emojisHeaderSticky);
}
const handleClear = useCallback(event => {
event.preventDefault();
onClear();
}, [onClear]);
return /*#__PURE__*/React.createElement("header", {
className: className.join(' ')
}, /*#__PURE__*/React.createElement("span", null, title), showPalette && skinTonePalette, showClear && /*#__PURE__*/React.createElement("button", {
className: classNames.clear,
title: messages.clearUsed,
type: "button",
onClick: handleClear
}, clearIcon));
}
function Emoji({
active,
emoji,
onEnter,
onLeave,
onSelect
}) {
const {
classNames,
emojiPadding,
emojiPath,
emojiSize,
emojiSource
} = useContext(Context);
const dimension = emojiPadding + emojiPadding + emojiSize;
const className = [classNames.emoji];
if (active) {
className.push(classNames.emojiActive);
} // Handlers
const handleClick = useCallback(event => {
event.stopPropagation();
onSelect(emoji, event);
}, [emoji, onSelect]);
const handleEnter = useCallback(event => {
event.stopPropagation();
onEnter(emoji, event);
}, [emoji, onEnter]);
const handleLeave = useCallback(event => {
event.stopPropagation();
onLeave(emoji, event);
}, [emoji, onLeave]);
return /*#__PURE__*/React.createElement("button", {
key: emoji.hexcode,
className: className.join(' ') // eslint-disable-next-line react-perf/jsx-no-new-object-as-prop
,
style: {
height: dimension,
padding: emojiPadding,
width: dimension
},
title: emoji.label,
type: "button",
onClick: handleClick,
onMouseEnter: handleEnter,
onMouseLeave: handleLeave
}, /*#__PURE__*/React.createElement(Emoji$1, {
emojiPath: emojiPath,
emojiSize: emojiSize,
emojiSource: emojiSource,
hexcode: emoji.hexcode
}));
}
function EmojiListRow({
data,
index,
style,
// Interweave
activeEmoji,
clearIcon,
commonMode,
skinTonePalette,
onClear,
onEnterEmoji,
onLeaveEmoji,
onSelectEmoji
}) {
const {
classNames
} = useContext(Context);
const row = data[index];
return /*#__PURE__*/React.createElement("div", {
className: classNames.emojisRow,
style: style
}, Array.isArray(row) ? /*#__PURE__*/React.createElement("div", {
className: classNames.emojisBody
}, row.map(emoji => /*#__PURE__*/React.createElement(Emoji, {
key: emoji.hexcode,
active: activeEmoji ? activeEmoji.hexcode === emoji.hexcode : false,
emoji: emoji,
onEnter: onEnterEmoji,
onLeave: onLeaveEmoji,
onSelect: onSelectEmoji
}))) : /*#__PURE__*/React.createElement(EmojiListHeader, {
clearIcon: clearIcon,
commonMode: commonMode,
group: row,
skinTonePalette: skinTonePalette,
onClear: onClear
}));
}
function EmojiList({
activeGroup,
columnCount,
columnPadding = 0,
groupedEmojis,
hideGroupHeaders,
noResults,
rowCount,
rowPadding = 0,
scrollToGroup,
stickyGroupHeader,
onScroll,
onScrollGroup,
...rowProps
}) {
const {
classNames,
emojiPadding,
emojiSize,
messages
} = useContext(Context);
const [rows, setRows] = useState([]);
const [indices, setIndices] = useState({});
const ref = useRef(null);
const size = emojiSize + emojiPadding * 2;
const rowHeight = size + rowPadding * 2;
const columnWidth = size + columnPadding * 2; // When emojis or virtual list props change,
// we need to regenerate the list of rows.
useEffect(() => {
const virtualRows = [];
const nextIndices = {
'': -1 // Handle empty scroll to's
};
Object.keys(groupedEmojis).forEach(group => {
nextIndices[group] = virtualRows.length;
if (group === GROUP_KEY_COMPONENT) {
return;
}
if (!hideGroupHeaders) {
virtualRows.push(group);
}
virtualRows.push(...chunk(groupedEmojis[group].emojis, columnCount));
});
setRows(virtualRows);
setIndices(nextIndices);
}, [columnCount, groupedEmojis, hideGroupHeaders]); // Scroll to the defined group when all data is available
useEffect(() => {
if (ref.current && scrollToGroup && indices[scrollToGroup] >= 0) {
ref.current.scrollToItem(indices[scrollToGroup], 'start');
}
}, [scrollToGroup, indices]); // Loop over each group section within the scrollable container
// and determine the active group.
const handleRendered = useCallback(({
visibleStartIndex
}) => {
let lastGroup = '';
Object.keys(indices).some(group => {
const index = indices[group]; // Special case for commonly used and smileys, as they usually both render in the same view
if (index === 0 && visibleStartIndex === 0) {
lastGroup = group;
return true;
} // When we have to sticky headers, we need to change the header on the index right
// before the next header, otherwise the change will happen too late
if (stickyGroupHeader && index >= visibleStartIndex + 1) {
return true; // Otherwise, we should update the active group when half way through the list
}
if (!stickyGroupHeader && index >= visibleStartIndex + rowCount / 2) {
return true;
}
lastGroup = group;
return false;
}); // Only update if a different group
if (lastGroup && lastGroup !== activeGroup) {
onScrollGroup(lastGroup);
}
}, [activeGroup, indices, onScrollGroup, rowCount, stickyGroupHeader]); // If no items to display, just return null
if (rows.length === 0) {
return /*#__PURE__*/React.createElement("div", {
className: classNames.noResults
}, noResults !== null && noResults !== void 0 ? noResults : messages.noResults);
}
return /*#__PURE__*/React.createElement("div", {
className: classNames.emojis
}, /*#__PURE__*/React.createElement(FixedSizeList, {
ref: ref,
className: classNames.emojisList,
height: rowHeight * rowCount,
itemCount: rows.length,
itemData: rows,
itemSize: rowHeight,
overscanCount: rowCount / 2,
width: columnWidth * columnCount,
onItemsRendered: handleRendered,
onScroll: onScroll
}, props => /*#__PURE__*/React.createElement(EmojiListRow, _extends({}, rowProps, props))), stickyGroupHeader && activeGroup !== GROUP_KEY_NONE && /*#__PURE__*/React.createElement(EmojiListHeader, _extends({}, rowProps, {
sticky: true,
group: activeGroup
})));
}
function Group({
active,
children,
commonMode,
group,
onSelect
}) {
const {
classNames
} = useContext(Context);
const className = [classNames.group];
const title = useGroupMessage(group, commonMode);
if (active) {
className.push(classNames.groupActive);
}
const handleClick = useCallback(event => {
event.stopPropagation();
onSelect(group, event);
}, [group, onSelect]);
return /*#__PURE__*/React.createElement("button", {
className: className.join(' '),
title: title,
type: "button",
onClick: handleClick
}, children);
}
function GroupTabs({
activeGroup,
commonEmojis,
commonMode,
icons,
onSelect
}) {
const {
classNames
} = useContext(Context);
const groups = GROUPS.filter(group => group !== GROUP_KEY_COMPONENT);
if (commonEmojis.length > 0) {
groups.unshift(GROUP_KEY_COMMONLY_USED);
}
return /*#__PURE__*/React.createElement("nav", {
className: classNames.groups
}, /*#__PURE__*/React.createElement("ul", {
className: classNames.groupsList
}, groups.map(group => {
var _ref, _icons$group;
return /*#__PURE__*/React.createElement("li", {
key: group
}, /*#__PURE__*/React.createElement(Group, {
active: group === activeGroup,
commonMode: commonMode,
group: group,
onSelect: onSelect
}, (_ref = (_icons$group = icons[group]) !== null && _icons$group !== void 0 ? _icons$group : icons[camelCase(group)]) !== null && _ref !== void 0 ? _ref : GROUP_ICONS[group]));
})));
} // eslint-disable-next-line complexity
function PreviewBar({
emoji,
hideEmoticon,
hideShortcodes,
noPreview
}) {
var _emoji$label;
const {
classNames,
emojiLargeSize,
emojiPath,
emojiSource,
messages
} = useContext(Context);
const title = useTitleFormat((_emoji$label = emoji === null || emoji === void 0 ? void 0 : emoji.label) !== null && _emoji$label !== void 0 ? _emoji$label : '');
const subtitle = [];
if (!emoji) {
const preview = noPreview !== null && noPreview !== void 0 ? noPreview : messages.noPreview;
return /*#__PURE__*/React.createElement("section", {
className: classNames.preview
}, preview && /*#__PURE__*/React.createElement("div", {
className: classNames.noPreview
}, preview));
}
if (!hideEmoticon && emoji.emoticon) {
if (Array.isArray(emoji.emoticon)) {
subtitle.push(...emoji.emoticon);
} else {
subtitle.push(emoji.emoticon);
}
}
if (!hideShortcodes && emoji.canonical_shortcodes) {
subtitle.push(...emoji.canonical_shortcodes);
}
return /*#__PURE__*/React.createElement("section", {
className: classNames.preview
}, /*#__PURE__*/React.createElement("div", {
className: classNames.previewEmoji
}, /*#__PURE__*/React.createElement(Emoji$1, {
enlargeEmoji: true,
emojiLargeSize: emojiLargeSize,
emojiPath: emojiPath,
emojiSource: emojiSource,
hexcode: emoji.hexcode
})), /*#__PURE__*/React.createElement("div", {
className: classNames.previewContent
}, title && /*#__PURE__*/React.createElement("div", {
className: classNames.previewTitle
}, title, emoji.skins && emoji.skins.length > 0 &&
/*#__PURE__*/
// eslint-disable-next-line react/jsx-no-literals
React.createElement("span", {
className: classNames.previewShiftMore
}, `(+${emoji.skins.length})`)), subtitle.length > 0 && /*#__PURE__*/React.createElement("div", {
className: classNames.previewSubtitle
}, subtitle.join(' '))));
}
function SearchBar({
autoFocus,
searchQuery,
onChange,
onKeyUp
}) {
const {
classNames,
messages
} = useContext(Context);
const ref = useRef(null);
useEffect(() => {
if (autoFocus && ref.current) {
ref.current.focus();
}
}, [autoFocus]);
const handleChange = useCallback(event => {
// Check if were still mounted
if (ref.current) {
onChange(event.target.value.trim(), event);
}
}, [onChange]);
return /*#__PURE__*/React.createElement("div", {
className: classNames.search
}, /*#__PURE__*/React.createElement("input", {
ref: ref,
"aria-label": messages.searchA11y,
className: classNames.searchInput,
placeholder: messages.search,
type: "search",
value: searchQuery,
onChange: handleChange,
onKeyUp: onKeyUp
}));
}
function useSkinToneMessage(skinTone) {
var _ref2;
const {
emojiData,
messages
} = useContext(Context);
const title = (_ref2 = skinTone === SKIN_KEY_NONE ? messages.skinNone : emojiData.SKIN_TONES_BY_KEY[skinTone]) !== null && _ref2 !== void 0 ? _ref2 : '';
return useTitleFormat(title);
}
function SkinTone({
active,
children,
skinTone,
onSelect
}) {
const {
classNames
} = useContext(Context);
const className = [classNames.skinTone];
const color = SKIN_COLORS[skinTone];
const title = useSkinToneMessage(skinTone);
if (active) {
className.push(classNames.skinToneActive);
}
const handleClick = useCallback(event => {
event.stopPropagation();
onSelect(skinTone, event);
}, [skinTone, onSelect]);
return /*#__PURE__*/React.createElement("button", {
className: className.join(' '),
"data-skin-color": color,
"data-skin-tone": skinTone,
title: title,
type: "button",
onClick: handleClick
}, children !== null && children !== void 0 ? children : ' ');
}
function SkinTonePalette({
activeSkinTone,
icons,
onSelect
}) {
const {
classNames
} = useContext(Context);
return /*#__PURE__*/React.createElement("nav", {
className: classNames.skinTones
}, /*#__PURE__*/React.createElement("ul", {
className: classNames.skinTonesList
}, SKIN_TONES.map(skinTone => {
var _ref3, _icons$skinTone;
return /*#__PURE__*/React.createElement("li", {
key: skinTone
}, /*#__PURE__*/React.createElement(SkinTone, {
active: activeSkinTone === skinTone,
skinTone: skinTone,
onSelect: onSelect
}, (_ref3 = (_icons$skinTone = icons[skinTone]) !== null && _icons$skinTone !== void 0 ? _icons$skinTone : icons[camelCase(skinTone)]) !== null && _ref3 !== void 0 ? _ref3 : null));
})));
}
const SKIN_MODIFIER_PATTERN = /1F3FB|1F3FC|1F3FD|1F3FE|1F3FF/g;
class InternalPicker extends React.PureComponent {
constructor(props) {
var _this$getSkinToneFrom;
super(props);
_defineProperty(this, "allowList", void 0);
_defineProperty(this, "blockList", void 0);
_defineProperty(this, "handleClear", () => {
if (this.state.activeGroup === GROUP_KEY_VARIATIONS) {
this.setUpdatedState({
activeGroup: this.state.searchQuery ? GROUP_KEY_SEARCH_RESULTS : GROUP_KEY_SMILEYS_EMOTION
}, true);
} else {
this.setUpdatedState({
commonEmojis: []
});
localStorage.removeItem(KEY_COMMONLY_USED);
}
});
_defineProperty(this, "handleEnterEmoji", (emoji, event) => {
this.setUpdatedState({
activeEmoji: emoji
});
this.props.onHoverEmoji(emoji, event);
});
_defineProperty(this, "handleKeyUp", event => {
const {
columnCount = 10
} = this.props;
const {
activeEmoji,
activeEmojiIndex,
emojis,
searchQuery
} = this.state; // Keyboard functionality is only available while searching
if (!searchQuery) {
return;
} // Reset search
if (event.key === 'Escape') {
event.preventDefault(); // @ts-expect-error Allow other event
this.handleSearch('', event); // Select active emoji
} else if (event.key === 'Enter') {
event.preventDefault();
if (activeEmoji) {
// @ts-expect-error Allow other event
this.handleSelectEmoji(activeEmoji, event);
} // Cycle search results
} else {
event.preventDefault();
let nextIndex = -1;
switch (event.key) {
case 'ArrowLeft':
nextIndex = activeEmojiIndex - 1;
break;
case 'ArrowRight':
nextIndex = activeEmojiIndex + 1;
break;
case 'ArrowUp':
nextIndex = activeEmojiIndex - columnCount;
break;
case 'ArrowDown':
nextIndex = activeEmojiIndex + columnCount;
break;
default:
return;
}
if (nextIndex >= 0 && nextIndex < emojis.length) {
this.setUpdatedState({
activeEmojiIndex: nextIndex
}); // @ts-expect-error Allow other event
this.handleEnterEmoji(emojis[nextIndex], event);
}
}
});
_defineProperty(this, "handleLeaveEmoji", () => {
this.setUpdatedState({
activeEmoji: null
});
});
_defineProperty(this, "handleScrollGroup", group => {
this.setUpdatedState({
activeGroup: group,
scrollToGroup: ''
});
this.props.onScrollGroup(group);
});
_defineProperty(this, "handleSearch", (query, event) => {
// Bypass custom logic and set immediately
this.setState({
searchQuery: query
});
this.handleSearchDebounced(query);
this.props.onSearch(query, event);
});
_defineProperty(this, "handleSearchDebounced", debounce(query => {
this.setUpdatedState({
searchQuery: String(query)
});
}, SEARCH_THROTTLE));
_defineProperty(this, "handleSelectEmoji", (emoji, event) => {
this.addCommonEmoji(emoji);
if (event.shiftKey && emoji.skins && emoji.skins.length > 0) {
// Avoid bulk logic when using `setUpdatedState`
this.setState({
activeEmoji: emoji,
activeEmojiIndex: 0,
activeGroup: GROUP_KEY_VARIATIONS,
emojis: emoji.skins,
groupedEmojis: {
[GROUP_KEY_VARIATIONS]: {
emojis: emoji.skins,
group: GROUP_KEY_VARIATIONS
}
},
scrollToGroup: GROUP_KEY_VARIATIONS
});
} else {
this.props.onSelectEmoji(emoji, event);
}
});
_defineProperty(this, "handleSelectGroup", (group, event) => {
this.setUpdatedState({
activeGroup: group,
scrollToGroup: group
});
this.props.onSelectGroup(group, event);
});
_defineProperty(this, "handleSelectSkinTone", (skinTone, event) => {
this.setUpdatedState({
activeSkinTone: skinTone
});
try {
localStorage.setItem(KEY_SKIN_TONE, skinTone);
} catch {// Do nothing
}
this.props.onSelectSkinTone(skinTone, event);
});
const {
blockList,
classNames,
defaultSkinTone,
messages,
allowList
} = props;
this.allowList = this.generateAllowBlockMap(allowList);
this.blockList = this.generateAllowBlockMap(blockList);
const _searchQuery = '';
const commonEmojis = this.generateCommonEmojis(this.getCommonEmojisFromStorage());
const activeGroup = this.getActiveGroup(commonEmojis.length > 0);
const activeSkinTone = (_this$getSkinToneFrom = this.getSkinToneFromStorage()) !== null && _this$getSkinToneFrom !== void 0 ? _this$getSkinToneFrom : defaultSkinTone;
const _emojis = this.generateEmojis(activeSkinTone, _searchQuery);
const groupedEmojis = this.groupEmojis(_emojis, commonEmojis, _searchQuery);
this.state = {
activeEmoji: null,
activeEmojiIndex: -1,
activeGroup,
activeSkinTone,
commonEmojis,
context: {
classNames: { ...CONTEXT_CLASSNAMES,
...classNames
},
emojiData: props.emojiData,
emojiLargeSize: props.emojiLargeSize,
emojiPadding: props.emojiPadding,
emojiPath: props.emojiPath,
emojiSize: props.emojiSize,
emojiSource: props.emojiSource,
messages: { ...CONTEXT_MESSAGES,
...messages
}
},
emojis: _emojis,
groupedEmojis,
scrollToGroup: activeGroup,
searchQuery: _searchQuery
};
}
/**
* Add a common emoji to local storage and update the current state.
*/
addCommonEmoji(emoji) {
const {
commonMode,
disableCommonlyUsed,
maxCommonlyUsed
} = this.props;
const {
hexcode
} = emoji;
if (disableCommonlyUsed) {
return;
}
const commonEmojis = this.getCommonEmojisFromStorage();
const currentIndex = commonEmojis.findIndex(common => common.hexcode === hexcode); // Add to the front of the list if it doesnt exist
if (currentIndex === -1) {
commonEmojis.unshift({
count: 1,
hexcode
});
} // Move to the front of the list and increase count
if (commonMode === COMMON_MODE_RECENT) {
if (currentIndex >= 1) {
const [common] = commonEmojis.splice(currentIndex, 1);
commonEmojis.unshift({
count: common.count + 1,
hexcode
});
} // Increase count and sort by usage
} else if (commonMode === COMMON_MODE_FREQUENT) {
if (currentIndex >= 0) {
commonEmojis[currentIndex].count += 1;
}
commonEmojis.sort((a, b) => b.count - a.count);
} // Trim to the max and store locally
try {
localStorage.setItem(KEY_COMMONLY_USED, JSON.stringify(commonEmojis.slice(0, maxCommonlyUsed)));
} catch {// Do nothing
}
this.setUpdatedState({
commonEmojis: this.generateCommonEmojis(commonEmojis)
});
}
/**
* Filter the dataset with the search query against a set of emoji properties.
*/
// eslint-disable-next-line complexity
filterOrSearch(emoji, searchQuery) {
const {
blockList,
maxEmojiVersion,
allowList
} = this.props; // Remove blocked emojis and non-allowed emojis
if (blockList.length > 0 && this.blockList[emoji.hexcode] || allowList.length > 0 && !this.allowList[emoji.hexcode]) {
return false;
} // Remove emojis released in newer versions (compact doesnt have a version)
if (emoji.version && emoji.version > maxEmojiVersion) {
return false;
} // No query to filter with
if (!searchQuery) {
return true;
}
const lookups = [];
if (emoji.canonical_shortcodes) {
lookups.push(...emoji.canonical_shortcodes);
}
if (emoji.tags) {
lookups.push(...emoji.tags);
}
if (emoji.label) {
lookups.push(emoji.label);
}
if (emoji.emoticon) {
if (Array.isArray(emoji.emoticon)) {
lookups.push(...emoji.emoticon);
} else {
lookups.push(emoji.emoticon);
}
}
const haystack = lookups.join(' ').toLowerCase(); // Support multi-word and case-insensitive searches
return searchQuery.toLowerCase().split(' ').some(needle => haystack.includes(needle));
}
/**
* Return the list of emojis filtered with the search query if applicable,
* and with skin tone applied if set.
*/
generateEmojis(skinTone, searchQuery) {
return this.props.emojis.filter(emoji => this.filterOrSearch(emoji, searchQuery)).map(emoji => this.getSkinnedEmoji(emoji, skinTone));
}
/**
* Convert the `blockList` or `allowList` prop to a map for quicker lookups.
*/
generateAllowBlockMap(list) {
const map = {};
list.forEach(hexcode => {
if (process.env.NODE_ENV !== "production" && hexcode.match(SKIN_MODIFIER_PATTERN)) {
// eslint-disable-next-line no-console
console.warn(`Hexcode with a skin modifier has been detected: ${hexcode}`, 'Emojis without skin modifiers are required for allow/block lists.');
}
map[hexcode] = true;
});
return map;
}
/**
* We only store the hexcode character for commonly used emojis,
* so we need to rebuild the list with full emoji objects.
*/
generateCommonEmojis(commonEmojis) {
if (this.props.disableCommonlyUsed) {
return [];
}
const data = this.props.emojiData;
return commonEmojis.map(emoji => data.EMOJIS[emoji.hexcode]).filter(Boolean);
}
/**
* Return the default group while handling commonly used scenarios.
*/
getActiveGroup(hasCommon) {
const {
defaultGroup,
disableGroups
} = this.props;
let group = defaultGroup; // Allow commonly used before "none" groups
if (group === GROUP_KEY_COMMONLY_USED) {
if (hasCommon) {
return group;
}
group = GROUP_KEY_SMILEYS_EMOTION;
}
return disableGroups ? GROUP_KEY_NONE : group;
}
/**
* Return the commonly used emojis from local storage.
*/
getCommonEmojisFromStorage() {
if (this.props.disableCommonlyUsed) {
return [];
}
const common = localStorage.getItem(KEY_COMMONLY_USED);
return common ? JSON.parse(common) : [];
}
/**
* Return an emoji with skin tone if the active skin tone is set,
* otherwise return the default skin tone (yellow).
*/
getSkinnedEmoji(emoji, skinTone) {
if (skinTone === SKIN_KEY_NONE || !emoji.skins) {
return emoji;
}
const toneIndex = SKIN_TONES.indexOf(skinTone);
const skinnedEmoji = (emoji.skins || []).find(skin => !!skin.tone && (skin.tone === toneIndex || Array.isArray(skin.tone) && skin.tone.includes(toneIndex)));
return skinnedEmoji !== null && skinnedEmoji !== void 0 ? skinnedEmoji : emoji;
}
/**
* Return the user's favorite skin tone from local storage.
*/
getSkinToneFromStorage() {
const tone = localStorage.getItem(KEY_SKIN_TONE);
if (tone) {
return tone;
}
return null;
}
/**
* Partition the dataset into multiple arrays based on the group they belong to.
*/
groupEmojis(emojis, commonEmojis, searchQuery) {
const {
disableGroups
} = this.props;
const groups = {}; // Add commonly used group if not searching
if (!searchQuery && commonEmojis.length > 0) {
groups[GROUP_KEY_COMMONLY_USED] = {
emojis: commonEmojis,
group: GROUP_KEY_COMMONLY_USED
};
} // Partition emojis into separate groups
emojis.forEach(emoji => {
let group = GROUP_KEY_NONE;
if (searchQuery) {
group = GROUP_KEY_SEARCH_RESULTS;
} else if (!disableGroups) {
// Dont show hidden emojis outside of search results
if (emoji.group === undefined) {
return;
}
group = GROUPS[emoji.group];
}
if (!group) {
return;
}
if (groups[group]) {
groups[group].emojis.push(emoji);
} else {
groups[group] = {
emojis: [emoji],
group
};
}
}); // Sort each group
Object.keys(groups).forEach(group => {
if (group !== GROUP_KEY_COMMONLY_USED) {
groups[group].emojis.sort((a, b) => {
var _a$order, _b$order;
return ((_a$order = a.order) !== null && _a$order !== void 0 ? _a$order : 0) - ((_b$order = b.order) !== null && _b$order !== void 0 ? _b$order : 0);
});
} // Remove the group if no emojis
if (groups[group].emojis.length === 0) {
delete groups[group];
}
});
return groups;
}
/**
* Triggered when common emoji cache or variation window is cleared.
*/
/**
* Catch all method to easily update the state. Will automatically handle updates
* and branching based on values being set.
*/
setUpdatedState(nextState, forceRebuild = false) {
// eslint-disable-next-line complexity
this.setState(prevState => {
const state = { ...prevState,
...nextState
};
const activeGroup = this.getActiveGroup(state.commonEmojis.length > 0);
let rebuildEmojis = false; // Common emojis have changed
if ('commonEmojis' in nextState) {
rebuildEmojis = true; // Reset the active group
if (state.commonEmojis.length === 0) {
state.activeGroup = activeGroup;
}
} // Active group has changed
if ('activeGroup' in nextState && // Reset search query
state.searchQuery) {
state.searchQuery = '';
rebuildEmojis = true;
} // Active skin tone has changed
if ('activeSkinTone' in nextState) {
rebuildEmojis = true;
} // Search query has changed
if ('searchQuery' in nextState) {
rebuildEmojis = true;
state.activeGroup = state.searchQuery ? GROUP_KEY_SEARCH_RESULTS : activeGroup;
state.scrollToGroup = state.searchQuery ? GROUP_KEY_SEARCH_RESULTS : activeGroup;
} // Rebuild the emoji datasets
if (rebuildEmojis || forceRebuild) {
state.emojis = this.generateEmojis(state.activeSkinTone, state.searchQuery);
state.groupedEmojis = this.groupEmojis(state.emojis, state.commonEmojis, state.searchQuery);
const hasResults = state.searchQuery && state.emojis.length > 0;
state.activeEmoji = hasResults ? state.emojis[0] : null;
state.activeEmojiIndex = hasResults ? 0 : -1;
}
return state;
});
}
render() {
const {
autoFocus,
clearIcon,
columnCount,
commonMode,
disableGroups,
disablePreview,
disableSearch,
disableSkinTones,
displayOrder,
groupIcons,
hideEmoticon,
hideGroupHeaders,
hideShortcodes,
noPreview,
noResults,
rowCount,
skinIcons,
stickyGroupHeader,
virtual,
onScroll
} = this.props;
const {
activeEmoji,
activeGroup,
activeSkinTone,
commonEmojis,
context,
groupedEmojis,
scrollToGroup,
searchQuery
} = this.state;
const skinTones = disableSkinTones ? null : /*#__PURE__*/React.createElement(SkinTonePalette, {
key: "skin-tones",
activeSkinTone: activeSkinTone,
icons: skinIcons,
onSelect: this.handleSelectSkinTone
});
const components = {
emojis: /*#__PURE__*/React.createElement(EmojiList, _extends({
key: "emojis"
}, virtual, {
activeEmoji: activeEmoji,
activeGroup: activeGroup,
clearIcon: clearIcon,
columnCount: columnCount,
commonMode: commonMode,
groupedEmojis: groupedEmojis,
hideGroupHeaders: hideGroupHeaders,
noResults: noResults,
rowCount: rowCount,
scrollToGroup: scrollToGroup,
skinTonePalette: displayOrder.includes('skin-tones') ? null : skinTones,
stickyGroupHeader: stickyGroupHeader,
onClear: this.handleClear,
onEnterEmoji: this.handleEnterEmoji,
onLeaveEmoji: this.handleLeaveEmoji,
onScroll: onScroll,
onScrollGroup: this.handleScrollGroup,
onSelectEmoji: this.handleSelectEmoji
})),
groups: disableGroups ? null : /*#__PURE__*/React.createElement(GroupTabs, {
key: "groups",
activeGroup: activeGroup,
commonEmojis: commonEmojis,
commonMode: commonMode,
icons: groupIcons,
onSelect: this.handleSelectGroup
}),
preview: disablePreview ? null : /*#__PURE__*/React.createElement(PreviewBar, {
key: "preview",
emoji: activeEmoji,
hideEmoticon: hideEmoticon,
hideShortcodes: hideShortcodes,
noPreview: noPreview
}),
search: disableSearch ? null : /*#__PURE__*/React.createElement(SearchBar, {
key: "search" // eslint-disable-next-line jsx-a11y/no-autofocus
,
autoFocus: autoFocus,
searchQuery: searchQuery,
onChange: this.handleSearch,
onKeyUp: this.handleKeyUp
}),
'skin-tones': skinTones
};
return /*#__PURE__*/React.createElement(Context.Provider, {
value: context
}, /*#__PURE__*/React.createElement("div", {
className: context.classNames.picker
}, displayOrder.map(key => components[key])));
}
}
_defineProperty(InternalPicker, "defaultProps", {
allowList: [],
autoFocus: false,
blockList: [],
classNames: {},
clearIcon: null,
columnCount: 10,
commonMode: COMMON_MODE_RECENT,
defaultGroup: GROUP_KEY_COMMONLY_USED,
defaultSkinTone: SKIN_KEY_NONE,
disableCommonlyUsed: false,
disableGroups: false,
disablePreview: false,
disableSearch: false,
disableSkinTones: false,
displayOrder: ['preview', 'emojis', 'groups', 'search'],
emojiPadding: 0,
groupIcons: {},
hideEmoticon: false,
hideGroupHeaders: false,
hideShortcodes: false,
maxCommonlyUsed: 30,
maxEmojiVersion: MAX_EMOJI_VERSION,
messages: {},
noPreview: null,
noResults: null,
onHoverEmoji() {},
onScroll() {},
onScrollGroup() {},
onSearch() {},
onSelectEmoji() {},
onSelectGroup() {},
onSelectSkinTone() {},
rowCount: 8,
skinIcons: {},
stickyGroupHeader: false,
virtual: {}
});
function EmojiPicker({
compact,
locale,
shortcodes,
throwErrors,
version,
...props
}) {
const [emojis, source, data] = useEmojiData({
compact,
locale,
shortcodes,
throwErrors,
version
});
if (emojis.length === 0) {
return null;
}
return /*#__PURE__*/React.createElement(InternalPicker, _extends({}, props, {
emojiData: data,
emojis: emojis,
emojiSource: source
}));
}
export { COMMON_MODE_FREQUENT, COMMON_MODE_RECENT, CONTEXT_CLASSNAMES, CONTEXT_MESSAGES, EmojiPicker, GROUPS, GROUP_ICONS, GROUP_KEY_COMMONLY_USED, GROUP_KEY_NONE, GROUP_KEY_SEARCH_RESULTS, GROUP_KEY_VARIATIONS, KEY_COMMONLY_USED, KEY_SKIN_TONE, SCROLL_BUFFER, SCROLL_DEBOUNCE, SEARCH_THROTTLE, SKIN_COLORS, SKIN_KEY_NONE, SKIN_TONES };
//# sourceMappingURL=index.js.map