Files
remark42/frontend/app/components/comment-form/text-expander.tsx
T
Konstantin KrivleniaandUmputun 17be003652 #378 Emoji suggestions (#546)
* #378 add emoji suggestion

* #378 use lazy load for nodeEmoji

* #378 apply remark styles only for light theme

* #378 add dark theme

* #378 fix test

* #378 use permanent class names

*  #378 flip if else for readability
2020-01-13 18:26:32 -06:00

99 lines
3.4 KiB
TypeScript

/** @jsx createElement */
import { createElement, Fragment, render, FunctionalComponent } from 'preact';
import { StaticStore } from '@app/common/static_store';
import { useEffect, useRef } from 'preact/hooks';
import '@github/text-expander-element';
import styles from './text-expander.module.pcss';
import cx from 'classnames';
import { Theme } from '@app/common/types';
import useTheme from '@app/hooks/useTheme';
type Emoji = {
key: string;
emoji: string;
};
function SuggestionList({ items, theme }: { items: Array<Emoji>; theme: Theme }) {
const isDarkTheme = theme === `dark`;
const suggesterClass = cx(styles.suggester, { [styles.suggesterDark]: isDarkTheme });
const suggesterItemClass = cx(styles.suggesterItem, { [styles.suggesterItemDark]: isDarkTheme });
return (
<ul className={suggesterClass}>
{items.map(({ key, emoji }: Emoji) => {
return (
// eslint-disable-next-line jsx-a11y/role-has-required-aria-props
<li role="option" className={suggesterItemClass} data-value={key}>
<span className={styles.emojiResult}>{emoji}</span> {key}
</li>
);
})}
</ul>
);
}
function searchEmoji(key: string, text: string, theme: Theme) {
return import(/* webpackChunkName: "node-emoji" */ `node-emoji`)
.then(nodeEmoji => {
if (key === ':') {
const emojiList = nodeEmoji.search(text);
if (emojiList.length === 0) {
return Promise.resolve({ matched: false });
}
const fragment = document.createDocumentFragment();
render(<SuggestionList theme={theme} items={emojiList.slice(0, 5)} />, fragment);
return Promise.resolve({ matched: true, fragment: fragment.firstChild });
}
return Promise.resolve({ matched: false });
})
.catch(() => Promise.resolve({ matched: false }));
}
type ChangeListerEvent = Event & {
detail: {
key: string;
text: string;
provide(value: Promise<{ matched: boolean }>): void;
};
};
type ValueListerEvent = Event & {
detail: {
key: string;
value: string;
item: HTMLLIElement;
};
};
export const TextExpander: FunctionalComponent = ({ children }) => {
const expanderRef = useRef<HTMLElement>();
const theme = useTheme();
useEffect(() => {
if (expanderRef.current) {
const expander = expanderRef.current;
expander.setAttribute(`keys`, ':');
const textExpanderChangeLister: EventListener = (event: Event) => {
const { provide, key, text } = (event as ChangeListerEvent).detail;
provide(searchEmoji(key, text, theme));
};
const textExpanderValueListener = (event: Event) => {
const { key, item } = (event as ValueListerEvent).detail;
if (key === ':') {
(event as ValueListerEvent).detail.value = `:${item.dataset.value}:`;
}
};
expander.addEventListener('text-expander-change', textExpanderChangeLister);
expander.addEventListener('text-expander-value', textExpanderValueListener);
return () => {
expander.removeEventListener('text-expander-change', textExpanderChangeLister);
expander.removeEventListener('text-expander-value', textExpanderValueListener);
};
}
return () => {};
}, [theme]);
if (StaticStore.config.emoji_enabled) {
return <text-expander ref={expanderRef}>{children}</text-expander>;
}
return <Fragment>{children}</Fragment>;
};