Add ESC btn to toolbar to quickly exit formatting (#1283)
* Add ESC btn to toolbar to quickly exit formatting * add horizontal scroll to toolbar item * make editor toolbar usable in touch device * fix editor hotkeys not working in window * remove unused import
This commit is contained in:
parent
2883b4c35b
commit
bc5e7445d9
7 changed files with 210 additions and 108 deletions
|
@ -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,
|
||||
{
|
||||
export const EditorToolbarBase = style({
|
||||
padding: `0 ${config.borderWidth.B300}`,
|
||||
});
|
||||
|
||||
export const EditorToolbar = style({
|
||||
padding: config.space.S100,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -104,7 +104,13 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { style, ...props } = attributes;
|
||||
return (
|
||||
<Text as="span" {...props} className={css.EditorPlaceholder} contentEditable={false}>
|
||||
<Text
|
||||
as="span"
|
||||
{...props}
|
||||
className={css.EditorPlaceholder}
|
||||
contentEditable={false}
|
||||
truncate
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<Icon size="50" src={icon} />
|
||||
<Icon size="200" src={icon} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
|
@ -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"
|
||||
>
|
||||
<Icon size="50" src={icon} />
|
||||
<Icon size="200" src={icon} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
|
@ -115,6 +129,7 @@ export function HeadingBlockButton() {
|
|||
return (
|
||||
<PopOut
|
||||
open={open}
|
||||
offset={5}
|
||||
align="Start"
|
||||
position="Top"
|
||||
content={
|
||||
|
@ -130,14 +145,14 @@ export function HeadingBlockButton() {
|
|||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<Box gap="100">
|
||||
<IconButton onClick={() => handleMenuSelect(1)} size="300" radii="300">
|
||||
<Icon size="100" src={Icons.Heading1} />
|
||||
<IconButton onClick={() => handleMenuSelect(1)} size="400" radii="300">
|
||||
<Icon size="200" src={Icons.Heading1} />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleMenuSelect(2)} size="300" radii="300">
|
||||
<Icon size="100" src={Icons.Heading2} />
|
||||
<IconButton onClick={() => handleMenuSelect(2)} size="400" radii="300">
|
||||
<Icon size="200" src={Icons.Heading2} />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleMenuSelect(3)} size="300" radii="300">
|
||||
<Icon size="100" src={Icons.Heading3} />
|
||||
<IconButton onClick={() => handleMenuSelect(3)} size="400" radii="300">
|
||||
<Icon size="200" src={Icons.Heading3} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Menu>
|
||||
|
@ -151,59 +166,59 @@ export function HeadingBlockButton() {
|
|||
variant="SurfaceVariant"
|
||||
onClick={() => (isActive ? toggleBlock(editor, BlockType.Heading) : setOpen(!open))}
|
||||
aria-pressed={isActive}
|
||||
size="300"
|
||||
size="400"
|
||||
radii="300"
|
||||
>
|
||||
<Icon size="50" src={Icons[`Heading${level}`]} />
|
||||
<Icon size="50" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
|
||||
<Icon size="200" src={Icons[`Heading${level}`]} />
|
||||
<Icon size="200" src={isActive ? Icons.Cross : Icons.ChevronBottom} />
|
||||
</IconButton>
|
||||
)}
|
||||
</PopOut>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<TooltipProvider tooltip={tooltip} delay={500}>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
ref={triggerRef}
|
||||
variant="SurfaceVariant"
|
||||
onClick={handleClick}
|
||||
size="400"
|
||||
radii="300"
|
||||
>
|
||||
<Text size="B400">{`Exit ${KeySymbol.Hyper}`}</Text>
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function Toolbar() {
|
||||
const editor = useSlate();
|
||||
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
||||
|
||||
const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
|
||||
|
||||
return (
|
||||
<Box className={css.EditorToolbarBase}>
|
||||
<Scroll direction="Horizontal" size="0">
|
||||
<Box className={css.EditorToolbar} alignItems="Center" gap="300">
|
||||
<Box gap="100">
|
||||
<HeadingBlockButton />
|
||||
<BlockButton
|
||||
format={BlockType.OrderedList}
|
||||
icon={Icons.OrderList}
|
||||
tooltip={
|
||||
<BtnTooltip text="Ordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 0`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.UnorderedList}
|
||||
icon={Icons.UnorderList}
|
||||
tooltip={
|
||||
<BtnTooltip text="Unordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 8`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.BlockQuote}
|
||||
icon={Icons.BlockQuote}
|
||||
tooltip={
|
||||
<BtnTooltip text="Block Quote" shortCode={`${modKey} + ${KeySymbol.Shift} + '`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.CodeBlock}
|
||||
icon={Icons.BlockCode}
|
||||
tooltip={
|
||||
<BtnTooltip text="Block Code" shortCode={`${modKey} + ${KeySymbol.Shift} + ;`} />
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{allowInline && (
|
||||
<>
|
||||
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
||||
<Box gap="100">
|
||||
<Box shrink="No" gap="100">
|
||||
<MarkButton
|
||||
format={MarkType.Bold}
|
||||
icon={Icons.Bold}
|
||||
|
@ -240,8 +255,54 @@ export function Toolbar() {
|
|||
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} />}
|
||||
/>
|
||||
</Box>
|
||||
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
||||
</>
|
||||
<Box shrink="No" gap="100">
|
||||
<BlockButton
|
||||
format={BlockType.BlockQuote}
|
||||
icon={Icons.BlockQuote}
|
||||
tooltip={
|
||||
<BtnTooltip text="Block Quote" shortCode={`${modKey} + ${KeySymbol.Shift} + '`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.CodeBlock}
|
||||
icon={Icons.BlockCode}
|
||||
tooltip={
|
||||
<BtnTooltip text="Block Code" shortCode={`${modKey} + ${KeySymbol.Shift} + ;`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.OrderedList}
|
||||
icon={Icons.OrderList}
|
||||
tooltip={
|
||||
<BtnTooltip text="Ordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 7`} />
|
||||
}
|
||||
/>
|
||||
<BlockButton
|
||||
format={BlockType.UnorderedList}
|
||||
icon={Icons.UnorderList}
|
||||
tooltip={
|
||||
<BtnTooltip
|
||||
text="Unordered List"
|
||||
shortCode={`${modKey} + ${KeySymbol.Shift} + 8`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<HeadingBlockButton />
|
||||
</Box>
|
||||
{canEscape && (
|
||||
<>
|
||||
<Line variant="SurfaceVariant" direction="Vertical" style={{ height: toRem(12) }} />
|
||||
<Box shrink="No" gap="100">
|
||||
<ExitFormatting
|
||||
tooltip={<BtnTooltip text="Exit Formatting" shortCode={`${modKey} + E`} />}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Scroll>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -15,7 +15,7 @@ export const INLINE_HOTKEYS: Record<string, MarkType> = {
|
|||
const INLINE_KEYS = Object.keys(INLINE_HOTKEYS);
|
||||
|
||||
export const BLOCK_HOTKEYS: Record<string, BlockType> = {
|
||||
'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<Element>): 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;
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Line,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
|
@ -95,6 +96,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<HTMLElement>;
|
||||
|
@ -158,6 +160,16 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
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]);
|
||||
|
@ -500,6 +512,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
>
|
||||
{(anchorRef) => (
|
||||
<>
|
||||
{!mobile && (
|
||||
<IconButton
|
||||
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
|
||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
|
||||
|
@ -512,15 +525,23 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
filled={emojiBoardTab === EmojiBoardTab.Sticker}
|
||||
/>
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
ref={anchorRef}
|
||||
aria-pressed={emojiBoardTab === EmojiBoardTab.Emoji}
|
||||
aria-pressed={
|
||||
mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||
}
|
||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
>
|
||||
<Icon src={Icons.Smile} filled={emojiBoardTab === EmojiBoardTab.Emoji} />
|
||||
<Icon
|
||||
src={Icons.Smile}
|
||||
filled={
|
||||
mobile ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
|
@ -532,7 +553,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
</IconButton>
|
||||
</>
|
||||
}
|
||||
bottom={toolbar && <Toolbar />}
|
||||
bottom={
|
||||
toolbar && (
|
||||
<div>
|
||||
<Line variant="SurfaceVariant" size="300" />
|
||||
<Toolbar />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,4 +3,7 @@ export enum KeySymbol {
|
|||
Shift = '⇧',
|
||||
Option = '⌥',
|
||||
Control = '⌃',
|
||||
Hyper = '✦',
|
||||
Super = '❖',
|
||||
Escape = '⎋',
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue