2020-10-30 20:34:04 +00:00
|
|
|
// Copyright 2019-2020 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2019-06-27 20:35:21 +00:00
|
|
|
import * as React from 'react';
|
2020-10-21 16:53:32 +00:00
|
|
|
|
|
|
|
import Delta from 'quill-delta';
|
|
|
|
import ReactQuill from 'react-quill';
|
2019-06-27 20:35:21 +00:00
|
|
|
import classNames from 'classnames';
|
|
|
|
import emojiRegex from 'emoji-regex';
|
2020-10-21 16:53:32 +00:00
|
|
|
import { Manager, Reference } from 'react-popper';
|
2020-11-02 22:07:52 +00:00
|
|
|
import Quill, { KeyboardStatic, RangeStatic } from 'quill';
|
2020-10-21 16:53:32 +00:00
|
|
|
import Op from 'quill-delta/dist/Op';
|
|
|
|
|
|
|
|
import { EmojiBlot, EmojiCompletion } from '../quill/emoji';
|
2019-06-27 20:35:21 +00:00
|
|
|
import { LocalizerType } from '../types/Util';
|
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
import { EmojiPickDataType } from './emoji/EmojiPicker';
|
|
|
|
import { convertShortName } from './emoji/lib';
|
|
|
|
import { matchEmojiBlot, matchEmojiImage } from '../quill/matchImage';
|
|
|
|
|
|
|
|
Quill.register('formats/emoji', EmojiBlot);
|
|
|
|
Quill.register('modules/emojiCompletion', EmojiCompletion);
|
|
|
|
|
|
|
|
const Block = Quill.import('blots/block');
|
|
|
|
Block.tagName = 'DIV';
|
|
|
|
Quill.register(Block, true);
|
|
|
|
|
|
|
|
declare module 'quill' {
|
|
|
|
interface Quill {
|
|
|
|
// in-code reference missing in @types
|
|
|
|
scrollingContainer: HTMLElement;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface KeyboardStatic {
|
|
|
|
// in-code reference missing in @types
|
|
|
|
bindings: Record<string | number, Array<unknown>>;
|
|
|
|
}
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
declare module 'react-quill' {
|
|
|
|
// `react-quill` uses a different but compatible version of Delta
|
|
|
|
// tell it to use the type definition from the `quill-delta` library
|
|
|
|
type DeltaStatic = Delta;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface HistoryStatic {
|
|
|
|
undo(): void;
|
|
|
|
clear(): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface InputApi {
|
|
|
|
focus: () => void;
|
|
|
|
insertEmoji: (e: EmojiPickDataType) => void;
|
|
|
|
reset: () => void;
|
|
|
|
resetEmojiResults: () => void;
|
|
|
|
submit: () => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface Props {
|
2019-06-27 20:35:21 +00:00
|
|
|
readonly i18n: LocalizerType;
|
|
|
|
readonly disabled?: boolean;
|
2019-08-06 19:18:37 +00:00
|
|
|
readonly large?: boolean;
|
2019-06-27 20:35:21 +00:00
|
|
|
readonly inputApi?: React.MutableRefObject<InputApi | undefined>;
|
|
|
|
readonly skinTone?: EmojiPickDataType['skinTone'];
|
2019-08-07 00:40:25 +00:00
|
|
|
readonly startingText?: string;
|
2019-06-27 20:35:21 +00:00
|
|
|
onDirtyChange?(dirty: boolean): unknown;
|
2020-10-21 16:53:32 +00:00
|
|
|
onEditorStateChange?(messageText: string, caretLocation?: number): unknown;
|
2019-09-13 21:54:19 +00:00
|
|
|
onTextTooLong(): unknown;
|
2019-06-27 20:35:21 +00:00
|
|
|
onPickEmoji(o: EmojiPickDataType): unknown;
|
|
|
|
onSubmit(message: string): unknown;
|
2020-07-01 01:59:38 +00:00
|
|
|
getQuotedMessage(): unknown;
|
|
|
|
clearQuotedMessage(): unknown;
|
2019-06-27 20:35:21 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const MAX_LENGTH = 64 * 1024;
|
2019-08-07 00:40:25 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
export const CompositionInput: React.ComponentType<Props> = props => {
|
|
|
|
const {
|
|
|
|
i18n,
|
|
|
|
disabled,
|
|
|
|
large,
|
|
|
|
inputApi,
|
|
|
|
onPickEmoji,
|
|
|
|
onSubmit,
|
|
|
|
skinTone,
|
|
|
|
startingText,
|
|
|
|
} = props;
|
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const [emojiCompletionElement, setEmojiCompletionElement] = React.useState<
|
|
|
|
JSX.Element
|
|
|
|
>();
|
2020-11-02 22:07:52 +00:00
|
|
|
const [
|
|
|
|
lastSelectionRange,
|
|
|
|
setLastSelectionRange,
|
|
|
|
] = React.useState<RangeStatic | null>(null);
|
2020-10-21 16:53:32 +00:00
|
|
|
|
|
|
|
const emojiCompletionRef = React.useRef<EmojiCompletion>();
|
|
|
|
const quillRef = React.useRef<Quill>();
|
|
|
|
const scrollerRef = React.useRef<HTMLDivElement>(null);
|
2020-10-22 23:33:53 +00:00
|
|
|
const propsRef = React.useRef<Props>(props);
|
2020-10-21 16:53:32 +00:00
|
|
|
|
|
|
|
const generateDelta = (text: string): Delta => {
|
|
|
|
const re = emojiRegex();
|
|
|
|
const ops: Array<Op> = [];
|
|
|
|
|
|
|
|
let index = 0;
|
|
|
|
|
|
|
|
let match: RegExpExecArray | null;
|
|
|
|
// eslint-disable-next-line no-cond-assign
|
|
|
|
while ((match = re.exec(text))) {
|
|
|
|
const [emoji] = match;
|
|
|
|
ops.push({ insert: text.slice(index, match.index) });
|
|
|
|
ops.push({ insert: { emoji } });
|
|
|
|
index = match.index + emoji.length;
|
|
|
|
}
|
2019-08-22 20:42:03 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
ops.push({ insert: text.slice(index, text.length) });
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
return new Delta(ops);
|
|
|
|
};
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const getText = (): string => {
|
|
|
|
const quill = quillRef.current;
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill === undefined) {
|
|
|
|
return '';
|
|
|
|
}
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const contents = quill.getContents();
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (contents === undefined) {
|
|
|
|
return '';
|
|
|
|
}
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const { ops } = contents;
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (ops === undefined) {
|
|
|
|
return '';
|
|
|
|
}
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const text = ops.reduce((acc, { insert }) => {
|
|
|
|
if (typeof insert === 'string') {
|
|
|
|
return acc + insert;
|
2020-09-12 00:46:52 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (insert.emoji) {
|
|
|
|
return acc + insert.emoji;
|
2020-09-12 00:46:52 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
return acc;
|
|
|
|
}, '');
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
return text.trim();
|
|
|
|
};
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const focus = () => {
|
|
|
|
const quill = quillRef.current;
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
quill.focus();
|
|
|
|
};
|
2020-09-12 00:46:52 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const insertEmoji = (e: EmojiPickDataType) => {
|
|
|
|
const quill = quillRef.current;
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const range = quill.getSelection();
|
|
|
|
|
2020-11-02 22:07:52 +00:00
|
|
|
const insertionRange = range || lastSelectionRange;
|
|
|
|
if (insertionRange === null) {
|
2020-10-21 16:53:32 +00:00
|
|
|
return;
|
2020-01-08 17:44:54 +00:00
|
|
|
}
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const emoji = convertShortName(e.shortName, e.skinTone);
|
|
|
|
|
|
|
|
const delta = new Delta()
|
2020-11-02 22:07:52 +00:00
|
|
|
.retain(insertionRange.index)
|
|
|
|
.delete(insertionRange.length)
|
2020-10-21 16:53:32 +00:00
|
|
|
.insert({ emoji });
|
|
|
|
|
|
|
|
quill.updateContents(delta, 'user');
|
2020-11-02 22:07:52 +00:00
|
|
|
quill.setSelection(insertionRange.index + 1, 0, 'user');
|
2020-10-21 16:53:32 +00:00
|
|
|
};
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const reset = () => {
|
|
|
|
const quill = quillRef.current;
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill === undefined) {
|
|
|
|
return;
|
2020-01-08 17:44:54 +00:00
|
|
|
}
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
quill.setText('');
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const historyModule: HistoryStatic = quill.getModule('history');
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (historyModule === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
historyModule.clear();
|
|
|
|
};
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const resetEmojiResults = () => {
|
|
|
|
const emojiCompletion = emojiCompletionRef.current;
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (emojiCompletion === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
2019-09-13 21:54:19 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
emojiCompletion.reset();
|
|
|
|
};
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const submit = () => {
|
|
|
|
const quill = quillRef.current;
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
const text = getText();
|
2020-10-23 00:01:15 +00:00
|
|
|
onSubmit(text.trim());
|
2020-10-21 16:53:32 +00:00
|
|
|
};
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (inputApi) {
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
inputApi.current = {
|
|
|
|
focus,
|
|
|
|
insertEmoji,
|
|
|
|
reset,
|
|
|
|
resetEmojiResults,
|
|
|
|
submit,
|
|
|
|
};
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
React.useEffect(() => {
|
|
|
|
propsRef.current = props;
|
|
|
|
}, [props]);
|
|
|
|
|
|
|
|
const onShortKeyEnter = () => {
|
|
|
|
submit();
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
const onEnter = () => {
|
2020-10-21 16:53:32 +00:00
|
|
|
const quill = quillRef.current;
|
|
|
|
const emojiCompletion = emojiCompletionRef.current;
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (emojiCompletion === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (emojiCompletion.results.length) {
|
|
|
|
emojiCompletion.completeEmoji();
|
|
|
|
return false;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
if (propsRef.current.large) {
|
2020-10-21 16:53:32 +00:00
|
|
|
return true;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
submit();
|
2019-12-17 18:52:36 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
return false;
|
2020-10-22 23:33:53 +00:00
|
|
|
};
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
const onTab = () => {
|
2020-10-21 16:53:32 +00:00
|
|
|
const quill = quillRef.current;
|
|
|
|
const emojiCompletion = emojiCompletionRef.current;
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2019-08-06 19:18:37 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (emojiCompletion === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (emojiCompletion.results.length) {
|
|
|
|
emojiCompletion.completeEmoji();
|
|
|
|
return false;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
return true;
|
2020-10-22 23:33:53 +00:00
|
|
|
};
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
const onEscape = () => {
|
2020-10-21 16:53:32 +00:00
|
|
|
const quill = quillRef.current;
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill === undefined) {
|
|
|
|
return false;
|
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
const emojiCompletion = emojiCompletionRef.current;
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (emojiCompletion) {
|
|
|
|
if (emojiCompletion.results.length) {
|
|
|
|
emojiCompletion.reset();
|
|
|
|
return false;
|
2019-08-14 17:32:38 +00:00
|
|
|
}
|
2020-10-21 16:53:32 +00:00
|
|
|
}
|
2019-08-14 17:32:38 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
if (propsRef.current.getQuotedMessage()) {
|
|
|
|
propsRef.current.clearQuotedMessage();
|
2020-10-21 16:53:32 +00:00
|
|
|
return false;
|
|
|
|
}
|
2019-08-14 17:32:38 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
return true;
|
2020-10-22 23:33:53 +00:00
|
|
|
};
|
2019-11-15 02:12:54 +00:00
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
const onChange = () => {
|
2020-10-21 16:53:32 +00:00
|
|
|
const text = getText();
|
|
|
|
const quill = quillRef.current;
|
2019-11-15 02:12:54 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (quill !== undefined) {
|
|
|
|
const historyModule: HistoryStatic = quill.getModule('history');
|
2019-12-03 20:02:50 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
if (text.length > MAX_LENGTH) {
|
|
|
|
historyModule.undo();
|
2020-10-22 23:33:53 +00:00
|
|
|
propsRef.current.onTextTooLong();
|
2020-10-21 16:53:32 +00:00
|
|
|
return;
|
2019-12-03 20:02:50 +00:00
|
|
|
}
|
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
if (propsRef.current.onEditorStateChange) {
|
2020-10-21 16:53:32 +00:00
|
|
|
const selection = quill.getSelection();
|
2020-10-22 23:33:53 +00:00
|
|
|
propsRef.current.onEditorStateChange(
|
|
|
|
text,
|
|
|
|
selection ? selection.index : undefined
|
|
|
|
);
|
2020-10-21 16:53:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-22 23:33:53 +00:00
|
|
|
if (propsRef.current.onDirtyChange) {
|
|
|
|
propsRef.current.onDirtyChange(text.length > 0);
|
2020-10-21 16:53:32 +00:00
|
|
|
}
|
2020-10-22 23:33:53 +00:00
|
|
|
};
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-01-08 17:44:54 +00:00
|
|
|
React.useEffect(() => {
|
2020-10-21 16:53:32 +00:00
|
|
|
const quill = quillRef.current;
|
|
|
|
|
|
|
|
if (quill === undefined) {
|
|
|
|
return;
|
2020-01-08 17:44:54 +00:00
|
|
|
}
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
quill.enable(!disabled);
|
|
|
|
quill.focus();
|
|
|
|
}, [disabled]);
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
const emojiCompletion = emojiCompletionRef.current;
|
|
|
|
|
|
|
|
if (emojiCompletion === undefined || skinTone === undefined) {
|
|
|
|
return;
|
2020-01-08 17:44:54 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
emojiCompletion.options.skinTone = skinTone;
|
|
|
|
}, [skinTone]);
|
2019-06-27 20:35:21 +00:00
|
|
|
|
2020-10-21 16:53:32 +00:00
|
|
|
React.useEffect(
|
|
|
|
() => () => {
|
|
|
|
const emojiCompletion = emojiCompletionRef.current;
|
|
|
|
|
|
|
|
if (emojiCompletion === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
emojiCompletion.destroy();
|
|
|
|
},
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
|
|
|
|
const reactQuill = React.useMemo(
|
|
|
|
() => {
|
|
|
|
const delta = generateDelta(startingText || '');
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ReactQuill
|
|
|
|
className="module-composition-input__quill"
|
|
|
|
onChange={onChange}
|
|
|
|
defaultValue={delta}
|
|
|
|
modules={{
|
|
|
|
toolbar: false,
|
|
|
|
clipboard: {
|
|
|
|
matchers: [
|
|
|
|
['IMG', matchEmojiImage],
|
|
|
|
['SPAN', matchEmojiBlot],
|
|
|
|
],
|
|
|
|
},
|
|
|
|
keyboard: {
|
|
|
|
bindings: {
|
|
|
|
onEnter: { key: 13, handler: onEnter }, // 13 = Enter
|
|
|
|
onShortKeyEnter: {
|
|
|
|
key: 13, // 13 = Enter
|
|
|
|
shortKey: true,
|
2020-10-22 23:33:53 +00:00
|
|
|
handler: onShortKeyEnter,
|
2020-10-21 16:53:32 +00:00
|
|
|
},
|
|
|
|
onEscape: { key: 27, handler: onEscape }, // 27 = Escape
|
|
|
|
},
|
|
|
|
},
|
|
|
|
emojiCompletion: {
|
|
|
|
setEmojiPickerElement: setEmojiCompletionElement,
|
|
|
|
onPickEmoji,
|
|
|
|
skinTone,
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
formats={['emoji']}
|
|
|
|
placeholder={i18n('sendMessage')}
|
|
|
|
readOnly={disabled}
|
|
|
|
ref={element => {
|
|
|
|
if (element) {
|
|
|
|
const quill = element.getEditor();
|
|
|
|
const keyboard = quill.getModule('keyboard') as KeyboardStatic;
|
|
|
|
|
|
|
|
// force the tab handler to be prepended, otherwise it won't be
|
|
|
|
// executed: https://github.com/quilljs/quill/issues/1967
|
|
|
|
keyboard.bindings[9].unshift({ key: 9, handler: onTab }); // 9 = Tab
|
|
|
|
// also, remove the default \t insertion binding
|
|
|
|
keyboard.bindings[9].pop();
|
|
|
|
|
|
|
|
// When loading a multi-line message out of a draft, the cursor
|
|
|
|
// position needs to be pushed to the end of the input manually.
|
|
|
|
quill.once('editor-change', () => {
|
|
|
|
const scroller = scrollerRef.current;
|
|
|
|
|
|
|
|
if (scroller !== null) {
|
|
|
|
quill.scrollingContainer = scroller;
|
|
|
|
}
|
|
|
|
|
|
|
|
quill.setSelection(quill.getLength(), 0);
|
|
|
|
});
|
|
|
|
|
2020-11-02 22:07:52 +00:00
|
|
|
quill.on(
|
|
|
|
'selection-change',
|
|
|
|
(newRange: RangeStatic, oldRange: RangeStatic) => {
|
|
|
|
// If we lose focus, store the last edit point for emoji insertion
|
|
|
|
if (newRange === null) {
|
|
|
|
setLastSelectionRange(oldRange);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2020-10-21 16:53:32 +00:00
|
|
|
quillRef.current = quill;
|
|
|
|
emojiCompletionRef.current = quill.getModule('emojiCompletion');
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
// quill shouldn't re-render, all changes should take place exclusively
|
|
|
|
// through mutating the quill state directly instead of through props
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
[]
|
|
|
|
);
|
2019-06-27 20:35:21 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Manager>
|
|
|
|
<Reference>
|
2020-10-21 16:53:32 +00:00
|
|
|
{({ ref }) => (
|
|
|
|
<div className="module-composition-input__input" ref={ref}>
|
|
|
|
<div
|
|
|
|
ref={scrollerRef}
|
|
|
|
className={classNames(
|
|
|
|
'module-composition-input__input__scroller',
|
|
|
|
large
|
|
|
|
? 'module-composition-input__input__scroller--large'
|
|
|
|
: null
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{reactQuill}
|
|
|
|
{emojiCompletionElement}
|
|
|
|
</div>
|
|
|
|
</div>
|
2019-06-27 20:35:21 +00:00
|
|
|
)}
|
|
|
|
</Reference>
|
|
|
|
</Manager>
|
|
|
|
);
|
|
|
|
};
|