Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

emoji proof of concept #638

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@babel/preset-typescript": "^7.13.0",
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"@react-hook/merged-ref": "^1.3.0",
"@storybook/addon-docs": "^6.1.20",
"@storybook/addon-knobs": "^6.1.20",
"@storybook/addon-storysource": "^6.1.20",
Expand All @@ -55,6 +56,7 @@
"@testing-library/react": "^11.2.5",
"@testing-library/react-hooks": "^5.0.3",
"@testing-library/user-event": "^12.7.3",
"@types/emoji-mart": "^3.0.4",
"@types/faker": "^5.1.7",
"@types/is-hotkey": "^0.1.2",
"@types/jest": "^26.0.20",
Expand All @@ -68,7 +70,6 @@
"@types/styled-components": "^5.1.7",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"@react-hook/merged-ref": "^1.3.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
Expand Down Expand Up @@ -130,10 +131,11 @@
"zustand": "^3.3.3"
},
"dependencies": {
"prismjs": "^1.23.0",
"@styled-icons/boxicons-regular": "^10.23.0",
"@styled-icons/foundation": "^10.28.0",
"@styled-icons/material": "^10.28.0",
"@styled-icons/foundation": "^10.28.0"
"emoji-mart": "^3.0.1",
"prismjs": "^1.23.0"
},
"resolutions": {
"eslint-plugin-prettier": "3.3.1",
Expand Down
1 change: 1 addition & 0 deletions stories/examples/combobox/useComboboxStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { IComboboxItem } from './components/Combobox.types';
export enum ComboboxKey {
TAG = 'tag',
SLASH_COMMAND = 'slash_command',
EMOJI = 'emoji',
}

export type ComboboxState = {
Expand Down
161 changes: 161 additions & 0 deletions stories/examples/emoji.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React, { useCallback, useMemo } from 'react';
import {
createHistoryPlugin,
createReactPlugin,
createSlatePluginsOptions,
getPointBefore,
getText,
isCollapsed,
OnChange,
SlatePlugin,
SlatePlugins,
useStoreEditor,
} from '@udecode/slate-plugins';
import { BaseEmoji, emojiIndex } from 'emoji-mart';
import { Range, Transforms } from 'slate';
import { initialValueCombobox } from '../config/initialValues';
import { editableProps } from '../config/pluginOptions';
import { useComboboxControls } from './combobox/hooks/useComboboxControls';
import { useComboboxOnKeyDown } from './combobox/hooks/useComboboxOnKeyDown';
import { useComboboxIsOpen } from './combobox/selectors/useComboboxIsOpen';
import { useComboboxStore } from './combobox/useComboboxStore';
import { EmojiCombobox } from './emoji/components/EmojiCombobox';
import { useEmojiOnChange } from './emoji/hooks/useEmojiOnChange';
import { useEmojiOnSelectItem } from './emoji/hooks/useEmojiOnSelectItem';

const id = 'Examples/Emoji';

export default {
title: id,
};

const useEmojiPlugin = (): SlatePlugin => {
return {
withOverrides: (editor) => {
const { insertText } = editor;
editor.insertText = (text) => {
if (!isCollapsed(editor.selection)) {
return insertText(text);
}

const selection = editor.selection as Range;

const startMarkup = ':';
const endMarkup = ':';

if (!text.endsWith(endMarkup)) {
return insertText(text);
}

const endMarkupPointBefore = selection.anchor;

const startMarkupPointBefore = getPointBefore(
editor,
selection.anchor,
{
matchString: startMarkup,
skipInvalid: true,
}
);

if (!startMarkupPointBefore) return insertText(text);

const markupRange: Range = {
anchor: startMarkupPointBefore,
focus: endMarkupPointBefore,
};

const markupText = getText(editor, markupRange);
// remove start markup from the txt
// i.e. :safety_pin => safety_pin
const emojiName = markupText.slice(startMarkup.length);

if (emojiName in emojiIndex.emojis) {
Transforms.select(editor, markupRange);

let emoji = emojiIndex.emojis[emojiName];
// if the emoji has skin variants the index returns an object indexed
// by the numbers 1-6. for now opt for 1 (generic)
if (!('id' in emoji)) {
emoji = emoji['1'];
}
Transforms.insertText(editor, (emoji as BaseEmoji).native);
Transforms.collapse(editor, { edge: 'end' });
return false;
}

return insertText(text);
};

return editor;
},
};
};

// Handle multiple combobox
const useComboboxOnChange = (): OnChange => {
const editor = useStoreEditor(id)!;

const emojiOnChange = useEmojiOnChange(editor);
const isOpen = useComboboxIsOpen();
const closeMenu = useComboboxStore((state) => state.closeMenu);

return useCallback(
() => () => {
let changed: boolean | undefined = false;
changed = emojiOnChange();

if (changed) return;

if (!changed && isOpen) {
closeMenu();
}
},
[closeMenu, isOpen, emojiOnChange]
);
};

// Handle multiple combobox
const ComboboxContainer = () => {
useComboboxControls();

return <EmojiCombobox />;
};

const options = createSlatePluginsOptions();

export const Example = () => {
const comboboxOnChange = useComboboxOnChange();

const emojiOnSelect = useEmojiOnSelectItem();

// Handle multiple combobox
const comboboxOnKeyDown = useComboboxOnKeyDown({
onSelectItem: emojiOnSelect,
});

const plugins: SlatePlugin[] = useMemo(
() => [
createReactPlugin(),
createHistoryPlugin(),
useEmojiPlugin(),
{
onChange: comboboxOnChange,
onKeyDown: comboboxOnKeyDown,
},
],
[comboboxOnChange, comboboxOnKeyDown]
);

return (
<SlatePlugins
id={id}
plugins={plugins}
options={options}
editableProps={editableProps}
initialValue={initialValueCombobox}
>
<ComboboxContainer />
</SlatePlugins>
);
};
49 changes: 49 additions & 0 deletions stories/examples/emoji/components/EmojiCombobox.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import styled, { css } from 'styled-components';

// const classNames = {
// root: 'slate-TagCombobox',
// tagItem: 'slate-TagCombobox-tagItem',
// tagItemHighlighted: 'slate-TagCombobox-tagItemHighlighted',
// };

export const ComboboxRoot = styled.ul<{ isOpen: boolean }>`
${({ isOpen }) =>
isOpen &&
css`
top: -9999px;
left: -9999px;
position: absolute;
padding: 0;
margin: 0;
z-index: 11;
background: white;
width: 300px;
border-radius: 0 0 2px 2px;
box-shadow: rgba(0, 0, 0, 0.133) 0 3.2px 7.2px 0,
rgba(0, 0, 0, 0.11) 0 0.6px 1.8px 0;
`}
`;

export const ComboboxItem = styled.div<{ highlighted: boolean }>`
display: flex;
align-items: center;
font-size: 14px;
font-weight: 400;
padding: 0 8px;
// padding: 1px 3px;
border-radius: 0;
// borderRadius: 3px;
min-height: 36px;
// lineHeight: "20px";
// overflowWrap: "break-word";
user-select: none;
color: rgb(32, 31, 30);
background: ${({ highlighted }) =>
!highlighted ? 'transparent' : 'rgb(237, 235, 233)'};
cursor: pointer;

:hover {
background-color: ${({ highlighted }) =>
!highlighted ? 'rgb(243, 242, 241)' : 'rgb(237, 235, 233)'};
}
`;
23 changes: 23 additions & 0 deletions stories/examples/emoji/components/EmojiCombobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import { Combobox } from '../../combobox/components/Combobox';
import { ComboboxKey, useComboboxStore } from '../../combobox/useComboboxStore';
import { useEmojiOnSelectItem } from '../hooks/useEmojiOnSelectItem';
import { EmojiComboboxItem } from './EmojiComboboxItem';

export const EmojiComboBoxComponent = () => {
const onSelectItem = useEmojiOnSelectItem();

return (
<Combobox onSelectItem={onSelectItem} onRenderItem={EmojiComboboxItem} />
);
};

export const EmojiCombobox = () => {
const key = useComboboxStore((state) => state.key);

return (
<div style={key !== ComboboxKey.EMOJI ? { display: 'none' } : {}}>
<EmojiComboBoxComponent />
</div>
);
};
5 changes: 5 additions & 0 deletions stories/examples/emoji/components/EmojiComboboxItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ComboboxItemProps } from '../../combobox/components/Combobox.types';

export const EmojiComboboxItem = ({ item }: ComboboxItemProps) => {
return item.text;
};
46 changes: 46 additions & 0 deletions stories/examples/emoji/hooks/useEmojiOnChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useCallback } from 'react';
import { TEditor } from '@udecode/slate-plugins-core';
import { BaseEmoji, emojiIndex } from 'emoji-mart';
import shallow from 'zustand/shallow';
import { IComboboxItem } from '../../combobox/components/Combobox.types';
import { useComboboxOnChange } from '../../combobox/hooks/useComboboxOnChange';
import { ComboboxKey, useComboboxStore } from '../../combobox/useComboboxStore';

export const useEmojiOnChange = (editor: TEditor) => {
const comboboxOnChange = useComboboxOnChange({
editor,
key: ComboboxKey.EMOJI,
trigger: ':',
});
const { maxSuggestions, setItems } = useComboboxStore(
// eslint-disable-next-line @typescript-eslint/no-shadow
({ maxSuggestions, setItems }) => ({
maxSuggestions,
setItems,
}),
shallow
);

return useCallback(() => {
const res = comboboxOnChange();
if (!res) return false;

const { search } = res;

const items: IComboboxItem[] = ((emojiIndex.search(search.toLowerCase()) ??
[]) as BaseEmoji[]) // cast to base emoji to access native type
// custom emojis are images but we don't support those yet
.slice(0, maxSuggestions)
.map((item) => {
return {
key: item.id,
text: `${item.native} ${item.colons}`,
data: item,
};
});

setItems(items);

return true;
}, [comboboxOnChange, maxSuggestions, setItems]);
};
47 changes: 47 additions & 0 deletions stories/examples/emoji/hooks/useEmojiOnSelectItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useCallback } from 'react';
import { getBlockAbove, SPEditor } from '@udecode/slate-plugins';
import { BaseEmoji } from 'emoji-mart';
import { Editor, Transforms } from 'slate';
import { IComboboxItem } from '../../combobox/components/Combobox.types';
import { useComboboxIsOpen } from '../../combobox/selectors/useComboboxIsOpen';
import { useComboboxStore } from '../../combobox/useComboboxStore';

/**
* Select the target range, add a tag node and set the target range to null
*/
export const useEmojiOnSelectItem = () => {
const isOpen = useComboboxIsOpen();
const targetRange = useComboboxStore((state) => state.targetRange);
const closeMenu = useComboboxStore((state) => state.closeMenu);

return useCallback(
(editor: SPEditor, item: IComboboxItem) => {
if (isOpen && targetRange) {
const pathAbove = getBlockAbove(editor)?.[1];
const isBlockEnd =
editor.selection &&
pathAbove &&
Editor.isEnd(editor, editor.selection.anchor, pathAbove);

// insert a space to fix the bug
if (isBlockEnd) {
Transforms.insertText(editor, ' ');
}

// select the tag text and insert the tag element
Transforms.select(editor, targetRange);
Transforms.insertText(editor, (item.data as BaseEmoji).native);
// move the selection after the tag element
Transforms.move(editor);

// delete the inserted space
if (isBlockEnd) {
Transforms.delete(editor);
}

return closeMenu();
}
},
[closeMenu, isOpen, targetRange]
);
};
Loading