Skip to content

Commit

Permalink
feat: add emoji picker
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisVega08 committed Jun 4, 2021
1 parent 2df5430 commit 637894b
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 7 deletions.
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "React Chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
1 change: 1 addition & 0 deletions assets/icon-smiley.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"classnames": "^2.2.6",
"date-fns": "^2.11.1",
"emoji-mart": "^3.0.1",
"markdown-it": "^8.4.1",
"markdown-it-link-attributes": "^2.1.0",
"markdown-it-sanitizer": "^0.4.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useState, useEffect } from 'react';
import { Picker } from 'emoji-mart';

import { AnyFunction } from '../../../../../../../../utils/types';

const emoji = require('../../../../../../../../../assets/icon-smiley.svg') as string;

import './style.scss';

type Props = {
onSelectEmoji: AnyFunction
}

function EmojiPicker({ onSelectEmoji } : Props) {
const [pickerStatus, setPickerState] = useState(false)
const togglePicker = () => {
setPickerState(prevPickerStatus => !prevPickerStatus)
}

useEffect(() => {
function onKeyup(e) {
if (e.key === "Escape" && !pickerStatus) togglePicker()
}
window.addEventListener('keyup', onKeyup);
return () => window.removeEventListener('keyup', onKeyup);
}, [])


return (
<>
{pickerStatus && (
<Picker
emoji='point_up'
title='Pick your emoji…'
style={{ position: 'absolute', bottom: '100px', left: '0', width: '100%' }}
onSelect={onSelectEmoji}
/>
)}
<button className='rcw-picker-btn' type="submit" onClick={togglePicker}>
<img src={emoji} className="rcw-picker-icon" alt="" />
</button>
</>
)
}

export default EmojiPicker;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@import 'variables/colors';
@import "~emoji-mart/css/emoji-mart.css";

.rcw-picker-container {
// position: relative;

}

.rcw-picker-btn { // Create mixin
background: $grey-2;
border: 0;
cursor: pointer;

.rcw-piker-icon {
height: 25px;
}
}

// Library style

// .emoji-mart-category-list {
// display: flex;
// flex-wrap: wrap;
// list-style: none;
// background-color: transparent;
// }

// .emoji-mart-anchors {
// display: flex;
// }

// .emoji-mart .emoji-mart-light {
// width: 355px;
// position: absolute;
// top: 0;
// left: 0;
// height: 400px;
// }

.emoji-mart-preview {
display: none;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import EmojiPicker from './components/EmojiPicker'
import cn from 'classnames';

import { GlobalState } from 'src/store/types';

import { getCaretIndex, isFirefox, updateCaret, insertNodeAtCaret, getSelection } from '../../../../../../utils/contentEditable'
const send = require('../../../../../../../assets/send_button.svg') as string;
const brRegex = /<br>/g;

import './style.scss';

Expand All @@ -19,36 +22,92 @@ type Props = {

function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInputChange, buttonAlt }: Props) {
const showChat = useSelector((state: GlobalState) => state.behavior.showChat);
const inputRef = useRef<HTMLSpanElement>(null);
const inputRef = useRef<HTMLDivElement>(null!);
const [enter, setEnter]= useState(false)
const [firefox, setFirefox] = useState(false);
// @ts-ignore
useEffect(() => { if (showChat && autofocus) inputRef.current?.focus(); }, [showChat]);
useEffect(() => { setFirefox(isFirefox())}, [])

const handlerOnChange = (event) => {
onTextInputChange && onTextInputChange(event)
}

const handlerSendMessage = () => {
const { current } = inputRef
if(current?.innerHTML) {
sendMessage(current.innerText);
current.innerHTML = ''
const el = inputRef.current;
if(el.innerHTML) {
sendMessage(el.innerText);
el.innerHTML = ''
}
}


const handlerOnSelectEmoji = (emoji) => {
const el = inputRef.current;
const { start, end } = getSelection(el)
if(el.innerHTML) {
const firstPart = el.innerHTML.substring(0, start);
const secondPart = el.innerHTML.substring(end);
el.innerHTML = (`${firstPart}${emoji.native}${secondPart}`)
} else {
el.innerHTML = emoji.native
}
updateCaret(el, start, emoji.native.length)
}

const handlerOnKeyPress = (event) => {
const el = inputRef.current;

if(event.charCode == 13 && !event.shiftKey) {
event.preventDefault()
handlerSendMessage();
}
if(event.charCode === 13 && event.shiftKey) {
event.preventDefault()
insertNodeAtCaret(el);
setEnter(true)
}
}

const handlerOnKeyUp = (event) => {
const el = inputRef.current;
if(!el) return true;
// Conditions need for firefox
if(firefox && event.key === 'Backspace') {
if(el.innerHTML.length === 1 && enter) {
el.innerHTML = ''
setEnter(false)
}
else if(brRegex.test(el.innerHTML)){
el.innerHTML = el.innerHTML.replace(brRegex, '')
}
}
}


const handlerOnKeyDown= (event) => {
const el = inputRef.current;

if( event.key === 'Backspace' && el){
const caretPosition = getCaretIndex(inputRef.current)
const character = el?.innerHTML.charAt(caretPosition - 1)
if(character === "\n") {
event.preventDefault();
event.stopPropagation()
el.innerHTML = (el.innerHTML.substring(0, caretPosition - 1) + el.innerHTML.substring(caretPosition))
updateCaret(el, caretPosition, -1)
}
}
}

return (
<div className="rcw-sender">
<EmojiPicker onSelectEmoji={handlerOnSelectEmoji}/>
<div className={cn('rcw-new-message', {
'rcw-message-disable': disabledInput,
})
}>
<span
<div
spellCheck
className="rcw-input"
role="textbox"
Expand All @@ -57,7 +116,10 @@ function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInpu
placeholder={placeholder}
onInput={handlerOnChange}
onKeyPress={handlerOnKeyPress}
onKeyUp={handlerOnKeyUp}
onKeyDown={handlerOnKeyDown}
/>

</div>
<button type="submit" className="rcw-send" onClick={handlerSendMessage}>
<img src={send} className="rcw-send-icon" alt={buttonAlt} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@
.rcw-input {
display: block;
height: 100%;
line-height: 20px;
max-height: 75px;
overflow-y: auto;
user-select: text;
white-space: pre-wrap;
word-wrap: break-word;


&:focus-visible {
outline: none;
Expand Down
66 changes: 66 additions & 0 deletions src/utils/contentEditable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export const getCaretIndex = (el) => {
let position = 0;
const selection = window.getSelection()!;
if (selection.rangeCount !== 0) {
const range = window.getSelection()!.getRangeAt(0)
const preCaretRange = range.cloneRange()
preCaretRange.selectNodeContents(el);
preCaretRange.setEnd(range.endContainer, range.endOffset);
position = preCaretRange.toString().length;
}
return position;
}

export const getSelection = (el) => {
const range = window.getSelection()!.getRangeAt(0);
const preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(el)
preSelectionRange.setEnd(range.startContainer, range.startOffset);

const start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
}
export const isFirefox = () => navigator.userAgent.search("Firefox") > 0;

export const updateCaret = (el, caret, offset) => {
const range = document.createRange();
const selection = window.getSelection()!;
range.setStart(el.childNodes[0], caret+offset);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
el.focus();
}

export const insertNodeAtCaret = (el) => {
const position = getCaretIndex(el)
let characterToEnter = '\n\n';
let prevChar, char = '';
if (position > 0) { // Change this
prevChar = el.innerHTML.charAt(position - 1);
char = el.innerHTML.charAt(position);
const newLines = el.innerHTML.match(/\n/g)
if(
prevChar === char ||
(prevChar === '\n' && char === '') ||
(isFirefox() && newLines?.length > 0)
) {
characterToEnter = '\n'
}
}
const selection = window.getSelection()!;
const node = document.createTextNode(characterToEnter);
const range = selection.getRangeAt(0);
range.collapse(false);
range.insertNode(node);
const cloneRange = range.cloneRange();
cloneRange.selectNodeContents(node);
cloneRange.collapse(false);
selection.removeAllRanges();
selection.addRange(cloneRange);
el.innerHTML = el.innerHTML.replace(/<br>/g, '')
updateCaret(el, position, 1)
}

0 comments on commit 637894b

Please sign in to comment.