initial commit
This commit is contained in:
195
src/app/organisms/emoji-board/EmojiBoard.jsx
Normal file
195
src/app/organisms/emoji-board/EmojiBoard.jsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './EmojiBoard.scss';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
import { emojiGroups, searchEmoji } from './emoji';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
import IconButton from '../../atoms/button/IconButton';
|
||||
import Input from '../../atoms/input/Input';
|
||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||
|
||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
|
||||
import CupIC from '../../../../public/res/ic/outlined/cup.svg';
|
||||
import BallIC from '../../../../public/res/ic/outlined/ball.svg';
|
||||
import PhotoIC from '../../../../public/res/ic/outlined/photo.svg';
|
||||
import BulbIC from '../../../../public/res/ic/outlined/bulb.svg';
|
||||
import PeaceIC from '../../../../public/res/ic/outlined/peace.svg';
|
||||
import FlagIC from '../../../../public/res/ic/outlined/flag.svg';
|
||||
|
||||
const viewEvent = new EventEmitter();
|
||||
|
||||
function EmojiGroup({ name, emojis }) {
|
||||
function getEmojiBoard() {
|
||||
const ROW_EMOJIS_COUNT = 7;
|
||||
const emojiRows = [];
|
||||
const totalEmojis = emojis.length;
|
||||
|
||||
for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
|
||||
const emojiRow = [];
|
||||
for (let c = r; c < r + ROW_EMOJIS_COUNT; c += 1) {
|
||||
const emojiIndex = r + c;
|
||||
if (emojiIndex >= totalEmojis) break;
|
||||
const emoji = emojis[emojiIndex];
|
||||
emojiRow.push(
|
||||
<span key={emojiIndex}>
|
||||
{
|
||||
parse(twemoji.parse(
|
||||
emoji.unicode,
|
||||
{
|
||||
attributes: () => ({
|
||||
unicode: emoji.unicode,
|
||||
shortcodes: emoji.shortcodes?.toString(),
|
||||
}),
|
||||
},
|
||||
))
|
||||
}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
emojiRows.push(<div key={r} className="emoji-row">{emojiRow}</div>);
|
||||
}
|
||||
return emojiRows;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="emoji-group">
|
||||
<Text className="emoji-group__header" variant="b2">{name}</Text>
|
||||
<div className="emoji-set">{getEmojiBoard()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
EmojiGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
emojis: PropTypes.arrayOf(PropTypes.shape({
|
||||
length: PropTypes.number,
|
||||
unicode: PropTypes.string,
|
||||
shortcodes: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
})).isRequired,
|
||||
};
|
||||
|
||||
function SearchedEmoji() {
|
||||
const [searchedEmojis, setSearchedEmojis] = useState([]);
|
||||
|
||||
function handleSearchEmoji(term) {
|
||||
if (term.trim() === '') {
|
||||
setSearchedEmojis([]);
|
||||
return;
|
||||
}
|
||||
setSearchedEmojis(searchEmoji(term));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
viewEvent.on('search-emoji', handleSearchEmoji);
|
||||
return () => {
|
||||
viewEvent.removeListener('search-emoji', handleSearchEmoji);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return searchedEmojis.length !== 0 && <EmojiGroup key="-1" name="Search results" emojis={searchedEmojis} />;
|
||||
}
|
||||
|
||||
function EmojiBoard({ onSelect }) {
|
||||
const searchRef = useRef(null);
|
||||
const scrollEmojisRef = useRef(null);
|
||||
|
||||
function isTargetNotEmoji(target) {
|
||||
return target.classList.contains('emoji') === false;
|
||||
}
|
||||
function getEmojiDataFromTarget(target) {
|
||||
const unicode = target.getAttribute('unicode');
|
||||
let shortcodes = target.getAttribute('shortcodes');
|
||||
if (typeof shortcodes === 'undefined') shortcodes = undefined;
|
||||
else shortcodes = shortcodes.split(',');
|
||||
return { unicode, shortcodes };
|
||||
}
|
||||
|
||||
function selectEmoji(e) {
|
||||
if (isTargetNotEmoji(e.target)) return;
|
||||
|
||||
const emoji = e.target;
|
||||
onSelect(getEmojiDataFromTarget(emoji));
|
||||
}
|
||||
|
||||
function hoverEmoji(e) {
|
||||
if (isTargetNotEmoji(e.target)) return;
|
||||
|
||||
const emoji = e.target;
|
||||
const { shortcodes } = getEmojiDataFromTarget(emoji);
|
||||
|
||||
if (typeof shortcodes === 'undefined') {
|
||||
searchRef.current.placeholder = 'Search';
|
||||
return;
|
||||
}
|
||||
if (searchRef.current.placeholder === shortcodes[0]) return;
|
||||
searchRef.current.setAttribute('placeholder', `:${shortcodes[0]}:`);
|
||||
}
|
||||
|
||||
function handleSearchChange(e) {
|
||||
const term = e.target.value;
|
||||
setTimeout(() => {
|
||||
if (e.target.value !== term) return;
|
||||
viewEvent.emit('search-emoji', term);
|
||||
scrollEmojisRef.current.scrollTop = 0;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function openGroup(groupOrder) {
|
||||
let tabIndex = groupOrder;
|
||||
const $emojiContent = scrollEmojisRef.current.firstElementChild;
|
||||
const groupCount = $emojiContent.childElementCount;
|
||||
if (groupCount > emojiGroups.length) tabIndex += groupCount - emojiGroups.length;
|
||||
$emojiContent.children[tabIndex].scrollIntoView();
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="emoji-board" className="emoji-board">
|
||||
<div className="emoji-board__content">
|
||||
<div className="emoji-board__emojis">
|
||||
<ScrollView ref={scrollEmojisRef} autoHide>
|
||||
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
|
||||
<SearchedEmoji />
|
||||
{
|
||||
emojiGroups.map((group) => (
|
||||
<EmojiGroup key={group.name} name={group.name} emojis={group.emojis} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div className="emoji-board__search">
|
||||
<RawIcon size="small" src={SearchIC} />
|
||||
<Input onChange={handleSearchChange} forwardRef={searchRef} placeholder="Search" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="emoji-board__nav">
|
||||
<IconButton onClick={() => openGroup(0)} src={EmojiIC} tooltip="Smileys" tooltipPlacement="right" />
|
||||
<IconButton onClick={() => openGroup(1)} src={DogIC} tooltip="Animals" tooltipPlacement="right" />
|
||||
<IconButton onClick={() => openGroup(2)} src={CupIC} tooltip="Food" tooltipPlacement="right" />
|
||||
<IconButton onClick={() => openGroup(3)} src={BallIC} tooltip="Activity" tooltipPlacement="right" />
|
||||
<IconButton onClick={() => openGroup(4)} src={PhotoIC} tooltip="Travel" tooltipPlacement="right" />
|
||||
<IconButton onClick={() => openGroup(5)} src={BulbIC} tooltip="Objects" tooltipPlacement="right" />
|
||||
<IconButton onClick={() => openGroup(6)} src={PeaceIC} tooltip="Symbols" tooltipPlacement="right" />
|
||||
<IconButton onClick={() => openGroup(7)} src={FlagIC} tooltip="Flags" tooltipPlacement="right" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EmojiBoard.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default EmojiBoard;
|
||||
89
src/app/organisms/emoji-board/EmojiBoard.scss
Normal file
89
src/app/organisms/emoji-board/EmojiBoard.scss
Normal file
@@ -0,0 +1,89 @@
|
||||
.emoji-board-flexBoxV {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.emoji-board-flexItem {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.emoji-board {
|
||||
display: flex;
|
||||
|
||||
&__content {
|
||||
@extend .emoji-board-flexItem;
|
||||
@extend .emoji-board-flexBoxV;
|
||||
height: 360px;
|
||||
}
|
||||
&__nav {
|
||||
@extend .emoji-board-flexBoxV;
|
||||
|
||||
padding: 4px 6px;
|
||||
background-color: var(--bg-surface-low);
|
||||
border-left: 1px solid var(--bg-surface-border);
|
||||
[dir=rtl] & {
|
||||
border-left: none;
|
||||
border-right: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
|
||||
& > .ic-btn-surface {
|
||||
margin: calc(var(--sp-ultra-tight) / 2) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.emoji-board__emojis {
|
||||
@extend .emoji-board-flexItem;
|
||||
}
|
||||
.emoji-board__search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: calc(var(--sp-ultra-tight) / 2) var(--sp-normal);
|
||||
|
||||
& .input-container {
|
||||
@extend .emoji-board-flexItem;
|
||||
& .input {
|
||||
min-width: 100%;
|
||||
width: 0;
|
||||
background-color: transparent;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-group {
|
||||
--emoji-padding: 6px;
|
||||
position: relative;
|
||||
margin-bottom: var(--sp-normal);
|
||||
|
||||
&__header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
background-color: var(--bg-surface);
|
||||
|
||||
padding: var(--sp-tight) var(--sp-normal);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
& .emoji-set {
|
||||
margin: 0 calc(var(--sp-normal) - var(--emoji-padding));
|
||||
margin-right: calc(var(--sp-extra-tight) - var(--emoji-padding));
|
||||
[dir=rtl] & {
|
||||
margin-right: calc(var(--sp-normal) - var(--emoji-padding));
|
||||
margin-left: calc(var(--sp-extra-tight) - var(--emoji-padding));
|
||||
}
|
||||
}
|
||||
& .emoji {
|
||||
width: 38px;
|
||||
padding: var(--emoji-padding);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--bg-surface-hover);
|
||||
border-radius: var(--bo-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/app/organisms/emoji-board/emoji.js
Normal file
76
src/app/organisms/emoji-board/emoji.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import emojisData from 'emojibase-data/en/compact.json';
|
||||
import shortcodes from 'emojibase-data/en/shortcodes/joypixels.json';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const emojiGroups = [{
|
||||
name: 'Smileys & people',
|
||||
order: 0,
|
||||
emojis: [],
|
||||
}, {
|
||||
name: 'Animals & nature',
|
||||
order: 1,
|
||||
emojis: [],
|
||||
}, {
|
||||
name: 'Food & drinks',
|
||||
order: 2,
|
||||
emojis: [],
|
||||
}, {
|
||||
name: 'Activity',
|
||||
order: 3,
|
||||
emojis: [],
|
||||
}, {
|
||||
name: 'Travel & places',
|
||||
order: 4,
|
||||
emojis: [],
|
||||
}, {
|
||||
name: 'Objects',
|
||||
order: 5,
|
||||
emojis: [],
|
||||
}, {
|
||||
name: 'Symbols',
|
||||
order: 6,
|
||||
emojis: [],
|
||||
}, {
|
||||
name: 'Flags',
|
||||
order: 7,
|
||||
emojis: [],
|
||||
}];
|
||||
Object.freeze(emojiGroups);
|
||||
|
||||
function addEmoji(emoji, order) {
|
||||
emojiGroups[order].emojis.push(emoji);
|
||||
}
|
||||
function addToGroup(emoji) {
|
||||
if (emoji.group === 0 || emoji.group === 1) addEmoji(emoji, 0);
|
||||
else if (emoji.group === 3) addEmoji(emoji, 1);
|
||||
else if (emoji.group === 4) addEmoji(emoji, 2);
|
||||
else if (emoji.group === 6) addEmoji(emoji, 3);
|
||||
else if (emoji.group === 5) addEmoji(emoji, 4);
|
||||
else if (emoji.group === 7) addEmoji(emoji, 5);
|
||||
else if (emoji.group === 8) addEmoji(emoji, 6);
|
||||
else if (emoji.group === 9) addEmoji(emoji, 7);
|
||||
}
|
||||
|
||||
const emojis = [];
|
||||
emojisData.forEach((emoji) => {
|
||||
const em = { ...emoji, shortcodes: shortcodes[emoji.hexcode] };
|
||||
addToGroup(em);
|
||||
emojis.push(em);
|
||||
});
|
||||
|
||||
function searchEmoji(term) {
|
||||
const options = {
|
||||
includeScore: true,
|
||||
keys: ['shortcodes', 'annotation', 'tags'],
|
||||
threshold: '0.3',
|
||||
};
|
||||
const fuse = new Fuse(emojis, options);
|
||||
|
||||
let result = fuse.search(term);
|
||||
if (result.length > 20) result = result.slice(0, 20);
|
||||
return result.map((finding) => finding.item);
|
||||
}
|
||||
|
||||
export {
|
||||
emojis, emojiGroups, searchEmoji,
|
||||
};
|
||||
Reference in New Issue
Block a user