Skip to content

Commit

Permalink
Add ESC btn to toolbar to quickly exit formatting (cinnyapp#1283)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ajbura authored Jun 16, 2023
1 parent 2883b4c commit bc5e744
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 106 deletions.
14 changes: 8 additions & 6 deletions src/app/components/editor/Editor.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const EditorPlaceholder = style([
{
position: 'absolute',
zIndex: 1,
width: '100%',
opacity: config.opacity.Placeholder,
pointerEvents: 'none',
userSelect: 'none',
Expand All @@ -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,
});
8 changes: 7 additions & 1 deletion src/app/components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
221 changes: 141 additions & 80 deletions src/app/components/editor/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ import {
Line,
Menu,
PopOut,
Scroll,
Text,
Tooltip,
TooltipProvider,
toRem,
} 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';
Expand All @@ -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);
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand All @@ -115,6 +129,7 @@ export function HeadingBlockButton() {
return (
<PopOut
open={open}
offset={5}
align="Start"
position="Top"
content={
Expand All @@ -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>
Expand All @@ -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"
>
<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>
);
}

type ExitFormattingProps = { tooltip: ReactNode };
export function ExitFormatting({ tooltip }: ExitFormattingProps) {
const editor = useSlate();

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 allowInline = !isBlockActive(editor, BlockType.CodeBlock);
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';

const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);

return (
<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">
<MarkButton
format={MarkType.Bold}
icon={Icons.Bold}
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />}
<Box className={css.EditorToolbarBase}>
<Scroll direction="Horizontal" size="0">
<Box className={css.EditorToolbar} alignItems="Center" gap="300">
<>
<Box shrink="No" gap="100">
<MarkButton
format={MarkType.Bold}
icon={Icons.Bold}
tooltip={<BtnTooltip text="Bold" shortCode={`${modKey} + B`} />}
/>
<MarkButton
format={MarkType.Italic}
icon={Icons.Italic}
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />}
/>
<MarkButton
format={MarkType.Underline}
icon={Icons.Underline}
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`} />}
/>
<MarkButton
format={MarkType.StrikeThrough}
icon={Icons.Strike}
tooltip={
<BtnTooltip
text="Strike Through"
shortCode={`${modKey} + ${KeySymbol.Shift} + U`}
/>
}
/>
<MarkButton
format={MarkType.Code}
icon={Icons.Code}
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} />}
/>
<MarkButton
format={MarkType.Spoiler}
icon={Icons.EyeBlind}
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} + '`} />
}
/>
<MarkButton
format={MarkType.Italic}
icon={Icons.Italic}
tooltip={<BtnTooltip text="Italic" shortCode={`${modKey} + I`} />}
<BlockButton
format={BlockType.CodeBlock}
icon={Icons.BlockCode}
tooltip={
<BtnTooltip text="Block Code" shortCode={`${modKey} + ${KeySymbol.Shift} + ;`} />
}
/>
<MarkButton
format={MarkType.Underline}
icon={Icons.Underline}
tooltip={<BtnTooltip text="Underline" shortCode={`${modKey} + U`} />}
<BlockButton
format={BlockType.OrderedList}
icon={Icons.OrderList}
tooltip={
<BtnTooltip text="Ordered List" shortCode={`${modKey} + ${KeySymbol.Shift} + 7`} />
}
/>
<MarkButton
format={MarkType.StrikeThrough}
icon={Icons.Strike}
<BlockButton
format={BlockType.UnorderedList}
icon={Icons.UnorderList}
tooltip={
<BtnTooltip
text="Strike Through"
shortCode={`${modKey} + ${KeySymbol.Shift} + U`}
text="Unordered List"
shortCode={`${modKey} + ${KeySymbol.Shift} + 8`}
/>
}
/>
<MarkButton
format={MarkType.Code}
icon={Icons.Code}
tooltip={<BtnTooltip text="Inline Code" shortCode={`${modKey} + [`} />}
/>
<MarkButton
format={MarkType.Spoiler}
icon={Icons.EyeBlind}
tooltip={<BtnTooltip text="Spoiler" shortCode={`${modKey} + H`} />}
/>
<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>
);
}
4 changes: 3 additions & 1 deletion src/app/components/editor/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
6 changes: 3 additions & 3 deletions src/app/components/editor/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down
Loading

0 comments on commit bc5e744

Please sign in to comment.