diff --git a/package-lock.json b/package-lock.json index 9f5f70c..e5a08bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "react-modal": "3.16.1", "sanitize-html": "2.8.0", "slate": "0.90.0", + "slate-history": "0.93.0", "slate-react": "0.90.0", "tippy.js": "6.3.7", "twemoji": "14.0.2", @@ -5210,6 +5211,17 @@ "tiny-warning": "^1.0.3" } }, + "node_modules/slate-history": { + "version": "0.93.0", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.93.0.tgz", + "integrity": "sha512-Gr1GMGPipRuxIz41jD2/rbvzPj8eyar56TVMyJBvBeIpQSSjNISssvGNDYfJlSWM8eaRqf6DAcxMKzsLCYeX6g==", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, "node_modules/slate-react": { "version": "0.90.0", "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.90.0.tgz", diff --git a/package.json b/package.json index 5f0f42f..bf9adea 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "react-modal": "3.16.1", "sanitize-html": "2.8.0", "slate": "0.90.0", + "slate-history": "0.93.0", "slate-react": "0.90.0", "tippy.js": "6.3.7", "twemoji": "14.0.2", diff --git a/src/app/components/editor/Editor.css.ts b/src/app/components/editor/Editor.css.ts index 034ded7..9ec8cfa 100644 --- a/src/app/components/editor/Editor.css.ts +++ b/src/app/components/editor/Editor.css.ts @@ -43,6 +43,7 @@ export const EditorPlaceholder = style([ { position: 'absolute', zIndex: 1, + width: '100%', opacity: config.opacity.Placeholder, pointerEvents: 'none', userSelect: 'none', @@ -55,9 +56,10 @@ export const EditorPlaceholder = style([ }, ]); -export const EditorToolbar = style([ - DefaultReset, - { - padding: config.space.S100, - }, -]); +export const EditorToolbarBase = style({ + padding: `0 ${config.borderWidth.B300}`, +}); + +export const EditorToolbar = style({ + padding: config.space.S100, +}); diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index 2657b21..f4241e0 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useState, } from 'react'; - import { Box, Scroll, Text } from 'folds'; import { Descendant, Editor, createEditor } from 'slate'; import { @@ -18,6 +17,7 @@ import { RenderElementProps, RenderPlaceholderProps, } from 'slate-react'; +import { withHistory } from 'slate-history'; import { BlockType, RenderElement, RenderLeaf } from './Elements'; import { CustomElement } from './slate'; import * as css from './Editor.css'; @@ -50,7 +50,7 @@ const withVoid = (editor: Editor): Editor => { }; export const useEditor = (): Editor => { - const [editor] = useState(withInline(withVoid(withReact(createEditor())))); + const [editor] = useState(withInline(withVoid(withReact(withHistory(createEditor()))))); return editor; }; @@ -104,7 +104,13 @@ export const CustomEditor = forwardRef( // eslint-disable-next-line @typescript-eslint/no-unused-vars const { style, ...props } = attributes; return ( - + {children} ); diff --git a/src/app/components/editor/Toolbar.tsx b/src/app/components/editor/Toolbar.tsx index a84fca2..72e2c38 100644 --- a/src/app/components/editor/Toolbar.tsx +++ b/src/app/components/editor/Toolbar.tsx @@ -10,6 +10,7 @@ import { Line, Menu, PopOut, + Scroll, Text, Tooltip, TooltipProvider, @@ -17,7 +18,14 @@ import { } from 'folds'; import React, { ReactNode, useState } from 'react'; import { ReactEditor, useSlate } from 'slate-react'; -import { isBlockActive, isMarkActive, toggleBlock, toggleMark } from './common'; +import { + isAnyMarkActive, + isBlockActive, + isMarkActive, + removeAllMark, + toggleBlock, + toggleMark, +} from './common'; import * as css from './Editor.css'; import { BlockType, MarkType } from './Elements'; import { HeadingLevel } from './slate'; @@ -44,6 +52,11 @@ function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) { type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode }; export function MarkButton({ format, icon, tooltip }: MarkButtonProps) { const editor = useSlate(); + const disableInline = isBlockActive(editor, BlockType.CodeBlock); + + if (disableInline) { + removeAllMark(editor); + } const handleClick = () => { toggleMark(editor, format); @@ -58,10 +71,11 @@ export function MarkButton({ format, icon, tooltip }: MarkButtonProps) { variant="SurfaceVariant" onClick={handleClick} aria-pressed={isMarkActive(editor, format)} - size="300" + size="400" radii="300" + disabled={disableInline} > - + )} @@ -89,10 +103,10 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) { variant="SurfaceVariant" onClick={handleClick} aria-pressed={isBlockActive(editor, format)} - size="300" + size="400" radii="300" > - + )} @@ -115,6 +129,7 @@ export function HeadingBlockButton() { return ( - handleMenuSelect(1)} size="300" radii="300"> - + handleMenuSelect(1)} size="400" radii="300"> + - handleMenuSelect(2)} size="300" radii="300"> - + handleMenuSelect(2)} size="400" radii="300"> + - handleMenuSelect(3)} size="300" radii="300"> - + handleMenuSelect(3)} size="400" radii="300"> + @@ -151,97 +166,143 @@ export function HeadingBlockButton() { variant="SurfaceVariant" onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))} aria-pressed={isActive} - size="300" + size="400" radii="300" > - - + + )} ); } -export function Toolbar() { +type ExitFormattingProps = { tooltip: ReactNode }; +export function ExitFormatting({ tooltip }: ExitFormattingProps) { const editor = useSlate(); - const allowInline = !isBlockActive(editor, BlockType.CodeBlock); - const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl'; + + const handleClick = () => { + if (isAnyMarkActive(editor)) { + removeAllMark(editor); + } else if (!isBlockActive(editor, BlockType.Paragraph)) { + toggleBlock(editor, BlockType.Paragraph); + } + ReactEditor.focus(editor); + }; return ( - - - - - } - /> - - } - /> - - } - /> - - } - /> - - {allowInline && ( - <> - - - } + + {(triggerRef) => ( + + {`Exit ${KeySymbol.Hyper}`} + + )} + + ); +} + +export function Toolbar() { + const editor = useSlate(); + const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl'; + + const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph); + + return ( + + + + <> + + } + /> + } + /> + } + /> + + } + /> + } + /> + } + /> + + + + + + } /> - } + + } /> - } + + } /> - } /> - } - /> - } - /> + - - )} + {canEscape && ( + <> + + + } + /> + + + )} + + ); } diff --git a/src/app/components/editor/common.ts b/src/app/components/editor/common.ts index 619a1bf..1988409 100644 --- a/src/app/components/editor/common.ts +++ b/src/app/components/editor/common.ts @@ -32,7 +32,9 @@ export const toggleMark = (editor: Editor, format: MarkType) => { }; export const removeAllMark = (editor: Editor) => { - ALL_MARK_TYPE.forEach((mark) => Editor.removeMark(editor, mark)); + ALL_MARK_TYPE.forEach((mark) => { + if (isMarkActive(editor, mark)) Editor.removeMark(editor, mark); + }); }; export const isBlockActive = (editor: Editor, format: BlockType) => { @@ -122,6 +124,15 @@ export const resetEditor = (editor: 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 = ( diff --git a/src/app/components/editor/keyboard.ts b/src/app/components/editor/keyboard.ts index 3fbe536..b6e1c3f 100644 --- a/src/app/components/editor/keyboard.ts +++ b/src/app/components/editor/keyboard.ts @@ -15,7 +15,7 @@ export const INLINE_HOTKEYS: Record = { const INLINE_KEYS = Object.keys(INLINE_HOTKEYS); export const BLOCK_HOTKEYS: Record = { - 'mod+shift+0': BlockType.OrderedList, + 'mod+shift+7': BlockType.OrderedList, 'mod+shift+8': BlockType.UnorderedList, "mod+shift+'": BlockType.BlockQuote, 'mod+shift+;': BlockType.CodeBlock, @@ -26,12 +26,12 @@ const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS); * @return boolean true if shortcut is toggled. */ export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent): boolean => { - if (isHotkey('escape', event)) { + if (isHotkey('mod+e', event)) { if (isAnyMarkActive(editor)) { removeAllMark(editor); return true; } - console.log(isBlockActive(editor, BlockType.Paragraph)); + if (!isBlockActive(editor, BlockType.Paragraph)) { toggleBlock(editor, BlockType.Paragraph); return true; diff --git a/src/app/components/editor/slate.d.ts b/src/app/components/editor/slate.d.ts index a321904..74b2070 100644 --- a/src/app/components/editor/slate.d.ts +++ b/src/app/components/editor/slate.d.ts @@ -1,10 +1,11 @@ import { BaseEditor } from 'slate'; import { ReactEditor } from 'slate-react'; +import { HistoryEditor } from 'slate-history'; import { BlockType } from './Elements'; export type HeadingLevel = 1 | 2 | 3; -export type Editor = BaseEditor & ReactEditor; +export type Editor = BaseEditor & HistoryEditor & ReactEditor; export type Text = { text: string; @@ -96,11 +97,9 @@ export type CustomElement = | OrderedListElement | UnorderedListElement; -export type CustomEditor = BaseEditor & ReactEditor; - declare module 'slate' { interface CustomTypes { - Editor: BaseEditor & ReactEditor; + Editor: Editor; Element: CustomElement; Text: FormattedText & Text; } diff --git a/src/app/organisms/room/RoomInput.tsx b/src/app/organisms/room/RoomInput.tsx index e79f488..39016ad 100644 --- a/src/app/organisms/room/RoomInput.tsx +++ b/src/app/organisms/room/RoomInput.tsx @@ -19,6 +19,7 @@ import { Icon, IconButton, Icons, + Line, Overlay, OverlayBackdrop, OverlayCenter, @@ -49,6 +50,7 @@ import { EmoticonAutocomplete, createEmoticonElement, moveCursor, + resetEditorHistory, } from '../../components/editor'; import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board'; import { UseStateProvider } from '../../components/UseStateProvider'; @@ -95,6 +97,7 @@ import { MessageReply } from '../../molecules/message/Message'; import colorMXID from '../../../util/colorMXID'; import { parseReplyBody, parseReplyFormattedBody } from '../../utils/room'; import { sanitizeText } from '../../utils/sanitize'; +import { getResizeObserverEntry, useResizeObserver } from '../../hooks/useResizeObserver'; interface RoomInputProps { roomViewRef: RefObject; @@ -158,6 +161,16 @@ export const RoomInput = forwardRef( const handlePaste = useFilePasteHandler(handleFiles); const dropZoneVisible = useFileDropZone(roomViewRef, handleFiles); + const [mobile, setMobile] = useState(document.body.clientWidth < 500); + useResizeObserver( + document.body, + useCallback((entries) => { + const bodyEntry = getResizeObserverEntry(document.body, entries); + if (bodyEntry && bodyEntry.contentRect.width < 500) setMobile(true); + else setMobile(false); + }, []) + ); + useEffect(() => { Transforms.insertFragment(editor, msgDraft); }, [editor, msgDraft]); @@ -168,6 +181,7 @@ export const RoomInput = forwardRef( const parsedDraft = JSON.parse(JSON.stringify(editor.children)); setMsgDraft(parsedDraft); resetEditor(editor); + resetEditorHistory(editor); }; }, [roomId, editor, setMsgDraft]); @@ -276,6 +290,7 @@ export const RoomInput = forwardRef( } mx.sendMessage(roomId, content); resetEditor(editor); + resetEditorHistory(editor); setReplyDraft(); sendTypingStatus(false); }, [mx, roomId, editor, replyDraft, sendTypingStatus, setReplyDraft]); @@ -500,27 +515,36 @@ export const RoomInput = forwardRef( > {(anchorRef) => ( <> - setEmojiBoardTab(EmojiBoardTab.Sticker)} - variant="SurfaceVariant" - size="300" - radii="300" - > - - + {!mobile && ( + setEmojiBoardTab(EmojiBoardTab.Sticker)} + variant="SurfaceVariant" + size="300" + radii="300" + > + + + )} setEmojiBoardTab(EmojiBoardTab.Emoji)} variant="SurfaceVariant" size="300" radii="300" > - + )} @@ -532,7 +556,14 @@ export const RoomInput = forwardRef( } - bottom={toolbar && } + bottom={ + toolbar && ( +
+ + +
+ ) + } /> ); diff --git a/src/app/utils/key-symbol.ts b/src/app/utils/key-symbol.ts index 7e758fd..00dd104 100644 --- a/src/app/utils/key-symbol.ts +++ b/src/app/utils/key-symbol.ts @@ -3,4 +3,7 @@ export enum KeySymbol { Shift = '⇧', Option = '⌥', Control = '⌃', + Hyper = '✦', + Super = '❖', + Escape = '⎋', }