-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add shortcut for text formatting and FormatBar component (#263)
* Add text formatting (bold, italic, code) * Add prevention of duplicate text formatting and toggle handling for multiple text formats * Add formatBar for bold, italic, and cod in Editor component * Styling component using MUI * Add shortcut to support for strikethrough text formatting * refactor FormatBar, TooltipButton component into seperate file * refactor functions within the Editor component * Change formatBar position (for YorkieIntelligence) & Add debouncing for selected text * refactor tooltipButton sx props * refactor combine formatBar states into a single state object * refactor change component TooltipButton -> TooltipToggleButton - TooltipToggleButton move to folder src/components/editor -> src/components/common - Remove custom interface and import ToogleButtonProps in MUI * refactor extract formatBar functions (useFormatUtils) - create customHook useFormatUtils.ts - extract Format&FormatBar functions to useFormatUtils.ts * Refactor useFormatUtils & TooltipToggleButton
- Loading branch information
Showing
4 changed files
with
337 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { ToggleButton, Tooltip, ToggleButtonProps } from "@mui/material"; | ||
|
||
const style = { | ||
toggleButton: { | ||
width: "25px", | ||
height: "25px", | ||
minWidth: "25px", | ||
padding: "0", | ||
margin: "2px", | ||
border: "none", | ||
fontWeight: "bold", | ||
}, | ||
}; | ||
|
||
function TooltipToggleButton({ | ||
title, | ||
children, | ||
...props | ||
}: ToggleButtonProps & { title?: string }) { | ||
return ( | ||
<Tooltip title={title} placement="top"> | ||
<ToggleButton sx={style.toggleButton} {...props}> | ||
{children} | ||
</ToggleButton> | ||
</Tooltip> | ||
); | ||
} | ||
|
||
export default TooltipToggleButton; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { Popover, ToggleButtonGroup } from "@mui/material"; | ||
import TooltipToggleButton from "../common/TooltipToggleButton"; | ||
import { FormatBarState, useFormatUtils, FormatType } from "../../hooks/useFormatUtils"; | ||
|
||
interface FormatBarProps { | ||
formatBarState: FormatBarState; | ||
onChangeFormatBarState: React.Dispatch<React.SetStateAction<FormatBarState>>; | ||
} | ||
|
||
function FormatBar({ | ||
formatBarState: { show: showFormatBar, position: formatBarPosition, selectedFormats }, | ||
onChangeFormatBarState, | ||
}: FormatBarProps) { | ||
const { toggleButtonChangeHandler } = useFormatUtils(); | ||
|
||
return ( | ||
<Popover | ||
open={showFormatBar} | ||
anchorReference="anchorPosition" | ||
anchorPosition={{ | ||
top: formatBarPosition.top, | ||
left: formatBarPosition.left, | ||
}} | ||
onClose={() => onChangeFormatBarState((prev) => ({ ...prev, show: false }))} | ||
anchorOrigin={{ | ||
vertical: "top", | ||
horizontal: "left", | ||
}} | ||
transformOrigin={{ | ||
vertical: "bottom", | ||
horizontal: "left", | ||
}} | ||
disableAutoFocus | ||
> | ||
<ToggleButtonGroup | ||
sx={{ padding: "3px 5px" }} | ||
value={Array.from(selectedFormats)} | ||
onChange={toggleButtonChangeHandler(selectedFormats, onChangeFormatBarState)} | ||
exclusive | ||
aria-label="text formatting" | ||
> | ||
<TooltipToggleButton | ||
color={selectedFormats.has(FormatType.ITALIC) ? "primary" : "secondary"} | ||
title={"Cmd+I / Ctrl+I"} | ||
value={FormatType.ITALIC} | ||
> | ||
<i>i</i> | ||
</TooltipToggleButton> | ||
<TooltipToggleButton | ||
color={selectedFormats.has(FormatType.BOLD) ? "primary" : "secondary"} | ||
title={"Cmd+B / Ctrl+B"} | ||
value={FormatType.BOLD} | ||
> | ||
<strong>B</strong> | ||
</TooltipToggleButton> | ||
<TooltipToggleButton | ||
color={selectedFormats.has(FormatType.STRIKETHROUGH) ? "primary" : "secondary"} | ||
title={"Cmd+Shift+X / Ctrl+Shfit+X"} | ||
value={FormatType.STRIKETHROUGH} | ||
> | ||
~ | ||
</TooltipToggleButton> | ||
<TooltipToggleButton | ||
color={selectedFormats.has(FormatType.CODE) ? "primary" : "secondary"} | ||
title={"Cmd+E / Ctrl+E"} | ||
value={FormatType.CODE} | ||
> | ||
{"</>"} | ||
</TooltipToggleButton> | ||
</ToggleButtonGroup> | ||
</Popover> | ||
); | ||
} | ||
|
||
export default FormatBar; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { MouseEvent, useCallback } from "react"; | ||
import { EditorState, Text, EditorSelection, Transaction } from "@codemirror/state"; | ||
import { EditorView } from "@codemirror/view"; | ||
import { indentWithTab } from "@codemirror/commands"; | ||
import { Dispatch, SetStateAction } from "react"; | ||
import { useSelector } from "react-redux"; | ||
import { selectEditor } from "../store/editorSlice"; | ||
|
||
export interface FormatBarState { | ||
show: boolean; | ||
position: { top: number; left: number }; | ||
selectedFormats: Set<FormatType>; | ||
} | ||
|
||
export enum FormatType { | ||
BOLD = "bold", | ||
ITALIC = "italic", | ||
CODE = "code", | ||
STRIKETHROUGH = "strikeThrough", | ||
} | ||
|
||
export const useFormatUtils = () => { | ||
const { cmView } = useSelector(selectEditor); | ||
|
||
const getFormatMarker = useCallback((formatType: FormatType) => { | ||
switch (formatType) { | ||
case FormatType.BOLD: | ||
return "**"; | ||
case FormatType.ITALIC: | ||
return "_"; | ||
case FormatType.CODE: | ||
return "`"; | ||
case FormatType.STRIKETHROUGH: | ||
return "~~"; | ||
} | ||
}, []); | ||
|
||
const getFormatMarkerLength = useCallback((state: EditorState, from: number) => { | ||
const maxCheckLength = 10; | ||
|
||
const markerSet = new Set(["*", "_", "`", "~"]); | ||
const docSlice = state.sliceDoc(Math.max(0, from - maxCheckLength), from); | ||
return [...docSlice].reduce((acc, c) => (markerSet.has(c) ? acc + 1 : acc), 0); | ||
}, []); | ||
|
||
const applyFormat = useCallback( | ||
(formatType: FormatType) => { | ||
const marker = getFormatMarker(formatType); | ||
const markerLength = marker.length; | ||
|
||
return ({ state, dispatch }: EditorView) => { | ||
const changes = state.changeByRange((range) => { | ||
const maxLength = getFormatMarkerLength(state, range.from); | ||
const beforeIdx = state | ||
.sliceDoc( | ||
range.from - maxLength < 0 ? 0 : range.from - maxLength, | ||
range.from | ||
) | ||
.indexOf(marker); | ||
const afterIdx = state.sliceDoc(range.to, range.to + maxLength).indexOf(marker); | ||
|
||
const changes = [ | ||
beforeIdx === -1 | ||
? { | ||
from: range.from, | ||
insert: Text.of([marker]), | ||
} | ||
: { | ||
from: range.from - maxLength + beforeIdx, | ||
to: range.from - maxLength + beforeIdx + markerLength, | ||
insert: Text.of([""]), | ||
}, | ||
|
||
afterIdx === -1 | ||
? { | ||
from: range.to, | ||
insert: Text.of([marker]), | ||
} | ||
: { | ||
from: range.to + afterIdx, | ||
to: range.to + afterIdx + markerLength, | ||
insert: Text.of([""]), | ||
}, | ||
]; | ||
|
||
const extendBefore = beforeIdx === -1 ? markerLength : -markerLength; | ||
const extendAfter = afterIdx === -1 ? markerLength : -markerLength; | ||
|
||
return { | ||
changes, | ||
range: EditorSelection.range( | ||
range.from + extendBefore, | ||
range.to + extendAfter | ||
), | ||
}; | ||
}); | ||
|
||
dispatch( | ||
state.update(changes, { | ||
scrollIntoView: true, | ||
annotations: Transaction.userEvent.of("input"), | ||
}) | ||
); | ||
|
||
return true; | ||
}; | ||
}, | ||
[getFormatMarker, getFormatMarkerLength] | ||
); | ||
|
||
const setKeymapConfig = useCallback( | ||
() => [ | ||
indentWithTab, | ||
{ key: "Mod-b", run: applyFormat(FormatType.BOLD) }, | ||
{ key: "Mod-i", run: applyFormat(FormatType.ITALIC) }, | ||
{ key: "Mod-e", run: applyFormat(FormatType.CODE) }, | ||
{ key: "Mod-Shift-x", run: applyFormat(FormatType.STRIKETHROUGH) }, | ||
], | ||
[applyFormat] | ||
); | ||
|
||
const toggleButtonChangeHandler = useCallback( | ||
( | ||
selectedFormats: Set<FormatType>, | ||
onChangeFormatBarState: Dispatch<SetStateAction<FormatBarState>> | ||
) => { | ||
return (_event: MouseEvent<HTMLElement>, format: FormatType) => { | ||
if (!cmView) return; | ||
|
||
const newSelectedFormats = new Set(selectedFormats); | ||
if (newSelectedFormats.has(format)) { | ||
newSelectedFormats.delete(format); | ||
} else { | ||
newSelectedFormats.add(format); | ||
} | ||
onChangeFormatBarState((prev) => ({ | ||
...prev, | ||
selectedFormats: newSelectedFormats, | ||
})); | ||
applyFormat(format)(cmView); | ||
}; | ||
}, | ||
[cmView, applyFormat] | ||
); | ||
|
||
return { | ||
getFormatMarkerLength, | ||
applyFormat, | ||
setKeymapConfig, | ||
toggleButtonChangeHandler, | ||
}; | ||
}; |