Editor Commands (#1450)
* add commands hook * add commands in editor * add command auto complete menu * add commands in room input * remove old reply code from room input * fix video component css * do not auto focus input on android or ios * fix crash on enable block after selection * fix circular deps in editor * fix autocomplete return focus move editor cursor * remove unwanted keydown from room input * fix emoji alignment in editor * test ipad user agent * refactor isAndroidOrIOS to mobileOrTablet * update slate & slate-react * downgrade slate-react to 0.98.4 0.99.0 has breaking changes with ReactEditor.focus * add sql to readable ext mimetype * fix empty editor formatting gets saved as draft * add option to use enter for newline * remove empty msg draft from atom family * prevent msg ctx menu from open on text selection
This commit is contained in:
parent
4d0b6b93bc
commit
613e6d6503
34 changed files with 620 additions and 131 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -56,9 +56,9 @@
|
|||
"react-modal": "3.16.1",
|
||||
"react-range": "1.8.14",
|
||||
"sanitize-html": "2.8.0",
|
||||
"slate": "0.90.0",
|
||||
"slate": "0.94.1",
|
||||
"slate-history": "0.93.0",
|
||||
"slate-react": "0.90.0",
|
||||
"slate-react": "0.98.4",
|
||||
"tippy.js": "6.3.7",
|
||||
"twemoji": "14.0.2",
|
||||
"ua-parser-js": "1.0.35"
|
||||
|
|
@ -5766,9 +5766,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/slate": {
|
||||
"version": "0.90.0",
|
||||
"resolved": "https://registry.npmjs.org/slate/-/slate-0.90.0.tgz",
|
||||
"integrity": "sha512-dv8idv0JjYyHiAJcVKf5yWKPDMTDi+PSZyfjsnquEI8VB5nmTVGjeJab06lc3o69O7aN05ROwO9/OY8mU1IUPA==",
|
||||
"version": "0.94.1",
|
||||
"resolved": "https://registry.npmjs.org/slate/-/slate-0.94.1.tgz",
|
||||
"integrity": "sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==",
|
||||
"dependencies": {
|
||||
"immer": "^9.0.6",
|
||||
"is-plain-object": "^5.0.0",
|
||||
|
|
@ -5787,9 +5787,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/slate-react": {
|
||||
"version": "0.90.0",
|
||||
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.90.0.tgz",
|
||||
"integrity": "sha512-z6pGd6jjU5VazLxlDi6zL3a6yaPBPJ+A2VyIlE/h/rvDywaLYGvk0xcrA9NrK71Dr47HK5ZN2zFEZNleh6wlPA==",
|
||||
"version": "0.98.4",
|
||||
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.98.4.tgz",
|
||||
"integrity": "sha512-8Of3v9hFuX8rIRc86LuuBhU9t8ps+9ARKL4yyhCrKQYZ93Ep/LFA3GvPGvtf3zYuVadZ8tkhRH8tbHOGNAndLw==",
|
||||
"dependencies": {
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@types/is-hotkey": "^0.1.1",
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@
|
|||
"react-modal": "3.16.1",
|
||||
"react-range": "1.8.14",
|
||||
"sanitize-html": "2.8.0",
|
||||
"slate": "0.90.0",
|
||||
"slate": "0.94.1",
|
||||
"slate-history": "0.93.0",
|
||||
"slate-react": "0.90.0",
|
||||
"slate-react": "0.98.4",
|
||||
"tippy.js": "6.3.7",
|
||||
"twemoji": "14.0.2",
|
||||
"ua-parser-js": "1.0.35"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const EditorTextarea = style([
|
|||
{
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
padding: `${toRem(13)} 0`,
|
||||
padding: `${toRem(13)} ${toRem(1)}`,
|
||||
selectors: {
|
||||
[`${EditorTextareaScroll}:first-child &`]: {
|
||||
paddingLeft: toRem(13),
|
||||
|
|
@ -34,6 +34,9 @@ export const EditorTextarea = style([
|
|||
[`${EditorTextareaScroll}:last-child &`]: {
|
||||
paddingRight: toRem(13),
|
||||
},
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import {
|
|||
RenderPlaceholderProps,
|
||||
} from 'slate-react';
|
||||
import { withHistory } from 'slate-history';
|
||||
import { BlockType, RenderElement, RenderLeaf } from './Elements';
|
||||
import { BlockType } from './types';
|
||||
import { RenderElement, RenderLeaf } from './Elements';
|
||||
import { CustomElement } from './slate';
|
||||
import * as css from './Editor.css';
|
||||
import { toggleKeyboardShortcut } from './keyboard';
|
||||
|
|
@ -34,8 +35,9 @@ const withInline = (editor: Editor): Editor => {
|
|||
const { isInline } = editor;
|
||||
|
||||
editor.isInline = (element) =>
|
||||
[BlockType.Mention, BlockType.Emoticon, BlockType.Link].includes(element.type) ||
|
||||
isInline(element);
|
||||
[BlockType.Mention, BlockType.Emoticon, BlockType.Link, BlockType.Command].includes(
|
||||
element.type
|
||||
) || isInline(element);
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
|
@ -44,7 +46,8 @@ const withVoid = (editor: Editor): Editor => {
|
|||
const { isVoid } = editor;
|
||||
|
||||
editor.isVoid = (element) =>
|
||||
[BlockType.Mention, BlockType.Emoticon].includes(element.type) || isVoid(element);
|
||||
[BlockType.Mention, BlockType.Emoticon, BlockType.Command].includes(element.type) ||
|
||||
isVoid(element);
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
|
@ -122,7 +125,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
|||
|
||||
return (
|
||||
<div className={css.Editor} ref={ref}>
|
||||
<Slate editor={editor} value={initialValue} onChange={onChange}>
|
||||
<Slate editor={editor} initialValue={initialValue} onChange={onChange}>
|
||||
{top}
|
||||
<Box alignItems="Start">
|
||||
{before && (
|
||||
|
|
|
|||
|
|
@ -1,34 +1,18 @@
|
|||
import { Scroll, Text } from 'folds';
|
||||
import React from 'react';
|
||||
import { RenderElementProps, RenderLeafProps, useFocused, useSelected } from 'slate-react';
|
||||
import {
|
||||
RenderElementProps,
|
||||
RenderLeafProps,
|
||||
useFocused,
|
||||
useSelected,
|
||||
useSlate,
|
||||
} from 'slate-react';
|
||||
|
||||
import * as css from '../../styles/CustomHtml.css';
|
||||
import { EmoticonElement, LinkElement, MentionElement } from './slate';
|
||||
import { CommandElement, EmoticonElement, LinkElement, MentionElement } from './slate';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
|
||||
export enum MarkType {
|
||||
Bold = 'bold',
|
||||
Italic = 'italic',
|
||||
Underline = 'underline',
|
||||
StrikeThrough = 'strikeThrough',
|
||||
Code = 'code',
|
||||
Spoiler = 'spoiler',
|
||||
}
|
||||
|
||||
export enum BlockType {
|
||||
Paragraph = 'paragraph',
|
||||
Heading = 'heading',
|
||||
CodeLine = 'code-line',
|
||||
CodeBlock = 'code-block',
|
||||
QuoteLine = 'quote-line',
|
||||
BlockQuote = 'block-quote',
|
||||
ListItem = 'list-item',
|
||||
OrderedList = 'ordered-list',
|
||||
UnorderedList = 'unordered-list',
|
||||
Mention = 'mention',
|
||||
Emoticon = 'emoticon',
|
||||
Link = 'link',
|
||||
}
|
||||
import { getBeginCommand } from './utils';
|
||||
import { BlockType } from './types';
|
||||
|
||||
// Put this at the start and end of an inline component to work around this Chromium bug:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
|
||||
|
|
@ -62,6 +46,29 @@ function RenderMentionElement({
|
|||
</span>
|
||||
);
|
||||
}
|
||||
function RenderCommandElement({
|
||||
attributes,
|
||||
element,
|
||||
children,
|
||||
}: { element: CommandElement } & RenderElementProps) {
|
||||
const selected = useSelected();
|
||||
const focused = useFocused();
|
||||
const editor = useSlate();
|
||||
|
||||
return (
|
||||
<span
|
||||
{...attributes}
|
||||
className={css.Command({
|
||||
focus: selected && focused,
|
||||
active: getBeginCommand(editor) === element.command,
|
||||
})}
|
||||
contentEditable={false}
|
||||
>
|
||||
{`/${element.command}`}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderEmoticonElement({
|
||||
attributes,
|
||||
|
|
@ -200,6 +207,12 @@ export function RenderElement({ attributes, element, children }: RenderElementPr
|
|||
{children}
|
||||
</RenderLinkElement>
|
||||
);
|
||||
case BlockType.Command:
|
||||
return (
|
||||
<RenderCommandElement attributes={attributes} element={element}>
|
||||
{children}
|
||||
</RenderCommandElement>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Text className={css.Paragraph} {...attributes}>
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ import {
|
|||
removeAllMark,
|
||||
toggleBlock,
|
||||
toggleMark,
|
||||
} from './common';
|
||||
} from './utils';
|
||||
import * as css from './Editor.css';
|
||||
import { BlockType, MarkType } from './Elements';
|
||||
import { BlockType, MarkType } from './types';
|
||||
import { HeadingLevel } from './slate';
|
||||
import { isMacOS } from '../../utils/user-agent';
|
||||
import { KeySymbol } from '../../utils/key-symbol';
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export function AutocompleteMenu({ headerContent, requestClose, children }: Auto
|
|||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => requestClose(),
|
||||
returnFocusOnDeactivate: false,
|
||||
clickOutsideDeactivates: true,
|
||||
allowOutsideClick: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => isHotkey('arrowdown', evt),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
useAsyncSearch,
|
||||
} from '../../../hooks/useAsyncSearch';
|
||||
import { onTabPress } from '../../../utils/keyboard';
|
||||
import { createEmoticonElement, moveCursor, replaceWithElement } from '../common';
|
||||
import { createEmoticonElement, moveCursor, replaceWithElement } from '../utils';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
import { useRelevantImagePacks } from '../../../hooks/useImagePacks';
|
||||
import { IEmoji, emojis } from '../../../plugins/emoji';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Editor } from 'slate';
|
|||
import { Avatar, AvatarFallback, AvatarImage, Icon, Icons, MenuItem, Text, color } from 'folds';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import { createMentionElement, moveCursor, replaceWithElement } from '../common';
|
||||
import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
|
||||
import { getRoomAvatarUrl, joinRuleToIconSrc } from '../../../utils/room';
|
||||
import { roomIdByActivity } from '../../../../util/sort';
|
||||
import initMatrix from '../../../../client/initMatrix';
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
useAsyncSearch,
|
||||
} from '../../../hooks/useAsyncSearch';
|
||||
import { onTabPress } from '../../../utils/keyboard';
|
||||
import { createMentionElement, moveCursor, replaceWithElement } from '../common';
|
||||
import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
|
||||
import { useKeyDown } from '../../../hooks/useKeyDown';
|
||||
import { getMxIdLocalPart, getMxIdServer, validMxId } from '../../../utils/matrix';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ export enum AutocompletePrefix {
|
|||
RoomMention = '#',
|
||||
UserMention = '@',
|
||||
Emoticon = ':',
|
||||
Command = '/',
|
||||
}
|
||||
export const AUTOCOMPLETE_PREFIXES: readonly AutocompletePrefix[] = [
|
||||
AutocompletePrefix.RoomMention,
|
||||
AutocompletePrefix.UserMention,
|
||||
AutocompletePrefix.Emoticon,
|
||||
AutocompletePrefix.Command,
|
||||
];
|
||||
|
||||
export type AutocompleteQuery<TPrefix extends string> = {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
export * from './autocomplete';
|
||||
export * from './common';
|
||||
export * from './utils';
|
||||
export * from './Editor';
|
||||
export * from './Elements';
|
||||
export * from './keyboard';
|
||||
export * from './output';
|
||||
export * from './Toolbar';
|
||||
export * from './input';
|
||||
export * from './types';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import parse from 'html-dom-parser';
|
|||
import { ChildNode, Element, isText, isTag } from 'domhandler';
|
||||
|
||||
import { sanitizeCustomHtml } from '../../utils/sanitize';
|
||||
import { BlockType, MarkType } from './Elements';
|
||||
import { BlockType, MarkType } from './types';
|
||||
import {
|
||||
BlockQuoteElement,
|
||||
CodeBlockElement,
|
||||
|
|
@ -21,7 +21,7 @@ import {
|
|||
UnorderedListElement,
|
||||
} from './slate';
|
||||
import { parseMatrixToUrl } from '../../utils/matrix';
|
||||
import { createEmoticonElement, createMentionElement } from './common';
|
||||
import { createEmoticonElement, createMentionElement } from './utils';
|
||||
|
||||
const markNodeToType: Record<string, MarkType> = {
|
||||
b: MarkType.Bold,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { isHotkey } from 'is-hotkey';
|
||||
import { KeyboardEvent } from 'react';
|
||||
import { Editor } from 'slate';
|
||||
import { isAnyMarkActive, isBlockActive, removeAllMark, toggleBlock, toggleMark } from './common';
|
||||
import { BlockType, MarkType } from './Elements';
|
||||
import { isAnyMarkActive, isBlockActive, removeAllMark, toggleBlock, toggleMark } from './utils';
|
||||
import { BlockType, MarkType } from './types';
|
||||
|
||||
export const INLINE_HOTKEYS: Record<string, MarkType> = {
|
||||
'mod+b': MarkType.Bold,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Descendant, Text } from 'slate';
|
||||
|
||||
import { sanitizeText } from '../../utils/sanitize';
|
||||
import { BlockType } from './Elements';
|
||||
import { BlockType } from './types';
|
||||
import { CustomElement } from './slate';
|
||||
import { parseInlineMD } from '../../utils/markdown';
|
||||
|
||||
|
|
@ -57,6 +57,8 @@ const elementToCustomHtml = (node: CustomElement, children: string): string => {
|
|||
: node.key;
|
||||
case BlockType.Link:
|
||||
return `<a href="${node.href}">${node.children}</a>`;
|
||||
case BlockType.Command:
|
||||
return `/${node.command}`;
|
||||
default:
|
||||
return children;
|
||||
}
|
||||
|
|
@ -104,6 +106,8 @@ const elementToPlainText = (node: CustomElement, children: string): string => {
|
|||
return node.key.startsWith('mxc://') ? `:${node.shortcode}:` : node.key;
|
||||
case BlockType.Link:
|
||||
return `[${node.children}](${node.href})`;
|
||||
case BlockType.Command:
|
||||
return `/${node.command}`;
|
||||
default:
|
||||
return children;
|
||||
}
|
||||
|
|
@ -129,4 +133,12 @@ export const toPlainText = (node: Descendant | Descendant[]): string => {
|
|||
export const customHtmlEqualsPlainText = (customHtml: string, plain: string): boolean =>
|
||||
customHtml.replace(/<br\/>/g, '\n') === sanitizeText(plain);
|
||||
|
||||
export const trimCustomHtml = (customHtml: string) => customHtml.replace(/<br\/>$/g, '');
|
||||
export const trimCustomHtml = (customHtml: string) => customHtml.replace(/<br\/>$/g, '').trim();
|
||||
|
||||
export const trimCommand = (cmdName: string, str: string) => {
|
||||
const cmdRegX = new RegExp(`^(\\s+)?(\\/${cmdName})([^\\S\n]+)?`);
|
||||
|
||||
const match = str.match(cmdRegX);
|
||||
if (!match) return str;
|
||||
return str.slice(match[0].length);
|
||||
};
|
||||
|
|
|
|||
10
src/app/components/editor/slate.d.ts
vendored
10
src/app/components/editor/slate.d.ts
vendored
|
|
@ -1,7 +1,7 @@
|
|||
import { BaseEditor } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { HistoryEditor } from 'slate-history';
|
||||
import { BlockType } from './Elements';
|
||||
import { BlockType } from './types';
|
||||
|
||||
export type HeadingLevel = 1 | 2 | 3;
|
||||
|
||||
|
|
@ -39,8 +39,13 @@ export type EmoticonElement = {
|
|||
shortcode: string;
|
||||
children: Text[];
|
||||
};
|
||||
export type CommandElement = {
|
||||
type: BlockType.Command;
|
||||
command: string;
|
||||
children: Text[];
|
||||
};
|
||||
|
||||
export type InlineElement = Text | LinkElement | MentionElement | EmoticonElement;
|
||||
export type InlineElement = Text | LinkElement | MentionElement | EmoticonElement | CommandElement;
|
||||
|
||||
export type ParagraphElement = {
|
||||
type: BlockType.Paragraph;
|
||||
|
|
@ -84,6 +89,7 @@ export type CustomElement =
|
|||
| LinkElement
|
||||
| MentionElement
|
||||
| EmoticonElement
|
||||
| CommandElement
|
||||
| ParagraphElement
|
||||
| HeadingElement
|
||||
| CodeLineElement
|
||||
|
|
|
|||
24
src/app/components/editor/types.ts
Normal file
24
src/app/components/editor/types.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
export enum MarkType {
|
||||
Bold = 'bold',
|
||||
Italic = 'italic',
|
||||
Underline = 'underline',
|
||||
StrikeThrough = 'strikeThrough',
|
||||
Code = 'code',
|
||||
Spoiler = 'spoiler',
|
||||
}
|
||||
|
||||
export enum BlockType {
|
||||
Paragraph = 'paragraph',
|
||||
Heading = 'heading',
|
||||
CodeLine = 'code-line',
|
||||
CodeBlock = 'code-block',
|
||||
QuoteLine = 'quote-line',
|
||||
BlockQuote = 'block-quote',
|
||||
ListItem = 'list-item',
|
||||
OrderedList = 'ordered-list',
|
||||
UnorderedList = 'unordered-list',
|
||||
Mention = 'mention',
|
||||
Emoticon = 'emoticon',
|
||||
Link = 'link',
|
||||
Command = 'command',
|
||||
}
|
||||
261
src/app/components/editor/utils.ts
Normal file
261
src/app/components/editor/utils.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import { BasePoint, BaseRange, Editor, Element, Point, Range, Text, Transforms } from 'slate';
|
||||
import { BlockType, MarkType } from './types';
|
||||
import {
|
||||
CommandElement,
|
||||
EmoticonElement,
|
||||
FormattedText,
|
||||
HeadingLevel,
|
||||
LinkElement,
|
||||
MentionElement,
|
||||
} from './slate';
|
||||
|
||||
const ALL_MARK_TYPE: MarkType[] = [
|
||||
MarkType.Bold,
|
||||
MarkType.Code,
|
||||
MarkType.Italic,
|
||||
MarkType.Spoiler,
|
||||
MarkType.StrikeThrough,
|
||||
MarkType.Underline,
|
||||
];
|
||||
|
||||
export const isMarkActive = (editor: Editor, format: MarkType) => {
|
||||
const marks = Editor.marks(editor);
|
||||
return marks ? marks[format] === true : false;
|
||||
};
|
||||
|
||||
export const isAnyMarkActive = (editor: Editor) => {
|
||||
const marks = Editor.marks(editor);
|
||||
return marks && !!ALL_MARK_TYPE.find((type) => marks[type] === true);
|
||||
};
|
||||
|
||||
export const toggleMark = (editor: Editor, format: MarkType) => {
|
||||
const isActive = isMarkActive(editor, format);
|
||||
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, format);
|
||||
} else {
|
||||
Editor.addMark(editor, format, true);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeAllMark = (editor: Editor) => {
|
||||
ALL_MARK_TYPE.forEach((mark) => {
|
||||
if (isMarkActive(editor, mark)) Editor.removeMark(editor, mark);
|
||||
});
|
||||
};
|
||||
|
||||
export const isBlockActive = (editor: Editor, format: BlockType) => {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: (node) => Element.isElement(node) && node.type === format,
|
||||
});
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
type BlockOption = { level: HeadingLevel };
|
||||
const NESTED_BLOCK = [
|
||||
BlockType.OrderedList,
|
||||
BlockType.UnorderedList,
|
||||
BlockType.BlockQuote,
|
||||
BlockType.CodeBlock,
|
||||
];
|
||||
|
||||
export const toggleBlock = (editor: Editor, format: BlockType, option?: BlockOption) => {
|
||||
Transforms.collapse(editor, {
|
||||
edge: 'end',
|
||||
});
|
||||
const isActive = isBlockActive(editor, format);
|
||||
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: (node) => Element.isElement(node) && NESTED_BLOCK.includes(node.type),
|
||||
split: true,
|
||||
});
|
||||
|
||||
if (isActive) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: BlockType.Paragraph,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === BlockType.OrderedList || format === BlockType.UnorderedList) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: BlockType.ListItem,
|
||||
});
|
||||
const block = {
|
||||
type: format,
|
||||
children: [],
|
||||
};
|
||||
Transforms.wrapNodes(editor, block);
|
||||
return;
|
||||
}
|
||||
if (format === BlockType.CodeBlock) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: BlockType.CodeLine,
|
||||
});
|
||||
const block = {
|
||||
type: format,
|
||||
children: [],
|
||||
};
|
||||
Transforms.wrapNodes(editor, block);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === BlockType.BlockQuote) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: BlockType.QuoteLine,
|
||||
});
|
||||
const block = {
|
||||
type: format,
|
||||
children: [],
|
||||
};
|
||||
Transforms.wrapNodes(editor, block);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format === BlockType.Heading) {
|
||||
Transforms.setNodes(editor, {
|
||||
type: format,
|
||||
level: option?.level ?? 1,
|
||||
});
|
||||
}
|
||||
|
||||
Transforms.setNodes(editor, {
|
||||
type: format,
|
||||
});
|
||||
};
|
||||
|
||||
export const resetEditor = (editor: Editor) => {
|
||||
Transforms.delete(editor, {
|
||||
at: {
|
||||
anchor: Editor.start(editor, []),
|
||||
focus: Editor.end(editor, []),
|
||||
},
|
||||
});
|
||||
|
||||
toggleBlock(editor, BlockType.Paragraph);
|
||||
removeAllMark(editor);
|
||||
};
|
||||
|
||||
export const resetEditorHistory = (editor: Editor) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
editor.history = {
|
||||
undos: [],
|
||||
redos: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const createMentionElement = (
|
||||
id: string,
|
||||
name: string,
|
||||
highlight: boolean
|
||||
): MentionElement => ({
|
||||
type: BlockType.Mention,
|
||||
id,
|
||||
highlight,
|
||||
name,
|
||||
children: [{ text: '' }],
|
||||
});
|
||||
|
||||
export const createEmoticonElement = (key: string, shortcode: string): EmoticonElement => ({
|
||||
type: BlockType.Emoticon,
|
||||
key,
|
||||
shortcode,
|
||||
children: [{ text: '' }],
|
||||
});
|
||||
|
||||
export const createLinkElement = (
|
||||
href: string,
|
||||
children: string | FormattedText[]
|
||||
): LinkElement => ({
|
||||
type: BlockType.Link,
|
||||
href,
|
||||
children: typeof children === 'string' ? [{ text: children }] : children,
|
||||
});
|
||||
|
||||
export const createCommandElement = (command: string): CommandElement => ({
|
||||
type: BlockType.Command,
|
||||
command,
|
||||
children: [{ text: '' }],
|
||||
});
|
||||
|
||||
export const replaceWithElement = (editor: Editor, selectRange: BaseRange, element: Element) => {
|
||||
Transforms.select(editor, selectRange);
|
||||
Transforms.insertNodes(editor, element);
|
||||
Transforms.collapse(editor, {
|
||||
edge: 'end',
|
||||
});
|
||||
};
|
||||
|
||||
export const moveCursor = (editor: Editor, withSpace?: boolean) => {
|
||||
Transforms.move(editor);
|
||||
if (withSpace) editor.insertText(' ');
|
||||
};
|
||||
|
||||
interface PointUntilCharOptions {
|
||||
match: (char: string) => boolean;
|
||||
reverse?: boolean;
|
||||
}
|
||||
export const getPointUntilChar = (
|
||||
editor: Editor,
|
||||
cursorPoint: BasePoint,
|
||||
options: PointUntilCharOptions
|
||||
): BasePoint | undefined => {
|
||||
let targetPoint: BasePoint | undefined;
|
||||
let prevPoint: BasePoint | undefined;
|
||||
let char: string | undefined;
|
||||
|
||||
const pointItr = Editor.positions(editor, {
|
||||
at: {
|
||||
anchor: Editor.start(editor, []),
|
||||
focus: Editor.point(editor, cursorPoint, { edge: 'start' }),
|
||||
},
|
||||
unit: 'character',
|
||||
reverse: options.reverse,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const point of pointItr) {
|
||||
if (!Point.equals(point, cursorPoint) && prevPoint) {
|
||||
char = Editor.string(editor, { anchor: point, focus: prevPoint });
|
||||
|
||||
if (options.match(char)) break;
|
||||
targetPoint = point;
|
||||
}
|
||||
prevPoint = point;
|
||||
}
|
||||
return targetPoint;
|
||||
};
|
||||
|
||||
export const getPrevWorldRange = (editor: Editor): BaseRange | undefined => {
|
||||
const { selection } = editor;
|
||||
if (!selection || !Range.isCollapsed(selection)) return undefined;
|
||||
const [cursorPoint] = Range.edges(selection);
|
||||
const worldStartPoint = getPointUntilChar(editor, cursorPoint, {
|
||||
reverse: true,
|
||||
match: (char) => char === ' ',
|
||||
});
|
||||
return worldStartPoint && Editor.range(editor, worldStartPoint, cursorPoint);
|
||||
};
|
||||
|
||||
export const isEmptyEditor = (editor: Editor): boolean => {
|
||||
const firstChildren = editor.children[0];
|
||||
if (firstChildren && Element.isElement(firstChildren)) {
|
||||
const isEmpty = editor.children.length === 1 && Editor.isEmpty(editor, firstChildren);
|
||||
return isEmpty;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getBeginCommand = (editor: Editor): string | undefined => {
|
||||
const lineBlock = editor.children[0];
|
||||
if (!Element.isElement(lineBlock)) return undefined;
|
||||
if (lineBlock.type !== BlockType.Paragraph) return undefined;
|
||||
|
||||
const [firstInline, secondInline] = lineBlock.children;
|
||||
const isEmptyText = Text.isText(firstInline) && firstInline.text.trim() === '';
|
||||
if (!isEmptyText) return undefined;
|
||||
if (Element.isElement(secondInline) && secondInline.type === BlockType.Command)
|
||||
return secondInline.command;
|
||||
return undefined;
|
||||
};
|
||||
|
|
@ -47,6 +47,7 @@ import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearc
|
|||
import { useDebounce } from '../../hooks/useDebounce';
|
||||
import { useThrottle } from '../../hooks/useThrottle';
|
||||
import { addRecentEmoji } from '../../plugins/recent-emoji';
|
||||
import { mobileOrTablet } from '../../utils/user-agent';
|
||||
|
||||
const RECENT_GROUP_ID = 'recent_group';
|
||||
const SEARCH_GROUP_ID = 'search_group';
|
||||
|
|
@ -782,7 +783,7 @@ export function EmojiBoard({
|
|||
maxLength={50}
|
||||
after={<Icon src={Icons.Search} size="50" />}
|
||||
onChange={handleOnChange}
|
||||
autoFocus
|
||||
autoFocus={!mobileOrTablet()}
|
||||
/>
|
||||
</Box>
|
||||
</Header>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ import * as css from './media.css';
|
|||
export const Video = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLVideoElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||
<video className={classNames(css.Image, className)} {...props} ref={ref} />
|
||||
<video className={classNames(css.Video, className)} {...props} ref={ref} />
|
||||
)
|
||||
);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue