refactor: markdown UX
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
// import React from 'react'
|
||||
// import { NimblePicker } from "emoji-mart";
|
||||
import { Picker } from "emoji-mart";
|
||||
// import data from "emoji-mart/data/";
|
||||
import "emoji-mart/css/emoji-mart.css";
|
||||
import styled from "styled-components";
|
||||
const StyledWrapper = styled.div`
|
||||
@@ -14,6 +16,8 @@ export default function EmojiPicker({ onSelect, ...rest }) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Picker
|
||||
// set="twitter"
|
||||
// data={data}
|
||||
// set="twitter"
|
||||
showPreview={false}
|
||||
showSkinTones={false}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from "react";
|
||||
import { cx, css } from "@emotion/css";
|
||||
|
||||
export const Button = React.forwardRef(
|
||||
({ className, active, reversed, ...props }, ref) => (
|
||||
<span
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
cursor: pointer;
|
||||
color: ${reversed
|
||||
? active
|
||||
? "white"
|
||||
: "#aaa"
|
||||
: active
|
||||
? "black"
|
||||
: "#ccc"};
|
||||
`
|
||||
)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const Icon = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<span
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cx(
|
||||
"material-icons",
|
||||
className,
|
||||
css`
|
||||
font-size: 18px;
|
||||
vertical-align: text-bottom;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
));
|
||||
|
||||
export const Menu = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
& > * {
|
||||
display: inline-block;
|
||||
}
|
||||
& > * + * {
|
||||
margin-left: 15px;
|
||||
}
|
||||
`
|
||||
)}
|
||||
/>
|
||||
));
|
||||
|
||||
export const Toolbar = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<Menu
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
position: relative;
|
||||
padding: 5px 16px;
|
||||
margin: 0 -20px;
|
||||
border-bottom: 1px solid #fff;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
));
|
||||
@@ -1,31 +0,0 @@
|
||||
import Editor from "rich-markdown-editor";
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.div`
|
||||
padding-top: 8px;
|
||||
.mde {
|
||||
padding: 4px;
|
||||
height: 150px;
|
||||
> div {
|
||||
height: 100%;
|
||||
.ProseMirror {
|
||||
overflow: scroll;
|
||||
height: 100%;
|
||||
max-height: 300px;
|
||||
padding: 5px 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default function MarkdownEditer({ value, updateValue }) {
|
||||
return (
|
||||
<Styled>
|
||||
<Editor
|
||||
defaultValue={value}
|
||||
onChange={updateValue}
|
||||
autoFocus={true}
|
||||
placeholder="Type '/' to insert..."
|
||||
className="mde"
|
||||
></Editor>
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import isHotkey from "is-hotkey";
|
||||
import { Editable, withReact, useSlate, Slate } from "slate-react";
|
||||
import {
|
||||
Editor,
|
||||
Transforms,
|
||||
createEditor,
|
||||
Element as SlateElement,
|
||||
} from "slate";
|
||||
import { withHistory } from "slate-history";
|
||||
|
||||
import Styled from "./styled";
|
||||
import { Button, Icon, Toolbar } from "./components";
|
||||
|
||||
const HOTKEYS = {
|
||||
"mod+b": "bold",
|
||||
"mod+i": "italic",
|
||||
"mod+u": "underline",
|
||||
"mod+`": "code",
|
||||
};
|
||||
|
||||
const LIST_TYPES = ["numbered-list", "bulleted-list"];
|
||||
const TEXT_ALIGN_TYPES = ["left", "center", "right", "justify"];
|
||||
|
||||
const MarkdownEditor = () => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const renderElement = useCallback((props) => <Element {...props} />, []);
|
||||
const renderLeaf = useCallback((props) => <Leaf {...props} />, []);
|
||||
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
|
||||
|
||||
return (
|
||||
<Styled>
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={value}
|
||||
onChange={(value) => setValue(value)}
|
||||
>
|
||||
<Toolbar>
|
||||
<MarkButton format="bold" icon="format_bold" />
|
||||
<MarkButton format="italic" icon="format_italic" />
|
||||
<MarkButton format="underline" icon="format_underlined" />
|
||||
<MarkButton format="code" icon="code" />
|
||||
{/* <BlockButton format="heading-one" icon="looks_one" />
|
||||
<BlockButton format="heading-two" icon="looks_two" /> */}
|
||||
<BlockButton format="block-quote" icon="format_quote" />
|
||||
<BlockButton format="numbered-list" icon="format_list_numbered" />
|
||||
<BlockButton format="bulleted-list" icon="format_list_bulleted" />
|
||||
<BlockButton format="left" icon="format_align_left" />
|
||||
<BlockButton format="center" icon="format_align_center" />
|
||||
<BlockButton format="right" icon="format_align_right" />
|
||||
<BlockButton format="justify" icon="format_align_justify" />
|
||||
</Toolbar>
|
||||
<Editable
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder="Enter some rich text…"
|
||||
spellCheck
|
||||
autoFocus
|
||||
onKeyDown={(event) => {
|
||||
for (const hotkey in HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
event.preventDefault();
|
||||
const mark = HOTKEYS[hotkey];
|
||||
toggleMark(editor, mark);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Slate>
|
||||
</Styled>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleBlock = (editor, format) => {
|
||||
const isActive = isBlockActive(
|
||||
editor,
|
||||
format,
|
||||
TEXT_ALIGN_TYPES.includes(format) ? "align" : "type"
|
||||
);
|
||||
const isList = LIST_TYPES.includes(format);
|
||||
|
||||
Transforms.unwrapNodes(editor, {
|
||||
match: (n) =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
LIST_TYPES.includes(n.type) &&
|
||||
!TEXT_ALIGN_TYPES.includes(format),
|
||||
split: true,
|
||||
});
|
||||
let newProperties;
|
||||
if (TEXT_ALIGN_TYPES.includes(format)) {
|
||||
newProperties = {
|
||||
align: isActive ? undefined : format,
|
||||
};
|
||||
} else {
|
||||
newProperties = {
|
||||
type: isActive ? "paragraph" : isList ? "list-item" : format,
|
||||
};
|
||||
}
|
||||
Transforms.setNodes < SlateElement > (editor, newProperties);
|
||||
|
||||
if (!isActive && isList) {
|
||||
const block = { type: format, children: [] };
|
||||
Transforms.wrapNodes(editor, block);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMark = (editor, format) => {
|
||||
const isActive = isMarkActive(editor, format);
|
||||
|
||||
if (isActive) {
|
||||
Editor.removeMark(editor, format);
|
||||
} else {
|
||||
Editor.addMark(editor, format, true);
|
||||
}
|
||||
};
|
||||
|
||||
const isBlockActive = (editor, format, blockType = "type") => {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
at: Editor.unhangRange(editor, selection),
|
||||
match: (n) =>
|
||||
!Editor.isEditor(n) &&
|
||||
SlateElement.isElement(n) &&
|
||||
n[blockType] === format,
|
||||
})
|
||||
);
|
||||
|
||||
return !!match;
|
||||
};
|
||||
|
||||
const isMarkActive = (editor, format) => {
|
||||
const marks = Editor.marks(editor);
|
||||
return marks ? marks[format] === true : false;
|
||||
};
|
||||
|
||||
const Element = ({ attributes, children, element }) => {
|
||||
const style = { textAlign: element.align };
|
||||
switch (element.type) {
|
||||
case "block-quote":
|
||||
return (
|
||||
<blockquote style={style} {...attributes}>
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
case "bulleted-list":
|
||||
return (
|
||||
<ul style={style} {...attributes}>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
case "heading-one":
|
||||
return (
|
||||
<h1 style={style} {...attributes}>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
case "heading-two":
|
||||
return (
|
||||
<h2 style={style} {...attributes}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
case "list-item":
|
||||
return (
|
||||
<li style={style} {...attributes}>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
case "numbered-list":
|
||||
return (
|
||||
<ol style={style} {...attributes}>
|
||||
{children}
|
||||
</ol>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p style={style} {...attributes}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Leaf = ({ attributes, children, leaf }) => {
|
||||
if (leaf.bold) {
|
||||
children = <strong>{children}</strong>;
|
||||
}
|
||||
|
||||
if (leaf.code) {
|
||||
children = <code>{children}</code>;
|
||||
}
|
||||
|
||||
if (leaf.italic) {
|
||||
children = <em>{children}</em>;
|
||||
}
|
||||
|
||||
if (leaf.underline) {
|
||||
children = <u>{children}</u>;
|
||||
}
|
||||
|
||||
return <span {...attributes}>{children}</span>;
|
||||
};
|
||||
|
||||
const BlockButton = ({ format, icon }) => {
|
||||
const editor = useSlate();
|
||||
return (
|
||||
<Button
|
||||
active={isBlockActive(
|
||||
editor,
|
||||
format,
|
||||
TEXT_ALIGN_TYPES.includes(format) ? "align" : "type"
|
||||
)}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleBlock(editor, format);
|
||||
}}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const MarkButton = ({ format, icon }) => {
|
||||
const editor = useSlate();
|
||||
return (
|
||||
<Button
|
||||
active={isMarkActive(editor, format)}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
toggleMark(editor, format);
|
||||
}}
|
||||
>
|
||||
<Icon>{icon}</Icon>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const initialValue = [
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{ text: "This is editable " },
|
||||
{ text: "rich", bold: true },
|
||||
{ text: " text, " },
|
||||
{ text: "much", italic: true },
|
||||
{ text: " better than a " },
|
||||
{ text: "<textarea>", code: true },
|
||||
{ text: "!" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
children: [
|
||||
{
|
||||
text:
|
||||
"Since it's rich text, you can do things like turn a selection of text ",
|
||||
},
|
||||
{ text: "bold", bold: true },
|
||||
{
|
||||
text:
|
||||
", or add a semantically rendered block quote in the middle of the page, like this:",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "block-quote",
|
||||
children: [{ text: "A wise quote." }],
|
||||
},
|
||||
{
|
||||
type: "paragraph",
|
||||
align: "center",
|
||||
children: [{ text: "Try it out for yourself!" }],
|
||||
},
|
||||
];
|
||||
|
||||
export default MarkdownEditor;
|
||||
@@ -1,62 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.div`
|
||||
* {
|
||||
line-height: 1.4;
|
||||
}
|
||||
/* ul,
|
||||
ol {
|
||||
list-style-position: inside;
|
||||
display: list-item;
|
||||
padding-left: 10px;
|
||||
}
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
} */
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
background-color: #eee;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
font-family: monospace;
|
||||
background-color: #eee;
|
||||
padding: 3px;
|
||||
}
|
||||
code {
|
||||
background-color: #ccc;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 20em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid #ddd;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 10px;
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote[dir="rtl"] {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
padding-right: 10px;
|
||||
border-right: 2px solid #ddd;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Styled;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import "@toast-ui/editor/dist/toastui-editor.css";
|
||||
import { Editor } from "@toast-ui/react-editor";
|
||||
import { useUploadImageMutation } from "../../../app/services/message";
|
||||
|
||||
import styled from "styled-components";
|
||||
import Button from "../../component/styled/Button";
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
width: -webkit-fill-available;
|
||||
margin-top: 16px;
|
||||
.toastui-editor-defaultUI {
|
||||
border-bottom: none;
|
||||
border-radius: 0;
|
||||
border-top: 1px solid #d0d5dd;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
.toastui-editor {
|
||||
padding: 16px 0;
|
||||
[contenteditable="true"] {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.toastui-editor-md-preview {
|
||||
padding-top: 16px;
|
||||
.toastui-editor-contents {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.toastui-editor-toolbar {
|
||||
display: none;
|
||||
}
|
||||
.send {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
`;
|
||||
export default function MarkdownEditor({ placeholder, sendMarkdown }) {
|
||||
const editorRef = useRef(null);
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
// const handleChange = (wtf) => {
|
||||
// const edtr = editorRef.current.getInstance();
|
||||
|
||||
// console.log("md", wtf, edtr.getMarkdown());
|
||||
// updateMarkdown(edtr.getMarkdown());
|
||||
// };
|
||||
useEffect(() => {
|
||||
if (editorRef) {
|
||||
const editorInstance = editorRef.current.getInstance();
|
||||
editorInstance.removeHook("addImageBlobHook");
|
||||
editorInstance.addHook("addImageBlobHook", async (blob, callback) => {
|
||||
const { data: url } = await uploadImage(blob);
|
||||
callback(url);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const send = () => {
|
||||
const edtr = editorRef.current.getInstance();
|
||||
const md = edtr.getMarkdown().trim();
|
||||
if (md) {
|
||||
sendMarkdown(edtr.getMarkdown());
|
||||
edtr.reset();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<StyledWrapper className="input">
|
||||
<Editor
|
||||
placeholder={placeholder}
|
||||
// onChange={handleChange}
|
||||
ref={editorRef}
|
||||
toolbarItems={[]}
|
||||
hideModeSwitch={true}
|
||||
// initialValue="hello world!"
|
||||
previewStyle="vertical"
|
||||
height="50vh"
|
||||
initialEditType="markdown"
|
||||
useCommandShortcut={true}
|
||||
/>
|
||||
<Button className="send small" onClick={send}>
|
||||
Send
|
||||
</Button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import "tippy.js/animations/scale.css";
|
||||
import "tippy.js/dist/tippy.css";
|
||||
import IconQuestion from "../../../../assets/icons/question.svg";
|
||||
import {
|
||||
BalloonToolbar,
|
||||
getPluginType,
|
||||
MARK_BOLD,
|
||||
MARK_ITALIC,
|
||||
MARK_UNDERLINE,
|
||||
MarkToolbarButton,
|
||||
usePlateEditorRef,
|
||||
} from "@udecode/plate";
|
||||
|
||||
const MarkBallonToolbar = () => {
|
||||
const editor = usePlateEditorRef();
|
||||
|
||||
const arrow = false;
|
||||
const theme = "dark";
|
||||
const tooltip = {
|
||||
arrow: true,
|
||||
delay: 0,
|
||||
duration: [200, 0],
|
||||
hideOnClick: false,
|
||||
offset: [0, 17],
|
||||
placement: "top",
|
||||
};
|
||||
|
||||
return (
|
||||
<BalloonToolbar
|
||||
popperOptions={{
|
||||
placement: "top",
|
||||
}}
|
||||
theme={theme}
|
||||
arrow={arrow}
|
||||
>
|
||||
<MarkToolbarButton
|
||||
type={getPluginType(editor, MARK_BOLD)}
|
||||
icon={<IconQuestion />}
|
||||
tooltip={{ content: "Bold (⌘B)", ...tooltip }}
|
||||
/>
|
||||
<MarkToolbarButton
|
||||
type={getPluginType(editor, MARK_ITALIC)}
|
||||
icon={<IconQuestion />}
|
||||
tooltip={{ content: "Italic (⌘I)", ...tooltip }}
|
||||
/>
|
||||
<MarkToolbarButton
|
||||
type={getPluginType(editor, MARK_UNDERLINE)}
|
||||
icon={<IconQuestion />}
|
||||
tooltip={{ content: "Underline (⌘U)", ...tooltip }}
|
||||
/>
|
||||
</BalloonToolbar>
|
||||
);
|
||||
};
|
||||
export default MarkBallonToolbar;
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
ELEMENT_H1,
|
||||
ELEMENT_PARAGRAPH,
|
||||
withPlaceholders,
|
||||
} from "@udecode/plate";
|
||||
|
||||
export const withStyledPlaceHolders = (components) =>
|
||||
withPlaceholders(components, [
|
||||
{
|
||||
key: ELEMENT_PARAGRAPH,
|
||||
placeholder: "Type a paragraph",
|
||||
hideOnBlur: true,
|
||||
},
|
||||
{
|
||||
key: ELEMENT_H1,
|
||||
placeholder: "Untitled",
|
||||
hideOnBlur: false,
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
// AutoformatPlugin,
|
||||
CodeBlockElement,
|
||||
createPlateUI,
|
||||
ELEMENT_BLOCKQUOTE,
|
||||
ELEMENT_CODE_BLOCK,
|
||||
ELEMENT_H1,
|
||||
ELEMENT_H2,
|
||||
ELEMENT_H3,
|
||||
ELEMENT_H4,
|
||||
ELEMENT_H5,
|
||||
ELEMENT_H6,
|
||||
ELEMENT_HR,
|
||||
ELEMENT_IMAGE,
|
||||
ELEMENT_PARAGRAPH,
|
||||
ELEMENT_TODO_LI,
|
||||
// ExitBreakPlugin,
|
||||
// IndentPlugin,
|
||||
isBlockAboveEmpty,
|
||||
isSelectionAtBlockStart,
|
||||
// NormalizeTypesPlugin,
|
||||
// PlatePlugin,
|
||||
// ResetNodePlugin,
|
||||
// SelectOnBackspacePlugin,
|
||||
// SoftBreakPlugin,
|
||||
// TrailingBlockPlugin,
|
||||
withProps,
|
||||
} from "@udecode/plate";
|
||||
// import { EditableProps } from 'slate-react/dist/components/editable'
|
||||
import { css } from "styled-components";
|
||||
|
||||
const resetBlockTypesCommonRule = {
|
||||
types: [ELEMENT_BLOCKQUOTE, ELEMENT_TODO_LI],
|
||||
defaultType: ELEMENT_PARAGRAPH,
|
||||
};
|
||||
|
||||
export const CONFIG = {
|
||||
editableProps: {
|
||||
spellCheck: false,
|
||||
autoFocus: true,
|
||||
placeholder: "Type…",
|
||||
},
|
||||
components: createPlateUI({
|
||||
[ELEMENT_CODE_BLOCK]: withProps(CodeBlockElement, {
|
||||
styles: {
|
||||
root: [
|
||||
css`
|
||||
background-color: #111827;
|
||||
code {
|
||||
color: white;
|
||||
}
|
||||
`,
|
||||
],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
resetBlockType: {
|
||||
options: {
|
||||
rules: [
|
||||
{
|
||||
...resetBlockTypesCommonRule,
|
||||
hotkey: "Enter",
|
||||
predicate: isBlockAboveEmpty,
|
||||
},
|
||||
{
|
||||
...resetBlockTypesCommonRule,
|
||||
hotkey: "Backspace",
|
||||
predicate: isSelectionAtBlockStart,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
trailingBlock: { type: ELEMENT_PARAGRAPH },
|
||||
softBreak: {
|
||||
options: {
|
||||
rules: [
|
||||
// { hotkey: "shift+enter" },
|
||||
// {
|
||||
// hotkey: "enter",
|
||||
// query: {
|
||||
// allow: [ELEMENT_CODE_BLOCK, ELEMENT_BLOCKQUOTE, ELEMENT_TD],
|
||||
// },
|
||||
// },
|
||||
],
|
||||
},
|
||||
},
|
||||
exitBreak: {
|
||||
options: {
|
||||
rules: [
|
||||
{
|
||||
hotkey: "mod+enter",
|
||||
},
|
||||
// {
|
||||
// hotkey: "mod+shift+enter",
|
||||
// before: true,
|
||||
// },
|
||||
// {
|
||||
// hotkey: "enter",
|
||||
// query: {
|
||||
// start: true,
|
||||
// end: true,
|
||||
// allow: KEYS_HEADING,
|
||||
// },
|
||||
// },
|
||||
],
|
||||
},
|
||||
},
|
||||
selectOnBackspace: {
|
||||
options: {
|
||||
query: {
|
||||
allow: [ELEMENT_IMAGE, ELEMENT_HR],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useState, useRef } from "react";
|
||||
// import { serialize } from "remark-slate";
|
||||
import { useKey } from "rooks";
|
||||
|
||||
import { useUploadImageMutation } from "../../../app/services/message";
|
||||
// import nodeTypes from "./values/nodeTypes";
|
||||
import { PLUGINS } from "./plugins";
|
||||
import {
|
||||
createPlateUI,
|
||||
// HeadingToolbar,
|
||||
// MentionCombobox,
|
||||
Plate,
|
||||
createExitBreakPlugin,
|
||||
// createMediaEmbedPlugin,
|
||||
createNodeIdPlugin,
|
||||
createParagraphPlugin,
|
||||
createResetNodePlugin,
|
||||
createSelectOnBackspacePlugin,
|
||||
createSoftBreakPlugin,
|
||||
// createDndPlugin,
|
||||
createTrailingBlockPlugin,
|
||||
// createComboboxPlugin,
|
||||
// createMentionPlugin,
|
||||
createPlugins,
|
||||
ELEMENT_PARAGRAPH,
|
||||
getPlateActions,
|
||||
platesActions,
|
||||
eventEditorStore,
|
||||
// usePlateStore
|
||||
// usePlateStore
|
||||
} from "@udecode/plate";
|
||||
import Styled from "./styled";
|
||||
import { CONFIG } from "./config";
|
||||
// import { VALUES } from "./values/values";
|
||||
|
||||
const id = "rustchat_rich_editor";
|
||||
|
||||
let components = createPlateUI({
|
||||
// customize your components by plugin key
|
||||
});
|
||||
// components = withStyledPlaceHolders(components);
|
||||
|
||||
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: "" }] }];
|
||||
const Plugins = ({ placeholder = "Write some markdown...", sendMessage }) => {
|
||||
const editableRef = useRef(null);
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
useKey(
|
||||
"Enter",
|
||||
(evt) => {
|
||||
console.log("enter keypress", evt);
|
||||
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.isComposing) {
|
||||
return true;
|
||||
}
|
||||
evt.preventDefault();
|
||||
sendMessage();
|
||||
platesActions.unset(id);
|
||||
platesActions.set(id, {
|
||||
initialValue,
|
||||
});
|
||||
// eventEditorStore.set()
|
||||
getPlateActions(id).enabled(false);
|
||||
getPlateActions(id).enabled(true);
|
||||
// eventEditorStore.get();
|
||||
// getPlateActions(id).set;
|
||||
// editor.reset
|
||||
},
|
||||
{
|
||||
// eventTypes: ["keydown"],
|
||||
target: editableRef,
|
||||
// when: !shift,
|
||||
}
|
||||
);
|
||||
// const plugins = createPlugins(
|
||||
// [
|
||||
// createParagraphPlugin(),
|
||||
// // createImagePlugin({
|
||||
// // options: {
|
||||
// // uploadImage: (imageData) => {
|
||||
// // return new Promise((resolve, reject) => {
|
||||
// // fetch(imageData)
|
||||
// // .then((res) => res.blob())
|
||||
// // .then((blob) => {
|
||||
// // uploadImage(blob).then(({ data }) => {
|
||||
// // console.log("upload image ", data);
|
||||
// // resolve(data);
|
||||
// // });
|
||||
// // });
|
||||
// // });
|
||||
// // },
|
||||
// // },
|
||||
// // }),
|
||||
// createNodeIdPlugin(),
|
||||
// createResetNodePlugin(CONFIG.resetBlockType),
|
||||
// // createSoftBreakPlugin(CONFIG.softBreak),
|
||||
// createExitBreakPlugin(CONFIG.exitBreak),
|
||||
// createTrailingBlockPlugin(CONFIG.trailingBlock),
|
||||
// createSelectOnBackspacePlugin(CONFIG.selectOnBackspace),
|
||||
// ],
|
||||
// {
|
||||
// components,
|
||||
// }
|
||||
// );
|
||||
const handleChange = (val) => {
|
||||
console.log("changed", val);
|
||||
};
|
||||
|
||||
return (
|
||||
<Styled className="input" ref={editableRef}>
|
||||
<Plate
|
||||
onChange={handleChange}
|
||||
id={id}
|
||||
editableProps={{
|
||||
...CONFIG.editableProps,
|
||||
placeholder,
|
||||
}}
|
||||
initialValue={initialValue}
|
||||
plugins={PLUGINS}
|
||||
></Plate>
|
||||
</Styled>
|
||||
);
|
||||
};
|
||||
export default Plugins;
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
createBasicElementsPlugin,
|
||||
createBlockquotePlugin,
|
||||
createBoldPlugin,
|
||||
createCodeBlockPlugin,
|
||||
createCodePlugin,
|
||||
createHeadingPlugin,
|
||||
createImagePlugin,
|
||||
createItalicPlugin,
|
||||
createParagraphPlugin,
|
||||
createSelectOnBackspacePlugin,
|
||||
createStrikethroughPlugin,
|
||||
createUnderlinePlugin,
|
||||
createPreviewPlugin,
|
||||
} from "@udecode/plate";
|
||||
import { CONFIG } from "./config";
|
||||
|
||||
const basicElements = [
|
||||
createParagraphPlugin(), // paragraph element
|
||||
createBlockquotePlugin(), // blockquote element
|
||||
createCodeBlockPlugin(), // code block element
|
||||
createHeadingPlugin(), // heading elements
|
||||
];
|
||||
|
||||
const basicMarks = [
|
||||
createBoldPlugin(), // bold mark
|
||||
createItalicPlugin(), // italic mark
|
||||
createUnderlinePlugin(), // underline mark
|
||||
createStrikethroughPlugin(), // strikethrough mark
|
||||
createCodePlugin(), // code mark
|
||||
];
|
||||
|
||||
export const PLUGINS = {
|
||||
basicElements,
|
||||
basicMarks,
|
||||
basicNodes: [...basicElements, ...basicMarks],
|
||||
createPreviewPlugin,
|
||||
image: [
|
||||
createBasicElementsPlugin(),
|
||||
...basicMarks,
|
||||
createImagePlugin(),
|
||||
createSelectOnBackspacePlugin(CONFIG.selectOnBackspace),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.div`
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #475467;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Styled;
|
||||
@@ -0,0 +1,24 @@
|
||||
const nodeTypes = {
|
||||
heading: {
|
||||
1: "h1",
|
||||
2: "h2",
|
||||
3: "h3",
|
||||
4: "h4",
|
||||
5: "h5",
|
||||
6: "h6",
|
||||
},
|
||||
paragraph: "p",
|
||||
link: "a",
|
||||
block_quote: "blockquote",
|
||||
ol_list: "ol",
|
||||
ul_list: "ul",
|
||||
listItem: "li",
|
||||
strong_mark: "bold",
|
||||
inline_code_mark: "code",
|
||||
emphasis_mark: "italic",
|
||||
delete_mark: "strikethrough",
|
||||
image: "img",
|
||||
code_block: "code_block",
|
||||
};
|
||||
|
||||
export default nodeTypes;
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
ELEMENT_LI,
|
||||
ELEMENT_LIC,
|
||||
ELEMENT_PARAGRAPH,
|
||||
ELEMENT_UL,
|
||||
// TElement,
|
||||
} from "@udecode/plate";
|
||||
// import { Text } from 'slate'
|
||||
|
||||
export const createElement = (
|
||||
text = "",
|
||||
{ type = ELEMENT_PARAGRAPH, mark } = {}
|
||||
) => {
|
||||
const leaf = { text };
|
||||
if (mark) {
|
||||
leaf[mark] = true;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
children: [leaf],
|
||||
};
|
||||
};
|
||||
|
||||
export const createList = (items = [], { splitSeparator = "`" } = {}) => {
|
||||
const children = items.map((item) => {
|
||||
const texts = item.split(splitSeparator);
|
||||
const marks = texts.map((text, index) => {
|
||||
const res = { text };
|
||||
if (index % 2 === 1) {
|
||||
res.code = true;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
return {
|
||||
type: ELEMENT_LI,
|
||||
children: [
|
||||
{
|
||||
type: ELEMENT_LIC,
|
||||
children: marks,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
type: ELEMENT_UL,
|
||||
children,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const getNodesWithRandomId = (nodes = []) => {
|
||||
let _id = 10000;
|
||||
nodes.forEach((node) => {
|
||||
node.id = _id;
|
||||
_id++;
|
||||
});
|
||||
|
||||
return nodes;
|
||||
};
|
||||
@@ -0,0 +1,275 @@
|
||||
import { ELEMENT_HR } from "@udecode/plate";
|
||||
// import { jsx } from "@udecode/plate-test-utils";
|
||||
import { createList, getNodesWithRandomId } from "./utils";
|
||||
|
||||
const align = (
|
||||
<>
|
||||
<hh1 align="right">Alignment</hh1>
|
||||
<hp align="right">This block text is aligned to the right.</hp>
|
||||
<hh2 align="center">Center</hh2>
|
||||
<hp align="justify">This block text is justified.</hp>
|
||||
</>
|
||||
);
|
||||
|
||||
const indent = (
|
||||
<>
|
||||
<hh1>Changing block indentation</hh1>
|
||||
<hp indent={1}>
|
||||
Use the toolbar buttons to control the indentation of specific blocks. You
|
||||
can use these tools to highlight an important piece of information,
|
||||
communicate a hierarchy or just give your content some room.
|
||||
</hp>
|
||||
<hp indent={2}>
|
||||
For instance, this paragraph looks like it belongs to the previous one.
|
||||
</hp>
|
||||
</>
|
||||
);
|
||||
|
||||
const horizontalRule = (
|
||||
<>
|
||||
<hp>This is a paragraph.</hp>
|
||||
<element type={ELEMENT_HR}>
|
||||
<htext />
|
||||
</element>
|
||||
<hp>And this is another paragraph.</hp>
|
||||
<element type={ELEMENT_HR}>
|
||||
<htext />
|
||||
</element>
|
||||
<hp>But between those paragraphs are horizontal rules.</hp>
|
||||
</>
|
||||
);
|
||||
|
||||
const mediaEmbed = (
|
||||
<>
|
||||
<hh2>🎥 Media Embed</hh2>
|
||||
<hp>
|
||||
In addition to simple image nodes, you can actually create complex
|
||||
embedded nodes. For example, this one contains an input element that lets
|
||||
you change the video being rendered!
|
||||
</hp>
|
||||
<hmediaembed url="https://player.vimeo.com/video/26689853">
|
||||
<htext />
|
||||
</hmediaembed>
|
||||
<hp>
|
||||
Try it out! This editor is built to handle Vimeo embeds, but you could
|
||||
handle any type.
|
||||
</hp>
|
||||
</>
|
||||
);
|
||||
|
||||
const image = (
|
||||
<>
|
||||
<hh2>📷 Image</hh2>
|
||||
<hp>
|
||||
In addition to nodes that contain editable text, you can also create other
|
||||
types of nodes, like images or videos.
|
||||
</hp>
|
||||
<himg url="https://source.unsplash.com/kFrdX5IeQzI" width="75%">
|
||||
<htext />
|
||||
</himg>
|
||||
<hp>
|
||||
This example shows images in action. It features two ways to add images.
|
||||
You can either add an image via the toolbar icon above, or if you want in
|
||||
on a little secret, copy an image URL to your keyboard and paste it
|
||||
anywhere in the editor! Additionally, you can customize the toolbar button
|
||||
to load an url asynchronously, for example showing a file picker and
|
||||
uploading a file to Amazon S3. You can also add a caption and resize the
|
||||
image.
|
||||
</hp>
|
||||
</>
|
||||
);
|
||||
|
||||
const link = (
|
||||
<>
|
||||
<hh2>🔗 Link</hh2>
|
||||
<hp>
|
||||
In addition to block nodes, you can create inline nodes, like{" "}
|
||||
<ha url="https://en.wikipedia.org/wiki/Hypertext">hyperlinks</ha>!
|
||||
</hp>
|
||||
<hp>
|
||||
This example shows hyperlinks in action. It features two ways to add
|
||||
links. You can either add a link via the toolbar icon above, or if you
|
||||
want in on a little secret, copy a URL to your keyboard and paste it while
|
||||
a range of text is selected.
|
||||
</hp>
|
||||
</>
|
||||
);
|
||||
|
||||
const autoformat = (
|
||||
<>
|
||||
<hh1>🏃♀️ Autoformat</hh1>
|
||||
<hp>
|
||||
The editor gives you full control over the logic you can add. For example,
|
||||
it's fairly common to want to add markdown-like shortcuts to editors.
|
||||
</hp>
|
||||
<hp>While typing, try these (mark rules):</hp>
|
||||
<hul>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>**</htext> or <htext code>__</htext> on either side
|
||||
of your text to add **bold* mark.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>*</htext> or <htext code>_</htext> on either side of
|
||||
your text to add *italic mark.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>`</htext> on either side of your text to add `inline
|
||||
code mark.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>~~</htext> on either side of your text to add
|
||||
~~strikethrough~ mark.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Note that nothing happens when there is a character before, try
|
||||
on:*bold
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
We even support smart quotes, try typing{" "}
|
||||
<htext code>"hello" 'world'</htext>.
|
||||
</hlic>
|
||||
</hli>
|
||||
</hul>
|
||||
<hp>
|
||||
At the beginning of any new block or existing block, try these (block
|
||||
rules):
|
||||
</hp>
|
||||
<hul>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>*</htext>, <htext code>-</htext> or{" "}
|
||||
<htext code>+</htext> followed by <htext code>space</htext> to create
|
||||
a bulleted list.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>1.</htext> or <htext code>1)</htext> followed by{" "}
|
||||
<htext code>space</htext> to create a numbered list.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>></htext> followed by <htext code>space</htext> to
|
||||
create a block quote.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>```</htext> to create a code block.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>---</htext> to create a horizontal rule.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>#</htext> followed by <htext code>space</htext> to
|
||||
create an H1 heading.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>##</htext> followed by <htext code>space</htext> to
|
||||
create an H2 sub-heading.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>###</htext> followed by <htext code>space</htext> to
|
||||
create an H3 sub-heading.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>####</htext> followed by <htext code>space</htext> to
|
||||
create an H4 sub-heading.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>#####</htext> followed by <htext code>space</htext>{" "}
|
||||
to create an H5 sub-heading.
|
||||
</hlic>
|
||||
</hli>
|
||||
<hli>
|
||||
<hlic>
|
||||
Type <htext code>######</htext> followed by <htext code>space</htext>{" "}
|
||||
to create an H6 sub-heading.
|
||||
</hlic>
|
||||
</hli>
|
||||
</hul>
|
||||
</>
|
||||
);
|
||||
|
||||
const pasteHtml = (
|
||||
<>
|
||||
<hh1>🍪 Deserialize HTML</hh1>
|
||||
<hp>
|
||||
By default, pasting content into a Slate editor will use the clipboard's{" "}
|
||||
<htext code>'text/plain'</htext> data. That's okay for some use cases, but
|
||||
sometimes you want users to be able to paste in content and have it
|
||||
maintain its formatting. To do this, your editor needs to handle{" "}
|
||||
<htext code>'text/html'</htext> data.
|
||||
</hp>
|
||||
<hp>This is an example of doing exactly that!</hp>
|
||||
<hp>
|
||||
Try it out for yourself! Copy and paste some rendered HTML rich text
|
||||
content (not the source code) from another site into this editor and it's
|
||||
formatting should be preserved.
|
||||
</hp>
|
||||
</>
|
||||
);
|
||||
|
||||
const pasteMd = (
|
||||
<>
|
||||
<hh1>🍩 Deserialize Markdown</hh1>
|
||||
<hp>
|
||||
By default, pasting content into a Slate editor will use the clipboard's{" "}
|
||||
<htext code>'text/plain'</htext> data. That's okay for some use cases, but
|
||||
sometimes you want users to be able to paste in content and have it
|
||||
maintain its formatting. To do this, your editor needs to handle{" "}
|
||||
<htext code>'text/html'</htext> data.
|
||||
</hp>
|
||||
<hp>This is an example of doing exactly that!</hp>
|
||||
<hp>
|
||||
Try it out for yourself! Copy and paste Markdown content from{" "}
|
||||
<ha url="https://markdown-it.github.io/">
|
||||
https://markdown-it.github.io/
|
||||
</ha>
|
||||
</hp>
|
||||
</>
|
||||
);
|
||||
|
||||
const basicMarks = [
|
||||
<hp key="d">
|
||||
The basic marks consist of text formatting such as bold, italic, underline,
|
||||
strikethrough, subscript, superscript, and code.
|
||||
</hp>,
|
||||
];
|
||||
const basicElements = [
|
||||
<hblockquote key="3">Blockquote</hblockquote>,
|
||||
<hcodeblock key="4" lang="javascript">
|
||||
<hcodeline>const a = 'Hello';</hcodeline>
|
||||
<hcodeline>const b = 'World';</hcodeline>
|
||||
</hcodeblock>,
|
||||
];
|
||||
|
||||
const playground = [...basicMarks, ...basicElements];
|
||||
|
||||
export const VALUES = {
|
||||
playground,
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
// import React from 'react'
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
|
||||
const MarkdownOverrides = createGlobalStyle`
|
||||
[class^='toastui-editor-']{
|
||||
.toastui-editor-md-container{
|
||||
border-bottom: none;
|
||||
.toastui-editor-md-splitter{
|
||||
background-color:#D0D5DD ;
|
||||
}
|
||||
}
|
||||
*{
|
||||
/* white-space: nowrap; */
|
||||
margin: 0 ;
|
||||
padding: 0;
|
||||
}
|
||||
p {
|
||||
margin:0 ;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: #475467;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
a{
|
||||
background-color: transparent;
|
||||
}
|
||||
blockquote {
|
||||
border-left:none;
|
||||
display: flex;
|
||||
margin-top:0;
|
||||
margin-bottom: 10px;
|
||||
padding: 16px;
|
||||
color: #98a2b3;
|
||||
opacity: 0.8;
|
||||
box-shadow: inset 2px 0px 0px #a8b0bd;
|
||||
> p{
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
img{
|
||||
cursor: pointer;
|
||||
max-width:300px;
|
||||
}
|
||||
ul {
|
||||
white-space: nowrap;
|
||||
margin-top:0;
|
||||
margin-bottom:10px;
|
||||
/* display: flex;
|
||||
flex-direction:column;
|
||||
margin-left: 20px; */
|
||||
li:before{
|
||||
margin-top: 8px;
|
||||
margin-left: -14px;
|
||||
background-color: #475467;
|
||||
}
|
||||
/* list-style-type: disc; */
|
||||
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #475467;
|
||||
/* list-style-position: inside; */
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,[class*='heading']{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
font-weight: 700;
|
||||
color: #475467;
|
||||
}
|
||||
h1,[class*='heading1'] {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
h2,[class*='heading2'] {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
h3,[class*='heading3'] {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default MarkdownOverrides;
|
||||
@@ -0,0 +1,40 @@
|
||||
// import React from 'react'
|
||||
import { useEditorRef } from "@udecode/plate";
|
||||
import { Transforms } from "slate";
|
||||
import { ReactEditor } from "slate-react";
|
||||
import styled from "styled-components";
|
||||
import iconClose from "../../../assets/icons/close.circle.svg?url";
|
||||
const StylePicture = styled.picture`
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
max-width: 240px;
|
||||
max-height: 240px;
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.remove {
|
||||
background: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -20px;
|
||||
}
|
||||
`;
|
||||
export default function ImageElement({ attributes, children, element }) {
|
||||
const editor = useEditorRef();
|
||||
const path = ReactEditor.findPath(editor, element);
|
||||
const handleRemoveImage = () => {
|
||||
Transforms.removeNodes(editor, { at: path });
|
||||
};
|
||||
|
||||
return (
|
||||
<StylePicture {...attributes}>
|
||||
{children}
|
||||
<img src={element.url}></img>
|
||||
<button className="remove" onClick={handleRemoveImage}>
|
||||
<img src={iconClose} alt="icon close" />
|
||||
</button>
|
||||
</StylePicture>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ELEMENT_IMAGE, ELEMENT_PARAGRAPH } from "@udecode/plate";
|
||||
|
||||
export const CONFIG = {
|
||||
editableProps: {
|
||||
spellCheck: false,
|
||||
autoFocus: true,
|
||||
placeholder: "Type…",
|
||||
},
|
||||
trailingBlock: { type: ELEMENT_PARAGRAPH },
|
||||
softBreak: {
|
||||
options: {
|
||||
rules: [
|
||||
{
|
||||
hotkey: "shift+enter",
|
||||
query: {
|
||||
allow: [ELEMENT_IMAGE, ELEMENT_PARAGRAPH],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
exitBreak: {
|
||||
options: {
|
||||
rules: [
|
||||
{
|
||||
hotkey: "mod+enter",
|
||||
query: {
|
||||
allow: [ELEMENT_IMAGE, ELEMENT_PARAGRAPH],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// hotkey: "mod+shift+enter",
|
||||
// before: true,
|
||||
// },
|
||||
// {
|
||||
// hotkey: "enter",
|
||||
// query: {
|
||||
// start: true,
|
||||
// end: true,
|
||||
// allow: KEYS_HEADING,
|
||||
// },
|
||||
// },
|
||||
],
|
||||
},
|
||||
},
|
||||
selectOnBackspace: {
|
||||
options: {
|
||||
query: {
|
||||
allow: [ELEMENT_IMAGE],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
import { useRef, useState, useCallback } from "react";
|
||||
import { useKey } from "rooks";
|
||||
import { Editor, Transforms } from "slate";
|
||||
import {
|
||||
createPlateUI,
|
||||
Plate,
|
||||
createExitBreakPlugin,
|
||||
createTrailingBlockPlugin,
|
||||
createNodeIdPlugin,
|
||||
createParagraphPlugin,
|
||||
createImagePlugin,
|
||||
createSoftBreakPlugin,
|
||||
createPlugins,
|
||||
ELEMENT_PARAGRAPH,
|
||||
getPlateActions,
|
||||
// usePlateEditorRef,
|
||||
ELEMENT_IMAGE,
|
||||
// usePlateStore
|
||||
} from "@udecode/plate";
|
||||
import Styled from "./styled";
|
||||
import ImageElement from "./ImageElement";
|
||||
import { CONFIG } from "./config";
|
||||
|
||||
export const TEXT_EDITOR_PREFIX = "rustchat_text_editor";
|
||||
|
||||
let components = createPlateUI({
|
||||
[ELEMENT_IMAGE]: ImageElement,
|
||||
// customize your components by plugin key
|
||||
});
|
||||
|
||||
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: "" }] }];
|
||||
const Plugins = ({
|
||||
id = "",
|
||||
placeholder = "Write some markdown...",
|
||||
sendMessages,
|
||||
}) => {
|
||||
// const plateEditor = usePlateEditorRef(`${TEXT_EDITOR_PREFIX}_${id}`);
|
||||
const [msgs, setMsgs] = useState([]);
|
||||
const [cmdKey, setCmdKey] = useState(false);
|
||||
const editableRef = useRef(null);
|
||||
// const editor = useEditorRef();
|
||||
const initialProps = {
|
||||
...CONFIG.editableProps,
|
||||
className: "box",
|
||||
placeholder,
|
||||
};
|
||||
useKey(
|
||||
"Enter",
|
||||
(evt) => {
|
||||
console.log("enter keypress", evt);
|
||||
if (evt.shiftKey || evt.ctrlKey || evt.altKey || evt.isComposing) {
|
||||
return true;
|
||||
}
|
||||
evt.preventDefault();
|
||||
sendMessages(msgs);
|
||||
// 清空
|
||||
const plateEditor = getPlateActions(`${TEXT_EDITOR_PREFIX}_${id}`).editor;
|
||||
Transforms.delete(plateEditor, {
|
||||
at: {
|
||||
anchor: Editor.start(plateEditor, []),
|
||||
focus: Editor.end(plateEditor, []),
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
// eventTypes: ["keydown"],
|
||||
target: editableRef,
|
||||
when: !cmdKey,
|
||||
}
|
||||
);
|
||||
useKey(
|
||||
[91, 93],
|
||||
(evt) => {
|
||||
setCmdKey(evt.type == "keydown");
|
||||
console.log("cmd", evt.type);
|
||||
},
|
||||
{
|
||||
eventTypes: ["keydown", "keyup"],
|
||||
target: editableRef,
|
||||
}
|
||||
);
|
||||
const plugins = createPlugins(
|
||||
[
|
||||
createParagraphPlugin(),
|
||||
createImagePlugin(),
|
||||
createNodeIdPlugin(),
|
||||
createSoftBreakPlugin(CONFIG.softBreak),
|
||||
createTrailingBlockPlugin(CONFIG.trailingBlock),
|
||||
createExitBreakPlugin(CONFIG.exitBreak),
|
||||
],
|
||||
{
|
||||
components,
|
||||
}
|
||||
);
|
||||
const handleChange = useCallback(
|
||||
async (val) => {
|
||||
console.log("tmps changed");
|
||||
const tmps = [];
|
||||
for await (const v of val) {
|
||||
if (v.type == "img") {
|
||||
// img
|
||||
const resp = await fetch(v.url);
|
||||
const value = await resp.blob();
|
||||
tmps.push({ type: "image", value });
|
||||
} else {
|
||||
// text
|
||||
const prev = tmps[tmps.length - 1];
|
||||
if (!prev) {
|
||||
tmps.push([{ type: "text", value: v.children[0].text }]);
|
||||
} else {
|
||||
if (Array.isArray(prev)) {
|
||||
tmps[tmps.length - 1].push({
|
||||
type: "text",
|
||||
value: v.children[0].text,
|
||||
});
|
||||
} else {
|
||||
tmps.push([{ type: "text", value: v.children[0].text }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const arr = tmps.map((tmp) => {
|
||||
return Array.isArray(tmp)
|
||||
? { type: "text", value: tmp.map((t) => t.value).join("\n") }
|
||||
: tmp;
|
||||
});
|
||||
const msgs = arr.filter(({ value }) => !!value);
|
||||
setMsgs(msgs);
|
||||
console.log("tmps", val, msgs);
|
||||
},
|
||||
[msgs]
|
||||
);
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// if (plateEditor) {
|
||||
// // 清空
|
||||
// Transforms.delete(plateEditor, {
|
||||
// at: {
|
||||
// anchor: Editor.start(plateEditor, []),
|
||||
// focus: Editor.end(plateEditor, []),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
// }, [plateEditor]);
|
||||
|
||||
return (
|
||||
<Styled className="input" ref={editableRef}>
|
||||
<Plate
|
||||
onChange={handleChange}
|
||||
id={`${TEXT_EDITOR_PREFIX}_${id}`}
|
||||
editableProps={{ ...initialProps, style: { userSelect: "text" } }}
|
||||
initialValue={initialValue}
|
||||
plugins={plugins}
|
||||
></Plate>
|
||||
</Styled>
|
||||
);
|
||||
};
|
||||
export default Plugins;
|
||||
// export default memo(Plugins, (prevs, nexts) => {
|
||||
// console.log("placeholder", prevs.placeholder, nexts.placeholder);
|
||||
// return prevs.placeholder == nexts.placeholder;
|
||||
// });
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
createBasicElementsPlugin,
|
||||
createImagePlugin,
|
||||
createParagraphPlugin,
|
||||
createSelectOnBackspacePlugin,
|
||||
} from "@udecode/plate";
|
||||
import { CONFIG } from "./config";
|
||||
|
||||
const basicElements = [
|
||||
createParagraphPlugin(), // paragraph element
|
||||
];
|
||||
|
||||
export const PLUGINS = {
|
||||
basicElements,
|
||||
basicNodes: [...basicElements],
|
||||
image: [
|
||||
createBasicElementsPlugin(),
|
||||
createImagePlugin(),
|
||||
createSelectOnBackspacePlugin(CONFIG.selectOnBackspace),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.div`
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #475467;
|
||||
> .box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
p {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Styled;
|
||||
@@ -0,0 +1,6 @@
|
||||
const nodeTypes = {
|
||||
paragraph: "p",
|
||||
image: "img",
|
||||
};
|
||||
|
||||
export default nodeTypes;
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
ELEMENT_PARAGRAPH,
|
||||
// TElement,
|
||||
} from "@udecode/plate";
|
||||
// import { Text } from 'slate'
|
||||
|
||||
export const createElement = (
|
||||
text = "",
|
||||
{ type = ELEMENT_PARAGRAPH, mark } = {}
|
||||
) => {
|
||||
const leaf = { text };
|
||||
if (mark) {
|
||||
leaf[mark] = true;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
children: [leaf],
|
||||
};
|
||||
};
|
||||
|
||||
export const getNodesWithRandomId = (nodes = []) => {
|
||||
let _id = 10000;
|
||||
nodes.forEach((node) => {
|
||||
node.id = _id;
|
||||
_id++;
|
||||
});
|
||||
|
||||
return nodes;
|
||||
};
|
||||
@@ -1,8 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import showdown from "showdown";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { Viewer } from "@toast-ui/react-editor";
|
||||
// import showdown from "showdown";
|
||||
// import showdownHighlight from "showdown-highlight";
|
||||
import styled from "styled-components";
|
||||
import ImagePreviewModal from "../../common/component/ImagePreviewModal";
|
||||
|
||||
// const { codeSyntaxHighlight } = Editor.plugin;
|
||||
const Styled = styled.div`
|
||||
p {
|
||||
* {
|
||||
user-select: text;
|
||||
}
|
||||
/* p {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
@@ -11,11 +19,6 @@ const Styled = styled.div`
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
i,
|
||||
em {
|
||||
/* padding: 0 2px; */
|
||||
font-style: italic;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
font-weight: 400;
|
||||
@@ -24,6 +27,10 @@ const Styled = styled.div`
|
||||
color: #475467;
|
||||
list-style-position: inside;
|
||||
}
|
||||
i,
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -56,15 +63,60 @@ const Styled = styled.div`
|
||||
line-height: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
pre .hljs {
|
||||
background-color: #eee;
|
||||
} */
|
||||
/* pre {
|
||||
user-select: text;
|
||||
padding: 16px;
|
||||
background-color: #0d1117;
|
||||
code {
|
||||
user-select: text;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #f5feff;
|
||||
}
|
||||
} */
|
||||
`;
|
||||
export default function MrakdownRender({ content }) {
|
||||
const [html, setHtml] = useState("");
|
||||
const mdContainer = useRef(null);
|
||||
const [previewImage, setPreviewImage] = useState(null);
|
||||
useEffect(() => {
|
||||
if (content) {
|
||||
const converter = new showdown.Converter();
|
||||
setHtml(converter.makeHtml(content));
|
||||
if (mdContainer) {
|
||||
const container = mdContainer.current;
|
||||
// 点击查看大图
|
||||
container.addEventListener(
|
||||
"click",
|
||||
(evt) => {
|
||||
console.log(evt);
|
||||
const { target } = evt;
|
||||
// 图片
|
||||
if (target.nodeType == 1) {
|
||||
setPreviewImage(target.dataset.origin || target.src);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
return <Styled dangerouslySetInnerHTML={{ __html: html }}></Styled>;
|
||||
}, []);
|
||||
const closePreviewModal = () => {
|
||||
setPreviewImage(null);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{previewImage && (
|
||||
<ImagePreviewModal
|
||||
image={previewImage}
|
||||
closeModal={closePreviewModal}
|
||||
/>
|
||||
)}
|
||||
<Styled ref={mdContainer}>
|
||||
<Viewer
|
||||
initialValue={content}
|
||||
// plugins={[[codeSyntaxHighlight]]}
|
||||
></Viewer>
|
||||
</Styled>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ import Styled from "./styled";
|
||||
import { CONFIG } from "./config";
|
||||
// import { VALUES } from "./values/values";
|
||||
|
||||
const id = "rustchat_richEditor";
|
||||
const id = "rustchat_rich_editor";
|
||||
|
||||
let components = createPlateUI({
|
||||
// customize your components by plugin key
|
||||
@@ -183,6 +183,7 @@ const Plugins = ({
|
||||
updatePureText("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Styled className="input" ref={editableRef}>
|
||||
<Plate
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import closeIcon from "../../../assets/icons/close.circle.svg?url";
|
||||
import pictureIcon from "../../../assets/icons/picture.svg?url";
|
||||
import { getFileIcon } from "../../utils";
|
||||
import { removeReplyingMessage } from "../../../app/slices/message";
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.div`
|
||||
@@ -41,6 +42,17 @@ const Styled = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.icon {
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
}
|
||||
.name {
|
||||
margin-left: 5px;
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
.close {
|
||||
background: none;
|
||||
@@ -51,7 +63,7 @@ const Styled = styled.div`
|
||||
}
|
||||
`;
|
||||
const renderContent = (data) => {
|
||||
const { content_type, content } = data;
|
||||
const { content_type, content, properties } = data;
|
||||
let res = null;
|
||||
switch (content_type) {
|
||||
case ContentTypes.text:
|
||||
@@ -61,7 +73,18 @@ const renderContent = (data) => {
|
||||
case ContentTypes.imageJPG:
|
||||
res = <img className="pic" src={pictureIcon} />;
|
||||
break;
|
||||
|
||||
case ContentTypes.file:
|
||||
{
|
||||
const { file_type, name } = properties;
|
||||
const icon = getFileIcon(file_type, name);
|
||||
res = (
|
||||
<>
|
||||
{icon}
|
||||
<span className="name">{name}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import React from 'react'
|
||||
import { useState, useRef } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import UploadModal from "../../component/UploadModal";
|
||||
import AddIcon from "../../../assets/icons/add.solid.svg";
|
||||
import MarkdownIcon from "../../../assets/icons/markdown.svg";
|
||||
const Styled = styled.div`
|
||||
@@ -8,9 +8,15 @@ const Styled = styled.div`
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
/* &.markdown {
|
||||
flex-direction: column;
|
||||
} */
|
||||
.md {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
.markdown path {
|
||||
fill: #22ccee;
|
||||
}
|
||||
}
|
||||
.add {
|
||||
cursor: pointer;
|
||||
@@ -28,30 +34,45 @@ const Styled = styled.div`
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default function Toolbar({
|
||||
contentType = "text",
|
||||
updateContentType,
|
||||
handleUpload,
|
||||
handleSend,
|
||||
}) {
|
||||
const toggleMarkdown = () => {
|
||||
updateContentType(contentType == "markdown" ? "text" : "markdown");
|
||||
export default function Toolbar({ toggleMode, mode, to, type }) {
|
||||
const [files, setFiles] = useState([]);
|
||||
const fileInputRef = useRef(null);
|
||||
const resetFiles = () => {
|
||||
setFiles([]);
|
||||
fileInputRef.current.value = "";
|
||||
};
|
||||
|
||||
const handleUpload = (evt) => {
|
||||
const files = [...evt.target.files];
|
||||
console.log("files", files);
|
||||
setFiles([...evt.target.files]);
|
||||
};
|
||||
return (
|
||||
<Styled>
|
||||
<div className="md" onClick={toggleMarkdown}>
|
||||
{contentType == "markdown" ? <MarkdownIcon /> : <MarkdownIcon />}
|
||||
</div>
|
||||
<div className="add">
|
||||
<AddIcon />
|
||||
<input
|
||||
multiple={true}
|
||||
onChange={handleUpload}
|
||||
type="file"
|
||||
name="file"
|
||||
id="file"
|
||||
<>
|
||||
{files.length !== 0 && (
|
||||
<UploadModal
|
||||
type={type}
|
||||
files={files}
|
||||
sendTo={to}
|
||||
closeModal={resetFiles}
|
||||
/>
|
||||
</div>
|
||||
</Styled>
|
||||
)}
|
||||
<Styled className={mode}>
|
||||
<div className="md" onClick={toggleMode}>
|
||||
<MarkdownIcon className={mode} />
|
||||
</div>
|
||||
<div className="add">
|
||||
<AddIcon />
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
multiple={false}
|
||||
onChange={handleUpload}
|
||||
type="file"
|
||||
name="file"
|
||||
id="file"
|
||||
/>
|
||||
</div>
|
||||
</Styled>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,122 +1,123 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { memo } from "react";
|
||||
// import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
// import { useKey } from "rooks";
|
||||
|
||||
import { getPlateEditorRef } from "@udecode/plate";
|
||||
import { updateInputMode } from "../../../app/slices/ui";
|
||||
import { removeReplyingMessage } from "../../../app/slices/message";
|
||||
import { useSendChannelMsgMutation } from "../../../app/services/channel";
|
||||
import { useSendMsgMutation } from "../../../app/services/contact";
|
||||
import { useReplyMessageMutation } from "../../../app/services/message";
|
||||
import StyledSend from "./styled";
|
||||
import useFiles from "./useFiles";
|
||||
import UploadModal from "./UploadModal";
|
||||
import Replying from "./Replying";
|
||||
import Toolbar from "./Toolbar";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
// import RichInput from "../RichInput";
|
||||
import RichInput from "../RichEditor";
|
||||
|
||||
import MarkdownEditor from "../MarkdownEditor";
|
||||
// import MarkdownInput from "../MarkdownInput";
|
||||
import MixedInput, { TEXT_EDITOR_PREFIX } from "../MixedInput";
|
||||
const Types = {
|
||||
channel: "#",
|
||||
user: "@",
|
||||
};
|
||||
export default function Send({
|
||||
const Modes = {
|
||||
text: "text",
|
||||
markdown: "markdown",
|
||||
};
|
||||
function Send({
|
||||
name,
|
||||
type = "channel",
|
||||
// 发给谁,或者是channel,或者是user
|
||||
id = "",
|
||||
dragFiles = [],
|
||||
}) {
|
||||
const [contentType, setContentType] = useState("text");
|
||||
// const editorRef = usePlateEditorRef(`${TEXT_EDITOR_PREFIX}_${type}_${id}`);
|
||||
const [replyMessage] = useReplyMessageMutation();
|
||||
const { files, setFiles, resetFiles } = useFiles([]);
|
||||
// const [shift, setShift] = useState(false);
|
||||
// const [enter, setEnter] = useState(false);
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [markdown, setMarkdown] = useState("");
|
||||
const [msg, setMsg] = useState("");
|
||||
const dispatch = useDispatch();
|
||||
// 谁发的
|
||||
const { from_uid, replying_mid = null } = useSelector((store) => {
|
||||
const { from_uid, replying_mid = null, mode } = useSelector((store) => {
|
||||
return {
|
||||
mode: store.ui.inputMode,
|
||||
from_uid: store.authData.uid,
|
||||
replying_mid: store.message.replying[id],
|
||||
};
|
||||
});
|
||||
useEffect(() => {
|
||||
if (dragFiles.length) {
|
||||
setFiles((prev) => [...prev, ...dragFiles]);
|
||||
}
|
||||
}, [dragFiles]);
|
||||
|
||||
const [sendMsg, { isLoading: userSending }] = useSendMsgMutation();
|
||||
const [
|
||||
sendChannelMsg,
|
||||
{ isLoading: channelSending },
|
||||
] = useSendChannelMsgMutation();
|
||||
const [sendMsg] = useSendMsgMutation();
|
||||
const [sendChannelMsg] = useSendChannelMsgMutation();
|
||||
const sendMessage = type == "channel" ? sendChannelMsg : sendMsg;
|
||||
const sendingMessage = userSending || channelSending;
|
||||
// const sendingMessage = userSending || channelSending;
|
||||
const insertEmoji = (emoji) => {
|
||||
console.log("insert emoji", emoji, editor);
|
||||
editor.insertText(emoji);
|
||||
};
|
||||
const handleUpload = (evt) => {
|
||||
setFiles([...evt.target.files]);
|
||||
};
|
||||
const handleSendMessage = () => {
|
||||
if (!markdown || !id || sendingMessage) return;
|
||||
console.log("send message", msg);
|
||||
const content_type = msg ? "text" : "markdown";
|
||||
const content = msg ? msg : markdown;
|
||||
console.log("current message", markdown, msg);
|
||||
if (replying_mid) {
|
||||
console.log("replying", replying_mid);
|
||||
replyMessage({
|
||||
id,
|
||||
reply_mid: replying_mid,
|
||||
type: content_type,
|
||||
content,
|
||||
context: type,
|
||||
from_uid,
|
||||
});
|
||||
dispatch(removeReplyingMessage(id));
|
||||
} else {
|
||||
sendMessage({
|
||||
id,
|
||||
type: content_type,
|
||||
content,
|
||||
from_uid,
|
||||
properties: { local_id: new Date().getTime() },
|
||||
});
|
||||
console.log("insert emoji", emoji);
|
||||
const editorRef = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${type}_${id}`);
|
||||
if (editorRef) {
|
||||
console.log("wtf", editorRef);
|
||||
editorRef.insertText(emoji);
|
||||
}
|
||||
setMsg("");
|
||||
};
|
||||
|
||||
const handleSendMessage = async (msgs = []) => {
|
||||
if (!msgs || msgs.length == 0 || !id) return;
|
||||
for await (const msg of msgs) {
|
||||
const { type, value: content } = msg;
|
||||
if (replying_mid) {
|
||||
console.log("replying", replying_mid);
|
||||
await replyMessage({
|
||||
id,
|
||||
reply_mid: replying_mid,
|
||||
type,
|
||||
content,
|
||||
context: type,
|
||||
from_uid,
|
||||
});
|
||||
dispatch(removeReplyingMessage(id));
|
||||
} else {
|
||||
await sendMessage({
|
||||
id,
|
||||
type,
|
||||
content,
|
||||
from_uid,
|
||||
properties: { local_id: new Date().getTime() },
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const sendMarkdown = (content) => {
|
||||
sendMessage({
|
||||
id,
|
||||
type: "markdown",
|
||||
content,
|
||||
from_uid,
|
||||
properties: { local_id: new Date().getTime() },
|
||||
});
|
||||
};
|
||||
const toggleMode = () => {
|
||||
dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<StyledSend className={`send ${replying_mid ? "reply" : ""} ${type}`}>
|
||||
{replying_mid && <Replying mid={replying_mid} id={id} />}
|
||||
<EmojiPicker selectEmoji={insertEmoji} />
|
||||
<RichInput
|
||||
setEditor={setEditor}
|
||||
<StyledSend
|
||||
className={`send ${mode} ${replying_mid ? "reply" : ""} ${type}`}
|
||||
>
|
||||
{replying_mid && <Replying mid={replying_mid} id={id} />}
|
||||
<EmojiPicker selectEmoji={insertEmoji} />
|
||||
{mode == Modes.text && (
|
||||
<MixedInput
|
||||
id={`${type}_${id}`}
|
||||
placeholder={`Send to ${Types[type]}${name} `}
|
||||
sendMessage={handleSendMessage}
|
||||
updateMarkdown={setMarkdown}
|
||||
updatePureText={setMsg}
|
||||
/>
|
||||
<Toolbar
|
||||
contentType={contentType}
|
||||
updateContentType={setContentType}
|
||||
handleUpload={handleUpload}
|
||||
/>
|
||||
</StyledSend>
|
||||
{files.length !== 0 && (
|
||||
<UploadModal
|
||||
type={type}
|
||||
files={files}
|
||||
sendTo={id}
|
||||
closeModal={resetFiles}
|
||||
sendMessages={handleSendMessage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{/* <MarkdownInput placeholder={`Send to ${Types[type]}${name} `} /> */}
|
||||
<Toolbar type={type} to={id} mode={mode} toggleMode={toggleMode} />
|
||||
{mode == Modes.markdown && (
|
||||
<MarkdownEditor
|
||||
placeholder={`Send to ${Types[type]}${name} `}
|
||||
// updateMarkdown={setMarkdown}
|
||||
sendMarkdown={sendMarkdown}
|
||||
/>
|
||||
)}
|
||||
</StyledSend>
|
||||
);
|
||||
}
|
||||
export default memo(Send, (prevs, nexts) => {
|
||||
console.log("send name", prevs.name, nexts.name);
|
||||
return prevs.name == nexts.name;
|
||||
});
|
||||
|
||||
@@ -5,20 +5,30 @@ const StyledSend = styled.div`
|
||||
background: #e5e7eb;
|
||||
border-radius: var(--br);
|
||||
width: 100%;
|
||||
/* width: fit-content; */
|
||||
/* min-height: 54px; */
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
|
||||
padding: 14px 18px;
|
||||
/* margin: 0 16px; */
|
||||
&.markdown {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0;
|
||||
.input {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
&.reply {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
/* padding: 4px 0; */
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DndProvider } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import "./assets/vars.css";
|
||||
import "animate.css";
|
||||
import MarkdownStyleOverride from "./common/component/MarkdownStyleOverride";
|
||||
import ReduxRoutes from "./routes";
|
||||
import BASE_URL from "./app/config";
|
||||
|
||||
@@ -21,6 +22,7 @@ ReactDOM.render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ReduxRoutes />
|
||||
</DndProvider>
|
||||
<MarkdownStyleOverride />
|
||||
</>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user