* #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
This commit is contained in:
committed by
Umputun
parent
3c12fde0f8
commit
17be003652
@@ -15,6 +15,7 @@ import { SubscribeByRSS } from './__subscribe-by-rss';
|
||||
|
||||
import MarkdownToolbar from './markdown-toolbar';
|
||||
import TextareaAutosize from './textarea-autosize';
|
||||
import { TextExpander } from './text-expander';
|
||||
|
||||
let textareaId = 0;
|
||||
|
||||
@@ -144,7 +145,7 @@ export class CommentForm extends Component<Props, State> {
|
||||
}
|
||||
|
||||
send(e: Event) {
|
||||
const text = this.state.text;
|
||||
const text = this.textAreaRef.current ? this.textAreaRef.current.getValue() : this.state.text;
|
||||
const props = this.props;
|
||||
|
||||
if (e) e.preventDefault();
|
||||
@@ -156,7 +157,7 @@ export class CommentForm extends Component<Props, State> {
|
||||
this.setState({ preview: null, text: '' });
|
||||
}
|
||||
|
||||
this.setState({ isDisabled: true, isErrorShown: false });
|
||||
this.setState({ isDisabled: true, isErrorShown: false, text });
|
||||
|
||||
props
|
||||
.onSubmit(text, pageTitle || document.title)
|
||||
@@ -172,11 +173,11 @@ export class CommentForm extends Component<Props, State> {
|
||||
}
|
||||
|
||||
getPreview() {
|
||||
const text = this.state.text;
|
||||
const text = this.textAreaRef.current ? this.textAreaRef.current.getValue() : this.state.text;
|
||||
|
||||
if (!text || !text.trim()) return;
|
||||
|
||||
this.setState({ isErrorShown: false, errorMessage: null });
|
||||
this.setState({ isErrorShown: false, errorMessage: null, text });
|
||||
|
||||
this.props
|
||||
.getPreview(text)
|
||||
@@ -369,21 +370,22 @@ export class CommentForm extends Component<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
<div className="comment-form__field-wrapper">
|
||||
<TextareaAutosize
|
||||
id={this.textareaId}
|
||||
onPaste={this.onPaste}
|
||||
ref={this.textAreaRef}
|
||||
className="comment-form__field"
|
||||
placeholder="Your comment here"
|
||||
value={text}
|
||||
maxLength={maxLength}
|
||||
onInput={this.onInput}
|
||||
onKeyDown={this.onKeyDown}
|
||||
disabled={isDisabled}
|
||||
autofocus={!!props.autofocus}
|
||||
spellcheck={true}
|
||||
/>
|
||||
|
||||
<TextExpander>
|
||||
<TextareaAutosize
|
||||
id={this.textareaId}
|
||||
onPaste={this.onPaste}
|
||||
ref={this.textAreaRef}
|
||||
className="comment-form__field"
|
||||
placeholder="Your comment here"
|
||||
value={text}
|
||||
maxLength={maxLength}
|
||||
onInput={this.onInput}
|
||||
onKeyDown={this.onKeyDown}
|
||||
disabled={isDisabled}
|
||||
autofocus={!!props.autofocus}
|
||||
spellcheck={true}
|
||||
/>
|
||||
</TextExpander>
|
||||
{charactersLeft < 100 && <span className="comment-form__counter">{charactersLeft}</span>}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
.suggester {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
min-width: 180px;
|
||||
padding: 0;
|
||||
margin: 24px 0 0;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #dfe2e5;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 5px rgba(27, 31, 35, 0.15);
|
||||
}
|
||||
|
||||
.suggesterDark {
|
||||
background: #22201c;
|
||||
color: #ddd;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.suggesterItem {
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.suggesterItemDark {
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.suggesterItem:hover,
|
||||
.suggesterItem[aria-selected='true'] {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background-color: rgba(37, 156, 154, 0.6);
|
||||
}
|
||||
|
||||
.suggesterItem:first-child {
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
.suggesterItem:last-child {
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.emojiResult {
|
||||
margin-right: 0;
|
||||
display: inline-block;
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/** @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>;
|
||||
};
|
||||
@@ -58,7 +58,9 @@ export default class TextareaAutosize extends Component<Props> {
|
||||
onRef(node: HTMLTextAreaElement) {
|
||||
this.textareaRef = node;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.textareaRef ? this.textareaRef.value : '';
|
||||
}
|
||||
autoResize() {
|
||||
if (this.textareaRef) {
|
||||
this.textareaRef.style.height = '';
|
||||
|
||||
Reference in New Issue
Block a user