Skip to content

Commit

Permalink
Merge pull request #59 from DiggesT/feature/emojis
Browse files Browse the repository at this point in the history
Emojis picker plugin
  • Loading branch information
ozanyurtsever authored Oct 2, 2023
2 parents 391ed49 + 085f631 commit 17351eb
Show file tree
Hide file tree
Showing 6 changed files with 16,833 additions and 7 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Coming soon... -->
| hashtagsEnabled | `boolean` | optional | Enables the automatic hashtag highlighting, default is `false` |
| autoLinkEnabled | `boolean` | optional | Enables the automatic link highlighting, default is `false` |
| emojisEnabled | `boolean` | optional | Replaces the emoji combiniations with its corresponding symbol, default is `false` |
| emojiPickerEnabled | `boolean` | optional | Use `:` for search and paste emoji, default is `false` |
| actionsEnabled | `boolean` | optional | Enables the actions toolbar, default is `false` |
| placeholder | `string` | optional | The default content of the editor when it is first loaded |
| listMaxIndent | `number` | optional | The maximum indent capacity of any listed element, the default is `7` |
Expand Down Expand Up @@ -196,6 +197,7 @@ Add your own font sizes.
| CodeHighlightPlugin | :white_check_mark: | Code Block with different languages | Independent |
| CommentPlugin | :x: | | CharacterStylesPopupPlugin |
| EmojisPlugin | :white_check_mark: | A few emojis | Editor.tsx |
| EmojiPickerPlugin | :white_check_mark: | Emoji picker (emoji-list.ts) | Editor.tsx |
| EquationsPlugin | :scissors: | Katex, It's too heavy (cut out) | InsertDropdown.tsx |
| ExcalidrawPlugin | :scissors: | Excalidraw (cut out) | InsertDropdown.tsx |
| HorizontalRulePlugin | :white_check_mark: | Horizontal divider | InsertDropdown.tsx |
Expand Down
4 changes: 4 additions & 0 deletions src/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ import EditorContext from './context/EditorContext';
import { LexicalEditor } from 'lexical';
import { useTranslation } from 'react-i18next';
import DragDropPaste from './plugins/DragDropPastePlugin';
import EmojiPickerPlugin from './plugins/EmojiPickerPlugin';

interface IEditorProps {
children?: ReactNode;
hashtagsEnabled?: boolean;
autoLinkEnabled?: boolean;
emojisEnabled?: boolean;
emojiPickerEnabled?: boolean;
actionsEnabled?: boolean;
placeholder?: string;
listMaxIndent?: number;
Expand All @@ -58,6 +60,7 @@ const Editor = ({
hashtagsEnabled = false,
autoLinkEnabled = false,
emojisEnabled = false,
emojiPickerEnabled = false,
actionsEnabled = false,
listMaxIndent = 7,
placeholder = '',
Expand Down Expand Up @@ -93,6 +96,7 @@ const Editor = ({
<ClearEditorPlugin />
{hashtagsEnabled && <HashtagPlugin />}
{emojisEnabled && <EmojisPlugin />}
{emojiPickerEnabled && <EmojiPickerPlugin />}
<KeywordsPlugin />
<SpeechToTextPlugin />
<DragDropPaste />
Expand Down
4 changes: 4 additions & 0 deletions src/EditorComposer.css
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,10 @@ select.font-family {
cursor: nw-resize;
}

.emoji-menu {
width: 200px;
}

.emoji {
color: transparent;
caret-color: rgb(5, 5, 5);
Expand Down
201 changes: 201 additions & 0 deletions src/plugins/EmojiPickerPlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
LexicalTypeaheadMenuPlugin,
MenuOption,
useBasicTypeaheadTriggerMatch,
} from '@lexical/react/LexicalTypeaheadMenuPlugin';
import {
$createTextNode,
$getSelection,
$isRangeSelection,
TextNode,
} from 'lexical';
import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import * as ReactDOM from 'react-dom';

class EmojiOption extends MenuOption {
title: string;
emoji: string;
keywords: Array<string>;

constructor(
title: string,
emoji: string,
options: {
keywords?: Array<string>;
}
) {
super(title);
this.title = title;
this.emoji = emoji;
this.keywords = options.keywords || [];
}
}
function EmojiMenuItem({
index,
isSelected,
onClick,
onMouseEnter,
option,
}: {
index: number;
isSelected: boolean;
onClick: () => void;
onMouseEnter: () => void;
option: EmojiOption;
}) {
let className = 'item';
if (isSelected) {
className += ' selected';
}
return (
<li
key={option.key}
tabIndex={-1}
className={className}
ref={option.setRefElement}
role="option"
aria-selected={isSelected}
id={'typeahead-item-' + index}
onMouseEnter={onMouseEnter}
onClick={onClick}
>
<span className="text">
{option.emoji} {option.title}
</span>
</li>
);
}

type Emoji = {
emoji: string;
description: string;
category: string;
aliases: Array<string>;
tags: Array<string>;
unicode_version: string;
ios_version: string;
skin_tones?: boolean;
};

const MAX_EMOJI_SUGGESTION_COUNT = 10;

export default function EmojiPickerPlugin() {
const [editor] = useLexicalComposerContext();
const [queryString, setQueryString] = useState<string | null>(null);
const [emojis, setEmojis] = useState<Array<Emoji>>([]);

useEffect(() => {
// @ts-ignore
import('../utils/emoji-list.ts').then((file) => setEmojis(file.default));
}, []);

const emojiOptions = useMemo(
() =>
emojis != null
? emojis.map(
({ emoji, aliases, tags }) =>
new EmojiOption(aliases[0], emoji, {
keywords: [...aliases, ...tags],
})
)
: [],
[emojis]
);

const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(':', {
minLength: 0,
});

const options: Array<EmojiOption> = useMemo(() => {
return emojiOptions
.filter((option: EmojiOption) => {
return queryString != null
? new RegExp(queryString, 'gi').exec(option.title) ||
option.keywords != null
? option.keywords.some((keyword: string) =>
new RegExp(queryString, 'gi').exec(keyword)
)
: false
: emojiOptions;
})
.slice(0, MAX_EMOJI_SUGGESTION_COUNT);
}, [emojiOptions, queryString]);

const onSelectOption = useCallback(
(
selectedOption: EmojiOption,
nodeToRemove: TextNode | null,
closeMenu: () => void
) => {
editor.update(() => {
const selection = $getSelection();

if (!$isRangeSelection(selection) || selectedOption == null) {
return;
}

if (nodeToRemove) {
nodeToRemove.remove();
}

selection.insertNodes([$createTextNode(selectedOption.emoji)]);

closeMenu();
});
},
[editor]
);

return (
<LexicalTypeaheadMenuPlugin
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForTriggerMatch}
options={options}
menuRenderFn={(
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }
) => {
if (anchorElementRef.current == null || options.length === 0) {
return null;
}

return anchorElementRef.current && options.length
? ReactDOM.createPortal(
<div className="typeahead-popover emoji-menu">
<ul>
{options.map((option: EmojiOption, index) => (
<div key={option.key}>
<EmojiMenuItem
index={index}
isSelected={selectedIndex === index}
onClick={() => {
setHighlightedIndex(index);
selectOptionAndCleanUp(option);
}}
onMouseEnter={() => {
setHighlightedIndex(index);
}}
option={option}
/>
</div>
))}
</ul>
</div>,
anchorElementRef.current
)
: null;
}}
/>
);
}
14 changes: 7 additions & 7 deletions src/plugins/EmojisPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
*
*/

import type { LexicalEditor } from 'lexical';
import type {LexicalEditor} from 'lexical';

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { TextNode } from 'lexical';
import { useEffect } from 'react';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {TextNode} from 'lexical';
import {useEffect} from 'react';

import { $createEmojiNode, EmojiNode } from '../nodes/EmojiNode';
import {$createEmojiNode, EmojiNode} from '../nodes/EmojiNode';

const emojis: Map<string, [string, string]> = new Map([
[':)', ['emoji happysmile', '🙂']],
Expand Down Expand Up @@ -51,7 +51,7 @@ function findAndTransformEmoji(node: TextNode): null | TextNode {
}

function textNodeTransform(node: TextNode): void {
let targetNode = node;
let targetNode: TextNode | null = node;

while (targetNode !== null) {
if (!targetNode.isSimpleText()) {
Expand All @@ -72,7 +72,7 @@ function useEmojis(editor: LexicalEditor): void {
}, [editor]);
}

export default function EmojisPlugin(): JSX.Element {
export default function EmojisPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEmojis(editor);
return null;
Expand Down
Loading

0 comments on commit 17351eb

Please sign in to comment.