Replace Draft with Quill for composition area
Co-authored-by: Sidney Keese <sidney@carbonfive.com>
This commit is contained in:
parent
544995cc21
commit
fbf93374c1
20 changed files with 2933 additions and 1130 deletions
|
@ -494,40 +494,6 @@ Signal Desktop makes use of the following open source projects.
|
|||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## draft-js
|
||||
|
||||
BSD License
|
||||
|
||||
For Draft.js software
|
||||
|
||||
Copyright (c) 2013-present, Facebook, Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name Facebook nor the names of its contributors may be used to
|
||||
endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## emoji-datasource
|
||||
|
||||
The MIT License (MIT)
|
||||
|
@ -1856,6 +1822,38 @@ Signal Desktop makes use of the following open source projects.
|
|||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## parchment
|
||||
|
||||
Copyright (c) 2015, Jason Chen
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## pify
|
||||
|
||||
MIT License
|
||||
|
@ -1918,6 +1916,63 @@ Signal Desktop makes use of the following open source projects.
|
|||
|
||||
License: MIT
|
||||
|
||||
## quill
|
||||
|
||||
Copyright (c) 2014, Jason Chen
|
||||
Copyright (c) 2013, salesforce.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
## quill-delta
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Jason Chen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
## react
|
||||
|
||||
MIT License
|
||||
|
@ -2070,6 +2125,30 @@ Signal Desktop makes use of the following open source projects.
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## react-quill
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020, zenoamaro <zenoamaro@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
## react-redux
|
||||
|
||||
License: MIT
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
>
|
||||
<title>Signal</title>
|
||||
<link href="node_modules/sanitize.css/sanitize.css" rel="stylesheet" type="text/css" />
|
||||
<link href="node_modules/react-quill/dist/quill.core.css" rel="stylesheet" type="text/css" />
|
||||
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
|
||||
<link href="node_modules/draft-js/dist/Draft.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<!--
|
||||
When making changes to these templates, be sure to update test/index.html as well
|
||||
|
|
|
@ -76,7 +76,6 @@
|
|||
"copy-text-to-clipboard": "2.1.0",
|
||||
"curve25519-n": "https://github.com/scottnonnenberg-signal/node-curve25519.git#3e94f60bc54b2426476520d8d1a0aa835c25f5cc",
|
||||
"dashdash": "1.14.1",
|
||||
"draft-js": "0.10.5",
|
||||
"emoji-datasource": "5.0.1",
|
||||
"emoji-datasource-apple": "5.0.1",
|
||||
"emoji-regex": "8.0.0",
|
||||
|
@ -107,10 +106,13 @@
|
|||
"p-map": "2.1.0",
|
||||
"p-props": "4.0.0",
|
||||
"p-queue": "6.2.1",
|
||||
"parchment": "1.1.4",
|
||||
"pify": "3.0.0",
|
||||
"popper.js": "1.15.0",
|
||||
"protobufjs": "6.8.6",
|
||||
"proxy-agent": "3.1.1",
|
||||
"quill": "1.3.7",
|
||||
"quill-delta": "4.0.1",
|
||||
"react": "16.8.3",
|
||||
"react-blurhash": "0.1.2",
|
||||
"react-contextmenu": "2.11.0",
|
||||
|
@ -119,6 +121,7 @@
|
|||
"react-hot-loader": "4.12.11",
|
||||
"react-measure": "2.3.0",
|
||||
"react-popper": "1.3.7",
|
||||
"react-quill": "2.0.0-beta.2",
|
||||
"react-redux": "7.1.0",
|
||||
"react-router-dom": "5.0.1",
|
||||
"react-sortable-hoc": "1.9.1",
|
||||
|
@ -162,7 +165,6 @@
|
|||
"@types/classnames": "2.2.3",
|
||||
"@types/config": "0.0.34",
|
||||
"@types/dashdash": "1.14.0",
|
||||
"@types/draft-js": "0.10.32",
|
||||
"@types/filesize": "3.6.0",
|
||||
"@types/fs-extra": "5.0.5",
|
||||
"@types/google-libphonenumber": "7.4.14",
|
||||
|
@ -181,6 +183,7 @@
|
|||
"@types/node-forge": "0.9.5",
|
||||
"@types/normalize-path": "3.0.0",
|
||||
"@types/pify": "3.0.2",
|
||||
"@types/quill": "1.3.10",
|
||||
"@types/react": "16.8.5",
|
||||
"@types/react-dom": "16.8.2",
|
||||
"@types/react-measure": "2.0.5",
|
||||
|
|
|
@ -8652,20 +8652,32 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
// Module: CompositionInput
|
||||
.module-composition-input {
|
||||
&__input {
|
||||
@include font-body-1;
|
||||
&__quill {
|
||||
height: 100%;
|
||||
|
||||
.ql-editor {
|
||||
padding: 0;
|
||||
|
||||
&.ql-blank::before {
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
|
||||
&:placeholder {
|
||||
color: $color-gray-45;
|
||||
// Override Quill styles
|
||||
.ql-container {
|
||||
@include font-body-1;
|
||||
}
|
||||
|
||||
// Override draft.js styles
|
||||
.public-DraftEditorPlaceholder-root {
|
||||
.ql-blank::before {
|
||||
color: $color-gray-45;
|
||||
}
|
||||
|
||||
|
@ -8745,6 +8757,10 @@ button.module-image__border-overlay:focus {
|
|||
border: none;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
@include font-body-2;
|
||||
|
||||
@include light-theme() {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import 'draft-js/dist/Draft.css';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
|
@ -32,7 +31,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
onChooseAttachment: action('onChooseAttachment'),
|
||||
// CompositionInput
|
||||
onSubmit: action('onSubmit'),
|
||||
onEditorSizeChange: action('onEditorSizeChange'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
startingText: overrideProps.startingText || undefined,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import { Editor } from 'draft-js';
|
||||
import { get, noop } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import { EmojiButton, Props as EmojiButtonProps } from './emoji/EmojiButton';
|
||||
|
@ -47,7 +46,6 @@ export type OwnProps = {
|
|||
export type Props = Pick<
|
||||
CompositionInputProps,
|
||||
| 'onSubmit'
|
||||
| 'onEditorSizeChange'
|
||||
| 'onEditorStateChange'
|
||||
| 'onTextTooLong'
|
||||
| 'startingText'
|
||||
|
@ -90,7 +88,6 @@ export const CompositionArea = ({
|
|||
// CompositionInput
|
||||
onSubmit,
|
||||
compositionApi,
|
||||
onEditorSizeChange,
|
||||
onEditorStateChange,
|
||||
onTextTooLong,
|
||||
startingText,
|
||||
|
@ -137,7 +134,6 @@ export const CompositionArea = ({
|
|||
const [micActive, setMicActive] = React.useState(false);
|
||||
const [dirty, setDirty] = React.useState(false);
|
||||
const [large, setLarge] = React.useState(false);
|
||||
const editorRef = React.useRef<Editor>(null);
|
||||
const inputApiRef = React.useRef<InputApi | undefined>();
|
||||
|
||||
const handleForceSend = React.useCallback(() => {
|
||||
|
@ -156,10 +152,10 @@ export const CompositionArea = ({
|
|||
);
|
||||
|
||||
const focusInput = React.useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.focus();
|
||||
if (inputApiRef.current) {
|
||||
inputApiRef.current.focus();
|
||||
}
|
||||
}, [editorRef]);
|
||||
}, [inputApiRef]);
|
||||
|
||||
const withStickers =
|
||||
countStickers({
|
||||
|
@ -413,11 +409,9 @@ export const CompositionArea = ({
|
|||
i18n={i18n}
|
||||
disabled={disabled}
|
||||
large={large}
|
||||
editorRef={editorRef}
|
||||
inputApi={inputApiRef}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onSubmit={handleSubmit}
|
||||
onEditorSizeChange={onEditorSizeChange}
|
||||
onEditorStateChange={onEditorStateChange}
|
||||
onTextTooLong={onTextTooLong}
|
||||
onDirtyChange={setDirty}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import 'draft-js/dist/Draft.css';
|
||||
import 'react-quill/dist/quill.core.css';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
@ -17,7 +17,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
i18n,
|
||||
disabled: boolean('disabled', overrideProps.disabled || false),
|
||||
onSubmit: action('onSubmit'),
|
||||
onEditorSizeChange: action('onEditorSizeChange'),
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
startingText: overrideProps.startingText || undefined,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import 'draft-js/dist/Draft.css';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean, date, select, text } from '@storybook/addon-knobs';
|
||||
|
|
|
@ -56,7 +56,7 @@ export const Emoji = React.memo(
|
|||
style={{ ...style, ...backgroundStyle }}
|
||||
>
|
||||
{inline ? (
|
||||
// When using this component as a draft.js decorator it is very
|
||||
// When using this component as in a CompositionInput it is very
|
||||
// important that these children are the only elements to render
|
||||
children
|
||||
) : (
|
||||
|
|
|
@ -20,7 +20,10 @@ import { dataByCategory, search } from './lib';
|
|||
import { useRestoreFocus } from '../../util/hooks';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type EmojiPickDataType = { skinTone?: number; shortName: string };
|
||||
export type EmojiPickDataType = {
|
||||
skinTone?: number;
|
||||
shortName: string;
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
|
|
|
@ -240,14 +240,14 @@ export function unifiedToEmoji(unified: string): string {
|
|||
.join('');
|
||||
}
|
||||
|
||||
export function convertShortName(
|
||||
export function convertShortNameToData(
|
||||
shortName: string,
|
||||
skinTone: number | SkinToneKey = 0
|
||||
): string {
|
||||
): EmojiData | undefined {
|
||||
const base = dataByShortName[shortName];
|
||||
|
||||
if (!base) {
|
||||
return '';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const toneKey = is.number(skinTone) ? skinTones[skinTone - 1] : skinTone;
|
||||
|
@ -255,11 +255,27 @@ export function convertShortName(
|
|||
if (skinTone && base.skin_variations) {
|
||||
const variation = base.skin_variations[toneKey];
|
||||
if (variation) {
|
||||
return unifiedToEmoji(variation.unified);
|
||||
return {
|
||||
...base,
|
||||
...variation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return unifiedToEmoji(base.unified);
|
||||
return base;
|
||||
}
|
||||
|
||||
export function convertShortName(
|
||||
shortName: string,
|
||||
skinTone: number | SkinToneKey = 0
|
||||
): string {
|
||||
const emojiData = convertShortNameToData(shortName, skinTone);
|
||||
|
||||
if (!emojiData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return unifiedToEmoji(emojiData.unified);
|
||||
}
|
||||
|
||||
export function emojiToImage(emoji: string): string | undefined {
|
||||
|
|
36
ts/quill/emoji/blot.tsx
Normal file
36
ts/quill/emoji/blot.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import Parchment from 'parchment';
|
||||
import Quill from 'quill';
|
||||
import { render } from 'react-dom';
|
||||
|
||||
import { Emoji } from '../../components/emoji/Emoji';
|
||||
|
||||
const Embed: typeof Parchment.Embed = Quill.import('blots/embed');
|
||||
|
||||
export class EmojiBlot extends Embed {
|
||||
static blotName = 'emoji';
|
||||
|
||||
static tagName = 'span';
|
||||
|
||||
static className = 'emoji-blot';
|
||||
|
||||
static create(emoji: string): Node {
|
||||
const node = super.create(undefined) as HTMLElement;
|
||||
node.dataset.emoji = emoji;
|
||||
|
||||
const emojiSpan = document.createElement('span');
|
||||
render(
|
||||
<Emoji emoji={emoji} inline size={20}>
|
||||
{emoji}
|
||||
</Emoji>,
|
||||
emojiSpan
|
||||
);
|
||||
node.appendChild(emojiSpan);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
static value(node: HTMLElement): string | undefined {
|
||||
return node.dataset.emoji;
|
||||
}
|
||||
}
|
340
ts/quill/emoji/completion.tsx
Normal file
340
ts/quill/emoji/completion.tsx
Normal file
|
@ -0,0 +1,340 @@
|
|||
import Quill from 'quill';
|
||||
import Delta from 'quill-delta';
|
||||
import React from 'react';
|
||||
|
||||
import { Popper } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
EmojiData,
|
||||
search,
|
||||
convertShortName,
|
||||
isShortName,
|
||||
convertShortNameToData,
|
||||
} from '../../components/emoji/lib';
|
||||
import { Emoji } from '../../components/emoji/Emoji';
|
||||
import { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
|
||||
|
||||
type UpdatedDelta = Delta;
|
||||
|
||||
declare module 'quill' {
|
||||
// this type is fixed in @types/quill, but our version of react-quill cannot
|
||||
// use the version of quill that has this fix in its typings
|
||||
// doing this manually allows us to use the correct type
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/commit/6090a81c7dbd02b6b917f903a28c6c010b8432ea#diff-bff5e435d15f8f99f733c837e76945bced86bb85e93a75467015cc9b33b48212
|
||||
interface UpdatedKey {
|
||||
key: string | number;
|
||||
shiftKey?: boolean;
|
||||
}
|
||||
|
||||
interface Blot {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
interface Quill {
|
||||
updateContents(delta: UpdatedDelta, source?: Sources): UpdatedDelta;
|
||||
getLeaf(index: number): [Blot, number];
|
||||
}
|
||||
|
||||
interface KeyboardStatic {
|
||||
addBinding(
|
||||
key: UpdatedKey,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callback: (range: RangeStatic, context: any) => void
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
interface EmojiPickerOptions {
|
||||
onPickEmoji: (emoji: EmojiPickDataType) => void;
|
||||
setEmojiPickerElement: (element: JSX.Element | null) => void;
|
||||
skinTone: number;
|
||||
}
|
||||
|
||||
export class EmojiCompletion {
|
||||
results: Array<EmojiData>;
|
||||
|
||||
index: number;
|
||||
|
||||
options: EmojiPickerOptions;
|
||||
|
||||
root: HTMLDivElement;
|
||||
|
||||
quill: Quill;
|
||||
|
||||
constructor(quill: Quill, options: EmojiPickerOptions) {
|
||||
this.results = [];
|
||||
this.index = 0;
|
||||
this.options = options;
|
||||
this.root = document.body.appendChild(document.createElement('div'));
|
||||
this.quill = quill;
|
||||
|
||||
const clearResults = () => {
|
||||
if (this.results.length) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const changeIndex = (by: number) => (): boolean => {
|
||||
if (this.results.length) {
|
||||
this.changeIndex(by);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
this.quill.keyboard.addBinding({ key: 37 }, clearResults); // 37 = Left
|
||||
this.quill.keyboard.addBinding({ key: 38 }, changeIndex(-1)); // 38 = Up
|
||||
this.quill.keyboard.addBinding({ key: 39 }, clearResults); // 39 = Right
|
||||
this.quill.keyboard.addBinding({ key: 40 }, changeIndex(1)); // 40 = Down
|
||||
|
||||
this.quill.on('text-change', this.onTextChange.bind(this));
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.root.remove();
|
||||
}
|
||||
|
||||
changeIndex(by: number): void {
|
||||
this.index = (this.index + by + this.results.length) % this.results.length;
|
||||
this.render();
|
||||
}
|
||||
|
||||
getCurrentLeafTextPartitions(): [string, string] {
|
||||
const range = this.quill.getSelection();
|
||||
|
||||
if (range) {
|
||||
const [blot, blotIndex] = this.quill.getLeaf(range.index);
|
||||
|
||||
if (blot !== undefined && blot.text !== undefined) {
|
||||
const leftLeafText = blot.text.substr(0, blotIndex);
|
||||
const rightLeafText = blot.text.substr(blotIndex);
|
||||
|
||||
return [leftLeafText, rightLeafText];
|
||||
}
|
||||
}
|
||||
|
||||
return ['', ''];
|
||||
}
|
||||
|
||||
onTextChange(): void {
|
||||
const range = this.quill.getSelection();
|
||||
|
||||
if (!range) return;
|
||||
|
||||
const [leftLeafText, rightLeafText] = this.getCurrentLeafTextPartitions();
|
||||
|
||||
const leftTokenTextMatch = /:([-+0-9a-z_]*)(:?)$/.exec(leftLeafText);
|
||||
const rightTokenTextMatch = /^([-+0-9a-z_]*):/.exec(rightLeafText);
|
||||
|
||||
if (!leftTokenTextMatch) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const [, leftTokenText, isSelfClosing] = leftTokenTextMatch;
|
||||
|
||||
if (isSelfClosing) {
|
||||
if (isShortName(leftTokenText)) {
|
||||
const emojiData = convertShortNameToData(
|
||||
leftTokenText,
|
||||
this.options.skinTone
|
||||
);
|
||||
|
||||
if (emojiData) {
|
||||
this.insertEmoji(
|
||||
emojiData,
|
||||
range.index - leftTokenText.length - 2,
|
||||
leftTokenText.length + 2
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (rightTokenTextMatch) {
|
||||
const [, rightTokenText] = rightTokenTextMatch;
|
||||
const tokenText = leftTokenText + rightTokenText;
|
||||
|
||||
if (isShortName(tokenText)) {
|
||||
const emojiData = convertShortNameToData(
|
||||
tokenText,
|
||||
this.options.skinTone
|
||||
);
|
||||
|
||||
if (emojiData) {
|
||||
this.insertEmoji(
|
||||
emojiData,
|
||||
range.index - leftTokenText.length - 1,
|
||||
tokenText.length + 2
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (leftTokenText.length < 2) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const results = search(leftTokenText, 10);
|
||||
|
||||
if (!results.length) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
this.results = results;
|
||||
this.render();
|
||||
}
|
||||
|
||||
completeEmoji(): void {
|
||||
const range = this.quill.getSelection();
|
||||
|
||||
if (range === null) return;
|
||||
|
||||
const emoji = this.results[this.index];
|
||||
const [leafText] = this.getCurrentLeafTextPartitions();
|
||||
|
||||
const tokenTextMatch = /:([-+0-9a-z_]*)(:?)$/.exec(leafText);
|
||||
|
||||
if (tokenTextMatch === null) return;
|
||||
|
||||
const [, tokenText] = tokenTextMatch;
|
||||
|
||||
this.insertEmoji(
|
||||
emoji,
|
||||
range.index - tokenText.length - 1,
|
||||
tokenText.length + 1,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
insertEmoji(
|
||||
emojiData: EmojiData,
|
||||
index: number,
|
||||
range: number,
|
||||
withTrailingSpace = false
|
||||
): void {
|
||||
const emoji = convertShortName(emojiData.short_name, this.options.skinTone);
|
||||
|
||||
const delta = new Delta()
|
||||
.retain(index)
|
||||
.delete(range)
|
||||
.insert({ emoji });
|
||||
|
||||
if (withTrailingSpace) {
|
||||
this.quill.updateContents(delta.insert(' '), 'user');
|
||||
this.quill.setSelection(index + 2, 0, 'user');
|
||||
} else {
|
||||
this.quill.updateContents(delta, 'user');
|
||||
this.quill.setSelection(index + 1, 0, 'user');
|
||||
}
|
||||
|
||||
this.options.onPickEmoji({
|
||||
shortName: emojiData.short_name,
|
||||
skinTone: this.options.skinTone,
|
||||
});
|
||||
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
if (this.results.length) {
|
||||
this.results = [];
|
||||
this.index = 0;
|
||||
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
onUnmount(): void {
|
||||
document.body.removeChild(this.root);
|
||||
}
|
||||
|
||||
render(): void {
|
||||
const { results: emojiResults, index: emojiResultsIndex } = this;
|
||||
|
||||
if (emojiResults.length === 0) {
|
||||
this.options.setEmojiPickerElement(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const element = createPortal(
|
||||
<Popper
|
||||
placement="top"
|
||||
modifiers={{
|
||||
width: {
|
||||
enabled: true,
|
||||
fn: oldData => {
|
||||
const data = oldData;
|
||||
const { width, left } = data.offsets.reference;
|
||||
|
||||
data.styles.width = `${width}px`;
|
||||
data.offsets.popper.width = width;
|
||||
data.offsets.popper.left = left;
|
||||
|
||||
return data;
|
||||
},
|
||||
order: 840,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{({ ref, style }) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className="module-composition-input__emoji-suggestions"
|
||||
style={style}
|
||||
role="listbox"
|
||||
aria-expanded
|
||||
aria-activedescendant={`emoji-result--${
|
||||
emojiResults.length
|
||||
? emojiResults[emojiResultsIndex].short_name
|
||||
: ''
|
||||
}`}
|
||||
tabIndex={0}
|
||||
>
|
||||
{emojiResults.map((emoji, index) => (
|
||||
<button
|
||||
type="button"
|
||||
key={emoji.short_name}
|
||||
id={`emoji-result--${emoji.short_name}`}
|
||||
role="option button"
|
||||
aria-selected={emojiResultsIndex === index}
|
||||
onClick={() => {
|
||||
this.index = index;
|
||||
this.completeEmoji();
|
||||
}}
|
||||
className={classNames(
|
||||
'module-composition-input__emoji-suggestions__row',
|
||||
emojiResultsIndex === index
|
||||
? 'module-composition-input__emoji-suggestions__row--selected'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
<Emoji
|
||||
shortName={emoji.short_name}
|
||||
size={16}
|
||||
skinTone={this.options.skinTone}
|
||||
/>
|
||||
<div className="module-composition-input__emoji-suggestions__row__short-name">
|
||||
:{emoji.short_name}:
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Popper>,
|
||||
this.root
|
||||
);
|
||||
|
||||
this.options.setEmojiPickerElement(element);
|
||||
}
|
||||
}
|
2
ts/quill/emoji/index.tsx
Normal file
2
ts/quill/emoji/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { EmojiBlot } from './blot';
|
||||
export { EmojiCompletion } from './completion';
|
21
ts/quill/matchImage.ts
Normal file
21
ts/quill/matchImage.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Delta from 'quill-delta';
|
||||
|
||||
export const matchEmojiBlot = (node: HTMLElement, delta: Delta): Delta => {
|
||||
if (node.classList.contains('emoji-blot')) {
|
||||
const { emoji } = node.dataset;
|
||||
return new Delta().insert({ emoji });
|
||||
}
|
||||
if (node.classList.contains('module-emoji')) {
|
||||
const emoji = node.innerText.trim();
|
||||
return new Delta().insert({ emoji });
|
||||
}
|
||||
return delta;
|
||||
};
|
||||
|
||||
export const matchEmojiImage = (node: Element): Delta => {
|
||||
if (node.classList.contains('emoji')) {
|
||||
const emoji = node.getAttribute('title');
|
||||
return new Delta().insert({ emoji });
|
||||
}
|
||||
return new Delta();
|
||||
};
|
356
ts/test/quill/emoji/completion_test.tsx
Normal file
356
ts/test/quill/emoji/completion_test.tsx
Normal file
|
@ -0,0 +1,356 @@
|
|||
import { assert } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { EmojiCompletion } from '../../../quill/emoji/completion';
|
||||
import { EmojiData } from '../../../components/emoji/lib';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const globalAsAny = global as any;
|
||||
|
||||
describe('emojiCompletion', () => {
|
||||
let emojiCompletion: EmojiCompletion;
|
||||
const mockOnPickEmoji = sinon.spy();
|
||||
const mockSetEmojiPickerElement = sinon.spy();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let mockQuill: any;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
this.oldDocument = globalAsAny.document;
|
||||
globalAsAny.document = {
|
||||
body: {
|
||||
appendChild: () => null,
|
||||
},
|
||||
createElement: () => null,
|
||||
};
|
||||
|
||||
mockQuill = {
|
||||
getLeaf: sinon.stub(),
|
||||
getSelection: sinon.stub(),
|
||||
keyboard: {
|
||||
addBinding: sinon.stub(),
|
||||
},
|
||||
on: sinon.stub(),
|
||||
setSelection: sinon.stub(),
|
||||
updateContents: sinon.stub(),
|
||||
};
|
||||
const options = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onPickEmoji: mockOnPickEmoji as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
setEmojiPickerElement: mockSetEmojiPickerElement as any,
|
||||
skinTone: 0,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
emojiCompletion = new EmojiCompletion(mockQuill as any, options);
|
||||
|
||||
// Stub rendering to avoid missing DOM until we bring in Enzyme
|
||||
emojiCompletion.render = sinon.stub();
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
mockOnPickEmoji.resetHistory();
|
||||
mockSetEmojiPickerElement.resetHistory();
|
||||
(emojiCompletion.render as sinon.SinonStub).resetHistory();
|
||||
|
||||
if (this.oldDocument === undefined) {
|
||||
delete globalAsAny.document;
|
||||
} else {
|
||||
globalAsAny.document = this.oldDocument;
|
||||
}
|
||||
});
|
||||
|
||||
describe('getCurrentLeafTextPartitions', () => {
|
||||
it('returns left and right text', () => {
|
||||
mockQuill.getSelection.returns({ index: 0, length: 0 });
|
||||
const blot = {
|
||||
text: ':smile:',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 2]);
|
||||
const [
|
||||
leftLeafText,
|
||||
rightLeafText,
|
||||
] = emojiCompletion.getCurrentLeafTextPartitions();
|
||||
assert.equal(leftLeafText, ':s');
|
||||
assert.equal(rightLeafText, 'mile:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onTextChange', () => {
|
||||
let insertEmojiStub: sinon.SinonStub<
|
||||
[EmojiData, number, number, (boolean | undefined)?],
|
||||
void
|
||||
>;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
emojiCompletion.results = [{ short_name: 'joy' } as any];
|
||||
emojiCompletion.index = 5;
|
||||
insertEmojiStub = sinon
|
||||
.stub(emojiCompletion, 'insertEmoji')
|
||||
.callThrough();
|
||||
});
|
||||
|
||||
afterEach(function afterEach() {
|
||||
insertEmojiStub.restore();
|
||||
});
|
||||
|
||||
describe('given an emoji is not starting (no colon)', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 3,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: 'smi',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 3]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('resets the completion', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
assert.equal(emojiCompletion.index, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji is starting but does not have 2 characters', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 2,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: ':s',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 2]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('resets the completion', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
assert.equal(emojiCompletion.index, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji is starting but does not match a short name', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 4,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: ':smy',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 4]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('resets the completion', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
assert.equal(emojiCompletion.index, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji is starting and matches short names', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 4,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text: ':smi',
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 4]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('stores the results and renders', () => {
|
||||
assert.equal(emojiCompletion.results.length, 10);
|
||||
assert.equal((emojiCompletion.render as sinon.SinonStub).called, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji was just completed', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: 7,
|
||||
length: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it matches a short name', () => {
|
||||
const text = ':smile:';
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 7]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, 7);
|
||||
});
|
||||
|
||||
it('resets the completion', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
assert.equal(emojiCompletion.index, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it does not match a short name', () => {
|
||||
const text = ':smyle:';
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, 7]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('resets the completion', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
assert.equal(emojiCompletion.index, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given an emoji was just completed from inside the colons', () => {
|
||||
const validEmoji = ':smile:';
|
||||
const invalidEmoji = ':smyle:';
|
||||
const middleCursorIndex = validEmoji.length - 3;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index: middleCursorIndex,
|
||||
length: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it matches a short name', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text: validEmoji,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, middleCursorIndex]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('inserts the emoji at the current cursor position', () => {
|
||||
const [emoji, index, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile');
|
||||
assert.equal(index, 0);
|
||||
assert.equal(range, validEmoji.length);
|
||||
});
|
||||
|
||||
it('resets the completion', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
assert.equal(emojiCompletion.index, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and given it does not match a short name', () => {
|
||||
beforeEach(function beforeEach() {
|
||||
const blot = {
|
||||
text: invalidEmoji,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, middleCursorIndex]);
|
||||
|
||||
emojiCompletion.onTextChange();
|
||||
});
|
||||
|
||||
it('resets the completion', () => {
|
||||
assert.equal(emojiCompletion.results.length, 0);
|
||||
assert.equal(emojiCompletion.index, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeEmoji', () => {
|
||||
let insertEmojiStub: sinon.SinonStub<
|
||||
[EmojiData, number, number, (boolean | undefined)?],
|
||||
void
|
||||
>;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
emojiCompletion.results = [
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ short_name: 'smile' } as any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ short_name: 'smile_cat' } as any,
|
||||
];
|
||||
emojiCompletion.index = 1;
|
||||
insertEmojiStub = sinon.stub(emojiCompletion, 'insertEmoji');
|
||||
});
|
||||
|
||||
describe('given a valid token', () => {
|
||||
const text = ':smi';
|
||||
const index = text.length;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, index]);
|
||||
|
||||
emojiCompletion.completeEmoji();
|
||||
});
|
||||
|
||||
it('inserts the currently selected emoji at the current cursor position', () => {
|
||||
const [emoji, insertIndex, range] = insertEmojiStub.args[0];
|
||||
|
||||
assert.equal(emoji.short_name, 'smile_cat');
|
||||
assert.equal(insertIndex, 0);
|
||||
assert.equal(range, text.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given a valid token is not present', () => {
|
||||
const text = 'smi';
|
||||
const index = text.length;
|
||||
|
||||
beforeEach(function beforeEach() {
|
||||
mockQuill.getSelection.returns({
|
||||
index,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const blot = {
|
||||
text,
|
||||
};
|
||||
mockQuill.getLeaf.returns([blot, index]);
|
||||
|
||||
emojiCompletion.completeEmoji();
|
||||
});
|
||||
|
||||
it('does not insert anything', () => {
|
||||
assert.equal(insertEmojiStub.called, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -2210,11 +2210,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
}
|
||||
},
|
||||
|
||||
focusMessageFieldAndClearDisabled() {
|
||||
this.compositionApi.current.setDisabled(false);
|
||||
this.focusMessageField();
|
||||
},
|
||||
|
||||
disableMessageField() {
|
||||
this.compositionApi.current.setDisabled(true);
|
||||
},
|
||||
|
@ -2928,7 +2923,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
if (message) {
|
||||
this.quote = await this.model.makeQuote(this.quotedMessage);
|
||||
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
this.enableMessageField();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2993,11 +2988,11 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
this.enableMessageField();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
this.enableMessageField();
|
||||
window.log.error(
|
||||
'sendMessage error:',
|
||||
error && error.stack ? error.stack : error
|
||||
|
@ -3033,7 +3028,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
|
||||
if (ToastView) {
|
||||
this.showToast(ToastView);
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
this.enableMessageField();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3070,7 +3065,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
error && error.stack ? error.stack : error
|
||||
);
|
||||
} finally {
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
this.enableMessageField();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -2164,14 +2164,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
|
||||
integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
|
||||
|
||||
"@types/draft-js@0.10.32":
|
||||
version "0.10.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/draft-js/-/draft-js-0.10.32.tgz#cbfed40c500d9bb1e486813dde73125291e7517d"
|
||||
integrity sha512-x63qkMUVpK8lAdxYQFk54F92F1mcG202qs4t0I+UqsZyCpkRRJlMQ+1v3QBbPcckVlmmdSg7m2PqoAbeXNjCaA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
immutable "^3.8.1"
|
||||
|
||||
"@types/eslint-visitor-keys@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
|
||||
|
@ -2416,6 +2408,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
|
||||
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
|
||||
|
||||
"@types/quill@1.3.10", "@types/quill@^1.3.10":
|
||||
version "1.3.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/quill/-/quill-1.3.10.tgz#dc1f7b6587f7ee94bdf5291bc92289f6f0497613"
|
||||
integrity sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==
|
||||
dependencies:
|
||||
parchment "^1.1.2"
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||
|
@ -4916,6 +4915,11 @@ clone-response@1.0.2, clone-response@^1.0.2:
|
|||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
clone@^2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
@ -6176,15 +6180,6 @@ dotnet-deps-parser@4.10.0:
|
|||
tslib "^1.10.0"
|
||||
xml2js "0.4.23"
|
||||
|
||||
draft-js@0.10.5:
|
||||
version "0.10.5"
|
||||
resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.10.5.tgz#bfa9beb018fe0533dbb08d6675c371a6b08fa742"
|
||||
integrity sha512-LE6jSCV9nkPhfVX2ggcRLA4FKs6zWq9ceuO/88BpXdNCS7mjRTgs0NsV6piUCJX9YxMsB9An33wnkMmU2sD2Zg==
|
||||
dependencies:
|
||||
fbjs "^0.8.15"
|
||||
immutable "~3.7.4"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
dtrace-provider@~0.8:
|
||||
version "0.8.7"
|
||||
resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.7.tgz#dc939b4d3e0620cfe0c1cd803d0d2d7ed04ffd04"
|
||||
|
@ -6967,6 +6962,11 @@ eventemitter2@~0.4.13:
|
|||
version "0.4.14"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab"
|
||||
|
||||
eventemitter3@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
|
||||
integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=
|
||||
|
||||
eventemitter3@^3.1.0:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
|
||||
|
@ -7130,7 +7130,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
|
|||
assign-symbols "^1.0.0"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
extend@3, extend@~3.0.0, extend@~3.0.2:
|
||||
extend@3, extend@^3.0.2, extend@~3.0.0, extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
|
||||
|
@ -7207,6 +7207,11 @@ fast-deep-equal@^3.1.1:
|
|||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-diff@1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154"
|
||||
integrity sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||
|
@ -7275,7 +7280,7 @@ faye-websocket@~0.11.1:
|
|||
dependencies:
|
||||
websocket-driver ">=0.5.1"
|
||||
|
||||
fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.15:
|
||||
fbjs@^0.8.0, fbjs@^0.8.1:
|
||||
version "0.8.17"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
|
||||
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
|
||||
|
@ -8946,16 +8951,6 @@ immer@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/immer/-/immer-2.1.5.tgz#0389947455c5a2c7a47f1e6f415c9d1a62a1ebed"
|
||||
integrity sha512-xyjQyTBYIeiz6jd02Hg12jV+9QISwF1crLcwTlzHpWH4e0ryNWj1kacpTwimK3bJV5NKKXw458G2vpqoB/inFA==
|
||||
|
||||
immutable@^3.8.1:
|
||||
version "3.8.2"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
|
||||
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
|
||||
|
||||
immutable@~3.7.4:
|
||||
version "3.7.6"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
|
||||
integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks=
|
||||
|
||||
import-cwd@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
|
||||
|
@ -12066,6 +12061,11 @@ param-case@2.1.x, param-case@^2.1.1:
|
|||
dependencies:
|
||||
no-case "^2.2.0"
|
||||
|
||||
parchment@1.1.4, parchment@^1.1.2, parchment@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/parchment/-/parchment-1.1.4.tgz#aeded7ab938fe921d4c34bc339ce1168bc2ffde5"
|
||||
integrity sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
|
@ -12894,6 +12894,36 @@ querystringify@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
|
||||
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
|
||||
|
||||
quill-delta@4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-4.0.1.tgz#21c8f166dffc6cd9a12051f964f7574fc237a167"
|
||||
integrity sha512-8MdJmDlsnkGTMP3VVqVb/S95l+X691/xY1iQ3tmDDBWkqUyqh72+tBnaEStxfEa5YqzozGeYWz8j2B3pni5Bew==
|
||||
dependencies:
|
||||
deep-equal "^1.0.1"
|
||||
extend "^3.0.2"
|
||||
fast-diff "1.1.2"
|
||||
|
||||
quill-delta@^3.6.2:
|
||||
version "3.6.3"
|
||||
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-3.6.3.tgz#b19fd2b89412301c60e1ff213d8d860eac0f1032"
|
||||
integrity sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==
|
||||
dependencies:
|
||||
deep-equal "^1.0.1"
|
||||
extend "^3.0.2"
|
||||
fast-diff "1.1.2"
|
||||
|
||||
quill@1.3.7, quill@^1.3.7:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8"
|
||||
integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==
|
||||
dependencies:
|
||||
clone "^2.1.1"
|
||||
deep-equal "^1.0.1"
|
||||
eventemitter3 "^2.0.3"
|
||||
extend "^3.0.2"
|
||||
parchment "^1.1.4"
|
||||
quill-delta "^3.6.2"
|
||||
|
||||
raf-schd@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
|
||||
|
@ -13243,6 +13273,15 @@ react-popper@^1.3.3:
|
|||
typed-styles "^0.0.7"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-quill@2.0.0-beta.2:
|
||||
version "2.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/react-quill/-/react-quill-2.0.0-beta.2.tgz#3aaf7fe9259249d36bcae435e02a4d15bb9980a8"
|
||||
integrity sha512-jzoq57Mt216sCOr59OC6aMgmckdAh3BgKgEqYI/FcS26JwCMlXMgGu7Dc7TApzObax9dwOWbr6lT8cEWxigyVA==
|
||||
dependencies:
|
||||
"@types/quill" "^1.3.10"
|
||||
lodash "^4.17.4"
|
||||
quill "^1.3.7"
|
||||
|
||||
react-redux@7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2"
|
||||
|
|
Loading…
Reference in a new issue