client: add textarea-markdown-editor package and replace current editor textarea
This commit is contained in:
parent
481d4ae36c
commit
f510813e4b
4 changed files with 73 additions and 127 deletions
|
@ -5,124 +5,53 @@ import ImageIcon from "@geist-ui/icons/image"
|
||||||
import { RefObject, useCallback, useMemo } from "react"
|
import { RefObject, useCallback, useMemo } from "react"
|
||||||
import styles from "../document.module.css"
|
import styles from "../document.module.css"
|
||||||
import { Button, ButtonGroup } from "@geist-ui/core"
|
import { Button, ButtonGroup } from "@geist-ui/core"
|
||||||
|
import { TextareaMarkdownRef } from "textarea-markdown-editor"
|
||||||
|
|
||||||
// TODO: clean up
|
// TODO: clean up
|
||||||
|
|
||||||
const FormattingIcons = ({
|
const FormattingIcons = ({
|
||||||
textareaRef,
|
textareaRef,
|
||||||
setText
|
|
||||||
}: {
|
}: {
|
||||||
textareaRef?: RefObject<HTMLTextAreaElement>
|
textareaRef?: RefObject<TextareaMarkdownRef>
|
||||||
setText?: (text: string) => void
|
|
||||||
}) => {
|
}) => {
|
||||||
// const { textBefore, textAfter, selectedText } = useMemo(() => {
|
|
||||||
// if (textareaRef && textareaRef.current) {
|
|
||||||
// const textarea = textareaRef.current
|
|
||||||
// const text = textareaRef.current.value
|
|
||||||
// const selectionStart = textarea.selectionStart
|
|
||||||
// const selectionEnd = textarea.selectionEnd
|
|
||||||
// const textBefore = text.substring(0, selectionStart)
|
|
||||||
// const textAfter = text.substring(selectionEnd)
|
|
||||||
// const selectedText = text.substring(selectionStart, selectionEnd)
|
|
||||||
// return { textBefore, textAfter, selectedText }
|
|
||||||
// }
|
|
||||||
// return { textBefore: '', textAfter: '' }
|
|
||||||
// }, [textareaRef,])
|
|
||||||
|
|
||||||
const handleBoldClick = useCallback(() => {
|
|
||||||
if (textareaRef?.current && setText) {
|
|
||||||
const selectionStart = textareaRef.current.selectionStart
|
|
||||||
const selectionEnd = textareaRef.current.selectionEnd
|
|
||||||
const text = textareaRef.current.value
|
|
||||||
const before = text.substring(0, selectionStart)
|
|
||||||
const after = text.substring(selectionEnd)
|
|
||||||
const selectedText = text.substring(selectionStart, selectionEnd)
|
|
||||||
|
|
||||||
const newText = `${before}**${selectedText}**${after}`
|
|
||||||
setText(newText)
|
|
||||||
}
|
|
||||||
}, [setText, textareaRef])
|
|
||||||
|
|
||||||
const handleItalicClick = useCallback(() => {
|
|
||||||
if (textareaRef?.current && setText) {
|
|
||||||
const selectionStart = textareaRef.current.selectionStart
|
|
||||||
const selectionEnd = textareaRef.current.selectionEnd
|
|
||||||
const text = textareaRef.current.value
|
|
||||||
const before = text.substring(0, selectionStart)
|
|
||||||
const after = text.substring(selectionEnd)
|
|
||||||
const selectedText = text.substring(selectionStart, selectionEnd)
|
|
||||||
const newText = `${before}*${selectedText}*${after}`
|
|
||||||
setText(newText)
|
|
||||||
}
|
|
||||||
}, [setText, textareaRef])
|
|
||||||
|
|
||||||
const handleLinkClick = useCallback(() => {
|
|
||||||
if (textareaRef?.current && setText) {
|
|
||||||
const selectionStart = textareaRef.current.selectionStart
|
|
||||||
const selectionEnd = textareaRef.current.selectionEnd
|
|
||||||
const text = textareaRef.current.value
|
|
||||||
const before = text.substring(0, selectionStart)
|
|
||||||
const after = text.substring(selectionEnd)
|
|
||||||
const selectedText = text.substring(selectionStart, selectionEnd)
|
|
||||||
let formattedText = ""
|
|
||||||
if (selectedText.includes("http")) {
|
|
||||||
formattedText = `[](${selectedText})`
|
|
||||||
} else {
|
|
||||||
formattedText = `[${selectedText}](https://)`
|
|
||||||
}
|
|
||||||
const newText = `${before}${formattedText}${after}`
|
|
||||||
setText(newText)
|
|
||||||
}
|
|
||||||
}, [setText, textareaRef])
|
|
||||||
|
|
||||||
const handleImageClick = useCallback(() => {
|
|
||||||
if (textareaRef?.current && setText) {
|
|
||||||
const selectionStart = textareaRef.current.selectionStart
|
|
||||||
const selectionEnd = textareaRef.current.selectionEnd
|
|
||||||
const text = textareaRef.current.value
|
|
||||||
const before = text.substring(0, selectionStart)
|
|
||||||
const after = text.substring(selectionEnd)
|
|
||||||
const selectedText = text.substring(selectionStart, selectionEnd)
|
|
||||||
let formattedText = ""
|
|
||||||
if (selectedText.includes("http")) {
|
|
||||||
formattedText = `![](${selectedText})`
|
|
||||||
} else {
|
|
||||||
formattedText = `![${selectedText}](https://)`
|
|
||||||
}
|
|
||||||
const newText = `${before}${formattedText}${after}`
|
|
||||||
setText(newText)
|
|
||||||
}
|
|
||||||
}, [setText, textareaRef])
|
|
||||||
|
|
||||||
const formattingActions = useMemo(
|
const formattingActions = useMemo(
|
||||||
() => [
|
() => {
|
||||||
{
|
const handleBoldClick = () => textareaRef?.current?.trigger("bold")
|
||||||
icon: <Bold />,
|
const handleItalicClick = () => textareaRef?.current?.trigger("italic")
|
||||||
name: "bold",
|
const handleLinkClick = () => textareaRef?.current?.trigger("link")
|
||||||
action: handleBoldClick
|
const handleImageClick = () => textareaRef?.current?.trigger("image")
|
||||||
},
|
return [
|
||||||
{
|
{
|
||||||
icon: <Italic />,
|
icon: <Bold />,
|
||||||
name: "italic",
|
name: "bold",
|
||||||
action: handleItalicClick
|
action: handleBoldClick
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// icon: <Underline />,
|
icon: <Italic />,
|
||||||
// name: 'underline',
|
name: "italic",
|
||||||
// action: handleUnderlineClick
|
action: handleItalicClick
|
||||||
// },
|
},
|
||||||
{
|
// {
|
||||||
icon: <Link />,
|
// icon: <Underline />,
|
||||||
name: "hyperlink",
|
// name: 'underline',
|
||||||
action: handleLinkClick
|
// action: handleUnderlineClick
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
icon: <ImageIcon />,
|
icon: <Link />,
|
||||||
name: "image",
|
name: "hyperlink",
|
||||||
action: handleImageClick
|
action: handleLinkClick
|
||||||
}
|
},
|
||||||
],
|
{
|
||||||
[handleBoldClick, handleImageClick, handleItalicClick, handleLinkClick]
|
icon: <ImageIcon />,
|
||||||
|
name: "image",
|
||||||
|
action: handleImageClick
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[textareaRef]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,16 +9,14 @@ import {
|
||||||
import styles from "./document.module.css"
|
import styles from "./document.module.css"
|
||||||
import Trash from "@geist-ui/icons/trash"
|
import Trash from "@geist-ui/icons/trash"
|
||||||
import FormattingIcons from "./formatting-icons"
|
import FormattingIcons from "./formatting-icons"
|
||||||
|
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
|
||||||
Card,
|
|
||||||
Input,
|
Input,
|
||||||
Spacer,
|
Spacer,
|
||||||
Tabs,
|
Tabs,
|
||||||
Textarea,
|
Textarea,
|
||||||
Tooltip
|
|
||||||
} from "@geist-ui/core"
|
} from "@geist-ui/core"
|
||||||
import Preview from "@components/preview"
|
import Preview from "@components/preview"
|
||||||
|
|
||||||
|
@ -27,7 +25,6 @@ type Props = {
|
||||||
title?: string
|
title?: string
|
||||||
content?: string
|
content?: string
|
||||||
setTitle?: (title: string) => void
|
setTitle?: (title: string) => void
|
||||||
setContent?: (content: string) => void
|
|
||||||
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||||
initialTab?: "edit" | "preview"
|
initialTab?: "edit" | "preview"
|
||||||
remove?: () => void
|
remove?: () => void
|
||||||
|
@ -40,11 +37,10 @@ const Document = ({
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
setTitle,
|
setTitle,
|
||||||
setContent,
|
|
||||||
initialTab = "edit",
|
initialTab = "edit",
|
||||||
handleOnContentChange
|
handleOnContentChange
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const codeEditorRef = useRef<HTMLTextAreaElement>(null)
|
const codeEditorRef = useRef<TextareaMarkdownRef>(null)
|
||||||
const [tab, setTab] = useState(initialTab)
|
const [tab, setTab] = useState(initialTab)
|
||||||
// const height = editable ? "500px" : '100%'
|
// const height = editable ? "500px" : '100%'
|
||||||
const height = "100%"
|
const height = "100%"
|
||||||
|
@ -126,7 +122,7 @@ const Document = ({
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.descriptionContainer}>
|
<div className={styles.descriptionContainer}>
|
||||||
{tab === "edit" && (
|
{tab === "edit" && (
|
||||||
<FormattingIcons setText={setContent} textareaRef={codeEditorRef} />
|
<FormattingIcons textareaRef={codeEditorRef} />
|
||||||
)}
|
)}
|
||||||
<Tabs
|
<Tabs
|
||||||
onChange={handleTabChange}
|
onChange={handleTabChange}
|
||||||
|
@ -143,18 +139,20 @@ const Document = ({
|
||||||
flexDirection: "column"
|
flexDirection: "column"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Textarea
|
<TextareaMarkdown.Wrapper ref={codeEditorRef}>
|
||||||
onPaste={onPaste ? onPaste : undefined}
|
<Textarea
|
||||||
ref={codeEditorRef}
|
onPaste={onPaste ? onPaste : undefined}
|
||||||
placeholder=""
|
ref={codeEditorRef}
|
||||||
value={content}
|
placeholder=""
|
||||||
onChange={handleOnContentChange}
|
value={content}
|
||||||
width="100%"
|
onChange={handleOnContentChange}
|
||||||
// TODO: Textarea should grow to fill parent if height == 100%
|
width="100%"
|
||||||
style={{ flex: 1, minHeight: 350 }}
|
// TODO: Textarea should grow to fill parent if height == 100%
|
||||||
resize="vertical"
|
style={{ flex: 1, minHeight: 350 }}
|
||||||
className={styles.textarea}
|
resize="vertical"
|
||||||
/>
|
className={styles.textarea}
|
||||||
|
/>
|
||||||
|
</TextareaMarkdown.Wrapper>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.Item>
|
</Tabs.Item>
|
||||||
<Tabs.Item label="Preview" value="preview">
|
<Tabs.Item label="Preview" value="preview">
|
||||||
|
|
|
@ -39,7 +39,8 @@
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
"rehype-slug": "^5.0.1",
|
"rehype-slug": "^5.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"swr": "^1.2.2"
|
"swr": "^1.2.2",
|
||||||
|
"textarea-markdown-editor": "^0.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "^12.1.0",
|
"@next/bundle-analyzer": "^12.1.0",
|
||||||
|
|
|
@ -2876,6 +2876,11 @@ module-lookup-amd@^7.0.1:
|
||||||
requirejs "^2.3.5"
|
requirejs "^2.3.5"
|
||||||
requirejs-config-file "^4.0.0"
|
requirejs-config-file "^4.0.0"
|
||||||
|
|
||||||
|
mousetrap@^1.6.5:
|
||||||
|
version "1.6.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
|
||||||
|
integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
|
||||||
|
|
||||||
mri@^1.1.0:
|
mri@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||||
|
@ -3739,6 +3744,11 @@ react-syntax-highlighter@^15.4.5:
|
||||||
prismjs "^1.25.0"
|
prismjs "^1.25.0"
|
||||||
refractor "^3.2.0"
|
refractor "^3.2.0"
|
||||||
|
|
||||||
|
react-trigger-change@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-trigger-change/-/react-trigger-change-1.0.2.tgz#af573398ecef2475362b84f8c08c07fea23914c3"
|
||||||
|
integrity sha1-r1czmOzvJHU2K4T4wIwH/qI5FMM=
|
||||||
|
|
||||||
react@17.0.2:
|
react@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
|
@ -4246,6 +4256,14 @@ text-table@^0.2.0:
|
||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||||
|
|
||||||
|
textarea-markdown-editor@^0.1.13:
|
||||||
|
version "0.1.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/textarea-markdown-editor/-/textarea-markdown-editor-0.1.13.tgz#27aeda4bc95148a8ed69021f553753feb224325f"
|
||||||
|
integrity sha512-2r1gTPFA/wwAzt+Aa6LVZWjJNvL0aXfR6Z9T6eQBpJ1AK6gtPVCZgkO97KIrqpAmMcwgNCz0ToYj2AqPufdVeg==
|
||||||
|
dependencies:
|
||||||
|
mousetrap "^1.6.5"
|
||||||
|
react-trigger-change "^1.0.2"
|
||||||
|
|
||||||
to-regex-range@^5.0.1:
|
to-regex-range@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||||
|
|
Loading…
Reference in a new issue