UNPKG

interweave-emoji-picker

Version:

React based emoji picker powered by Interweave and Emojibase.

1,451 lines (1,255 loc) β€’ 42.1 kB
// Bundled with Packemon: https://packemon.dev // Platform: browser, Support: stable, Format: lib 'use strict'; 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; } Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } }); const React = require('react'); const interweaveEmoji = require('interweave-emoji'); const debounce = require('lodash/debounce'); const emojibase = require('emojibase'); const reactWindow = require('react-window'); const chunk = require('lodash/chunk'); const camelCase = require('lodash/camelCase'); const _interopDefault = e => e && e.__esModule ? e : { default: e }; const React__default = /*#__PURE__*/_interopDefault(React); const debounce__default = /*#__PURE__*/_interopDefault(debounce); const chunk__default = /*#__PURE__*/_interopDefault(chunk); const camelCase__default = /*#__PURE__*/_interopDefault(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 = [emojibase.GROUP_KEY_SMILEYS_EMOTION, emojibase.GROUP_KEY_PEOPLE_BODY, emojibase.GROUP_KEY_COMPONENT, // Unused but required for order emojibase.GROUP_KEY_ANIMALS_NATURE, emojibase.GROUP_KEY_FOOD_DRINK, emojibase.GROUP_KEY_TRAVEL_PLACES, emojibase.GROUP_KEY_ACTIVITIES, emojibase.GROUP_KEY_OBJECTS, emojibase.GROUP_KEY_SYMBOLS, emojibase.GROUP_KEY_FLAGS]; const GROUP_ICONS = { [GROUP_KEY_COMMONLY_USED]: 'πŸ•‘', [emojibase.GROUP_KEY_SMILEYS_EMOTION]: 'πŸ˜ƒ', [emojibase.GROUP_KEY_PEOPLE_BODY]: 'πŸ‘', [emojibase.GROUP_KEY_ANIMALS_NATURE]: '🌿', [emojibase.GROUP_KEY_FOOD_DRINK]: '🍎', [emojibase.GROUP_KEY_TRAVEL_PLACES]: 'πŸ—ΊοΈ', [emojibase.GROUP_KEY_ACTIVITIES]: '⚽️', [emojibase.GROUP_KEY_OBJECTS]: 'πŸ“˜', [emojibase.GROUP_KEY_SYMBOLS]: '⛔️', [emojibase.GROUP_KEY_FLAGS]: '🏴' }; const SKIN_KEY_NONE = 'none'; const SKIN_TONES = [SKIN_KEY_NONE, emojibase.SKIN_KEY_LIGHT, emojibase.SKIN_KEY_MEDIUM_LIGHT, emojibase.SKIN_KEY_MEDIUM, emojibase.SKIN_KEY_MEDIUM_DARK, emojibase.SKIN_KEY_DARK]; const SKIN_COLORS = { [SKIN_KEY_NONE]: '#FFCC22', [emojibase.SKIN_KEY_LIGHT]: '#FADCBC', [emojibase.SKIN_KEY_MEDIUM_LIGHT]: '#E0BB95', [emojibase.SKIN_KEY_MEDIUM]: '#BF8F68', [emojibase.SKIN_KEY_MEDIUM_DARK]: '#9B643D', [emojibase.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' : interweaveEmoji.LATEST_DATASET_VERSION; const Context = /*#__PURE__*/React__default.default.createContext({ classNames: CONTEXT_CLASSNAMES, emojiData: interweaveEmoji.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 } = React.useContext(Context); let title = ''; if (group === GROUP_KEY_COMMONLY_USED) { title = messages[camelCase__default.default(commonMode)]; } else { var _emojiData$GROUPS_BY_; const key = camelCase__default.default(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 } = React.useContext(Context); const showClear = clearIcon && (group === GROUP_KEY_COMMONLY_USED || group === GROUP_KEY_VARIATIONS); const showPalette = skinTonePalette && (group === emojibase.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 = React.useCallback(event => { event.preventDefault(); onClear(); }, [onClear]); return /*#__PURE__*/React__default.default.createElement("header", { className: className.join(' ') }, /*#__PURE__*/React__default.default.createElement("span", null, title), showPalette && skinTonePalette, showClear && /*#__PURE__*/React__default.default.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 } = React.useContext(Context); const dimension = emojiPadding + emojiPadding + emojiSize; const className = [classNames.emoji]; if (active) { className.push(classNames.emojiActive); } // Handlers const handleClick = React.useCallback(event => { event.stopPropagation(); onSelect(emoji, event); }, [emoji, onSelect]); const handleEnter = React.useCallback(event => { event.stopPropagation(); onEnter(emoji, event); }, [emoji, onEnter]); const handleLeave = React.useCallback(event => { event.stopPropagation(); onLeave(emoji, event); }, [emoji, onLeave]); return /*#__PURE__*/React__default.default.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__default.default.createElement(interweaveEmoji.Emoji, { 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 } = React.useContext(Context); const row = data[index]; return /*#__PURE__*/React__default.default.createElement("div", { className: classNames.emojisRow, style: style }, Array.isArray(row) ? /*#__PURE__*/React__default.default.createElement("div", { className: classNames.emojisBody }, row.map(emoji => /*#__PURE__*/React__default.default.createElement(Emoji, { key: emoji.hexcode, active: activeEmoji ? activeEmoji.hexcode === emoji.hexcode : false, emoji: emoji, onEnter: onEnterEmoji, onLeave: onLeaveEmoji, onSelect: onSelectEmoji }))) : /*#__PURE__*/React__default.default.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 } = React.useContext(Context); const [rows, setRows] = React.useState([]); const [indices, setIndices] = React.useState({}); const ref = React.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. React.useEffect(() => { const virtualRows = []; const nextIndices = { '': -1 // Handle empty scroll to's }; Object.keys(groupedEmojis).forEach(group => { nextIndices[group] = virtualRows.length; if (group === emojibase.GROUP_KEY_COMPONENT) { return; } if (!hideGroupHeaders) { virtualRows.push(group); } virtualRows.push(...chunk__default.default(groupedEmojis[group].emojis, columnCount)); }); setRows(virtualRows); setIndices(nextIndices); }, [columnCount, groupedEmojis, hideGroupHeaders]); // Scroll to the defined group when all data is available React.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 = React.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__default.default.createElement("div", { className: classNames.noResults }, noResults !== null && noResults !== void 0 ? noResults : messages.noResults); } return /*#__PURE__*/React__default.default.createElement("div", { className: classNames.emojis }, /*#__PURE__*/React__default.default.createElement(reactWindow.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__default.default.createElement(EmojiListRow, _extends({}, rowProps, props))), stickyGroupHeader && activeGroup !== GROUP_KEY_NONE && /*#__PURE__*/React__default.default.createElement(EmojiListHeader, _extends({}, rowProps, { sticky: true, group: activeGroup }))); } function Group({ active, children, commonMode, group, onSelect }) { const { classNames } = React.useContext(Context); const className = [classNames.group]; const title = useGroupMessage(group, commonMode); if (active) { className.push(classNames.groupActive); } const handleClick = React.useCallback(event => { event.stopPropagation(); onSelect(group, event); }, [group, onSelect]); return /*#__PURE__*/React__default.default.createElement("button", { className: className.join(' '), title: title, type: "button", onClick: handleClick }, children); } function GroupTabs({ activeGroup, commonEmojis, commonMode, icons, onSelect }) { const { classNames } = React.useContext(Context); const groups = GROUPS.filter(group => group !== emojibase.GROUP_KEY_COMPONENT); if (commonEmojis.length > 0) { groups.unshift(GROUP_KEY_COMMONLY_USED); } return /*#__PURE__*/React__default.default.createElement("nav", { className: classNames.groups }, /*#__PURE__*/React__default.default.createElement("ul", { className: classNames.groupsList }, groups.map(group => { var _ref, _icons$group; return /*#__PURE__*/React__default.default.createElement("li", { key: group }, /*#__PURE__*/React__default.default.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__default.default(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 } = React.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__default.default.createElement("section", { className: classNames.preview }, preview && /*#__PURE__*/React__default.default.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__default.default.createElement("section", { className: classNames.preview }, /*#__PURE__*/React__default.default.createElement("div", { className: classNames.previewEmoji }, /*#__PURE__*/React__default.default.createElement(interweaveEmoji.Emoji, { enlargeEmoji: true, emojiLargeSize: emojiLargeSize, emojiPath: emojiPath, emojiSource: emojiSource, hexcode: emoji.hexcode })), /*#__PURE__*/React__default.default.createElement("div", { className: classNames.previewContent }, title && /*#__PURE__*/React__default.default.createElement("div", { className: classNames.previewTitle }, title, emoji.skins && emoji.skins.length > 0 && /*#__PURE__*/ // eslint-disable-next-line react/jsx-no-literals React__default.default.createElement("span", { className: classNames.previewShiftMore }, `(+${emoji.skins.length})`)), subtitle.length > 0 && /*#__PURE__*/React__default.default.createElement("div", { className: classNames.previewSubtitle }, subtitle.join(' ')))); } function SearchBar({ autoFocus, searchQuery, onChange, onKeyUp }) { const { classNames, messages } = React.useContext(Context); const ref = React.useRef(null); React.useEffect(() => { if (autoFocus && ref.current) { ref.current.focus(); } }, [autoFocus]); const handleChange = React.useCallback(event => { // Check if were still mounted if (ref.current) { onChange(event.target.value.trim(), event); } }, [onChange]); return /*#__PURE__*/React__default.default.createElement("div", { className: classNames.search }, /*#__PURE__*/React__default.default.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 } = React.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 } = React.useContext(Context); const className = [classNames.skinTone]; const color = SKIN_COLORS[skinTone]; const title = useSkinToneMessage(skinTone); if (active) { className.push(classNames.skinToneActive); } const handleClick = React.useCallback(event => { event.stopPropagation(); onSelect(skinTone, event); }, [skinTone, onSelect]); return /*#__PURE__*/React__default.default.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 } = React.useContext(Context); return /*#__PURE__*/React__default.default.createElement("nav", { className: classNames.skinTones }, /*#__PURE__*/React__default.default.createElement("ul", { className: classNames.skinTonesList }, SKIN_TONES.map(skinTone => { var _ref3, _icons$skinTone; return /*#__PURE__*/React__default.default.createElement("li", { key: skinTone }, /*#__PURE__*/React__default.default.createElement(SkinTone, { active: activeSkinTone === skinTone, skinTone: skinTone, onSelect: onSelect }, (_ref3 = (_icons$skinTone = icons[skinTone]) !== null && _icons$skinTone !== void 0 ? _icons$skinTone : icons[camelCase__default.default(skinTone)]) !== null && _ref3 !== void 0 ? _ref3 : null)); }))); } const SKIN_MODIFIER_PATTERN = /1F3FB|1F3FC|1F3FD|1F3FE|1F3FF/g; class InternalPicker extends React__default.default.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 : emojibase.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__default.default(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 = emojibase.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__default.default.createElement(SkinTonePalette, { key: "skin-tones", activeSkinTone: activeSkinTone, icons: skinIcons, onSelect: this.handleSelectSkinTone }); const components = { emojis: /*#__PURE__*/React__default.default.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__default.default.createElement(GroupTabs, { key: "groups", activeGroup: activeGroup, commonEmojis: commonEmojis, commonMode: commonMode, icons: groupIcons, onSelect: this.handleSelectGroup }), preview: disablePreview ? null : /*#__PURE__*/React__default.default.createElement(PreviewBar, { key: "preview", emoji: activeEmoji, hideEmoticon: hideEmoticon, hideShortcodes: hideShortcodes, noPreview: noPreview }), search: disableSearch ? null : /*#__PURE__*/React__default.default.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__default.default.createElement(Context.Provider, { value: context }, /*#__PURE__*/React__default.default.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: interweaveEmoji.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] = interweaveEmoji.useEmojiData({ compact, locale, shortcodes, throwErrors, version }); if (emojis.length === 0) { return null; } return /*#__PURE__*/React__default.default.createElement(InternalPicker, _extends({}, props, { emojiData: data, emojis: emojis, emojiSource: source })); } Object.defineProperty(exports, 'GROUP_KEY_ACTIVITIES', { enumerable: true, get: () => emojibase.GROUP_KEY_ACTIVITIES }); Object.defineProperty(exports, 'GROUP_KEY_ANIMALS_NATURE', { enumerable: true, get: () => emojibase.GROUP_KEY_ANIMALS_NATURE }); Object.defineProperty(exports, 'GROUP_KEY_COMPONENT', { enumerable: true, get: () => emojibase.GROUP_KEY_COMPONENT }); Object.defineProperty(exports, 'GROUP_KEY_FLAGS', { enumerable: true, get: () => emojibase.GROUP_KEY_FLAGS }); Object.defineProperty(exports, 'GROUP_KEY_FOOD_DRINK', { enumerable: true, get: () => emojibase.GROUP_KEY_FOOD_DRINK }); Object.defineProperty(exports, 'GROUP_KEY_OBJECTS', { enumerable: true, get: () => emojibase.GROUP_KEY_OBJECTS }); Object.defineProperty(exports, 'GROUP_KEY_PEOPLE_BODY', { enumerable: true, get: () => emojibase.GROUP_KEY_PEOPLE_BODY }); Object.defineProperty(exports, 'GROUP_KEY_SMILEYS_EMOTION', { enumerable: true, get: () => emojibase.GROUP_KEY_SMILEYS_EMOTION }); Object.defineProperty(exports, 'GROUP_KEY_SYMBOLS', { enumerable: true, get: () => emojibase.GROUP_KEY_SYMBOLS }); Object.defineProperty(exports, 'GROUP_KEY_TRAVEL_PLACES', { enumerable: true, get: () => emojibase.GROUP_KEY_TRAVEL_PLACES }); Object.defineProperty(exports, 'SKIN_KEY_DARK', { enumerable: true, get: () => emojibase.SKIN_KEY_DARK }); Object.defineProperty(exports, 'SKIN_KEY_LIGHT', { enumerable: true, get: () => emojibase.SKIN_KEY_LIGHT }); Object.defineProperty(exports, 'SKIN_KEY_MEDIUM', { enumerable: true, get: () => emojibase.SKIN_KEY_MEDIUM }); Object.defineProperty(exports, 'SKIN_KEY_MEDIUM_DARK', { enumerable: true, get: () => emojibase.SKIN_KEY_MEDIUM_DARK }); Object.defineProperty(exports, 'SKIN_KEY_MEDIUM_LIGHT', { enumerable: true, get: () => emojibase.SKIN_KEY_MEDIUM_LIGHT }); exports.COMMON_MODE_FREQUENT = COMMON_MODE_FREQUENT; exports.COMMON_MODE_RECENT = COMMON_MODE_RECENT; exports.CONTEXT_CLASSNAMES = CONTEXT_CLASSNAMES; exports.CONTEXT_MESSAGES = CONTEXT_MESSAGES; exports.EmojiPicker = EmojiPicker; exports.GROUPS = GROUPS; exports.GROUP_ICONS = GROUP_ICONS; exports.GROUP_KEY_COMMONLY_USED = GROUP_KEY_COMMONLY_USED; exports.GROUP_KEY_NONE = GROUP_KEY_NONE; exports.GROUP_KEY_SEARCH_RESULTS = GROUP_KEY_SEARCH_RESULTS; exports.GROUP_KEY_VARIATIONS = GROUP_KEY_VARIATIONS; exports.KEY_COMMONLY_USED = KEY_COMMONLY_USED; exports.KEY_SKIN_TONE = KEY_SKIN_TONE; exports.SCROLL_BUFFER = SCROLL_BUFFER; exports.SCROLL_DEBOUNCE = SCROLL_DEBOUNCE; exports.SEARCH_THROTTLE = SEARCH_THROTTLE; exports.SKIN_COLORS = SKIN_COLORS; exports.SKIN_KEY_NONE = SKIN_KEY_NONE; exports.SKIN_TONES = SKIN_TONES; //# sourceMappingURL=index.js.map