Skip to content

Commit

Permalink
Editor Commands (#1450)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ajbura authored Oct 18, 2023
1 parent 4d0b6b9 commit 613e6d6
Show file tree
Hide file tree
Showing 34 changed files with 620 additions and 131 deletions.
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion src/app/components/editor/Editor.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ export const EditorTextarea = style([
{
flexGrow: 1,
height: '100%',
padding: `${toRem(13)} 0`,
padding: `${toRem(13)} ${toRem(1)}`,
selectors: {
[`${EditorTextareaScroll}:first-child &`]: {
paddingLeft: toRem(13),
},
[`${EditorTextareaScroll}:last-child &`]: {
paddingRight: toRem(13),
},
'&:focus': {
outline: 'none',
},
},
},
]);
Expand Down
13 changes: 8 additions & 5 deletions src/app/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
};
Expand All @@ -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;
};
Expand Down Expand Up @@ -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 && (
Expand Down
65 changes: 39 additions & 26 deletions src/app/components/editor/Elements.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}>
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/editor/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
2 changes: 2 additions & 0 deletions src/app/components/editor/autocomplete/autocompleteQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {
Expand Down
3 changes: 2 additions & 1 deletion src/app/components/editor/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 2 deletions src/app/components/editor/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/editor/keyboard.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
16 changes: 14 additions & 2 deletions src/app/components/editor/output.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 changes: 8 additions & 2 deletions src/app/components/editor/slate.d.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -84,6 +89,7 @@ export type CustomElement =
| LinkElement
| MentionElement
| EmoticonElement
| CommandElement
| ParagraphElement
| HeadingElement
| CodeLineElement
Expand Down
24 changes: 24 additions & 0 deletions src/app/components/editor/types.ts
Original file line number Diff line number Diff line change
@@ -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',
}
Loading

0 comments on commit 613e6d6

Please sign in to comment.