feat: markdown fullscreen

This commit is contained in:
zerosoul
2022-05-06 21:19:46 +08:00
parent 4a3b478082
commit 66215e6c4a
8 changed files with 316 additions and 258 deletions
+7 -2
View File
@@ -10,7 +10,12 @@ import useUploadFile from "../../hook/useUploadFile";
import Button from "../../component/styled/Button";
function MarkdownEditor({ placeholder, sendMarkdown, setEditorInstance }) {
function MarkdownEditor({
height = "50vh",
placeholder,
sendMarkdown,
setEditorInstance,
}) {
const editorRef = useRef(undefined);
const { uploadFile } = useUploadFile();
// const [pHolder, setPHolder] = useState(placeholder);
@@ -52,7 +57,7 @@ function MarkdownEditor({ placeholder, sendMarkdown, setEditorInstance }) {
hideModeSwitch={true}
// initialValue="hello world!"
previewStyle="vertical"
height="50vh"
height={height}
initialEditType="markdown"
useCommandShortcut={true}
/>
@@ -5,6 +5,10 @@ const MarkdownOverrides = createGlobalStyle`
[class^='toastui-editor-']{
.toastui-editor-md-container{
border-bottom: none;
.toastui-editor-md-preview{
padding-right: 0;
padding-left: 8px;
}
.toastui-editor-md-splitter{
background-color:#D0D5DD ;
}
@@ -16,13 +20,14 @@ const MarkdownOverrides = createGlobalStyle`
margin: 0 ;
padding: 0;
}
p,.toastui-editor.md-mode {
.ProseMirror,p,.toastui-editor.md-mode {
margin:0 ;
font-weight: 400;
font-size: 14px;
line-height: 22px;
color: #475467;
margin-bottom: 16px;
word-break: break-all;
}
a{
background-color: transparent;
+258 -250
View File
@@ -1,266 +1,274 @@
import { useRef, useEffect, useState, useCallback } from 'react';
import { useKey } from 'rooks';
import { useDispatch, useSelector } from 'react-redux';
import { Editor, Transforms } from 'slate';
import { useRef, useEffect, useState, useCallback } from "react";
import { useKey } from "rooks";
import { useDispatch, useSelector } from "react-redux";
import { Editor, Transforms } from "slate";
import {
createPlateUI,
Plate,
createExitBreakPlugin,
createTrailingBlockPlugin,
createNodeIdPlugin,
createParagraphPlugin,
// createImagePlugin,
createSoftBreakPlugin,
// createComboboxPlugin,
createMentionPlugin,
// comboboxSelectors,
// getMentionOnSelectItem,
// findMentionInput,
// removeMentionInput,
// isSelectionInMentionInput,
createPlugins,
ELEMENT_PARAGRAPH,
getPlateEditorRef,
// usePlateEditorRef,
// ELEMENT_IMAGE,
MentionCombobox
} from '@udecode/plate';
import { createComboboxPlugin } from '@udecode/plate-combobox';
import { ReactEditor } from 'slate-react';
import Styled from './styled';
import { CONFIG } from './config';
import Contact from '../Contact';
import { updateUploadFiles } from '../../../app/slices/ui';
export const TEXT_EDITOR_PREFIX = 'rustchat_text_editor';
createPlateUI,
Plate,
createExitBreakPlugin,
createTrailingBlockPlugin,
createNodeIdPlugin,
createParagraphPlugin,
// createImagePlugin,
createSoftBreakPlugin,
// createComboboxPlugin,
createMentionPlugin,
// comboboxSelectors,
// getMentionOnSelectItem,
// findMentionInput,
// removeMentionInput,
// isSelectionInMentionInput,
createPlugins,
ELEMENT_PARAGRAPH,
getPlateEditorRef,
// usePlateEditorRef,
// ELEMENT_IMAGE,
MentionCombobox,
} from "@udecode/plate";
import { createComboboxPlugin } from "@udecode/plate-combobox";
import { ReactEditor } from "slate-react";
import Styled from "./styled";
import { CONFIG } from "./config";
import Contact from "../Contact";
import { updateUploadFiles } from "../../../app/slices/ui";
export const TEXT_EDITOR_PREFIX = "rustchat_text_editor";
let components = createPlateUI({
// [ELEMENT_IMAGE]: ImageElement,
// customize your components by plugin key
// [ELEMENT_IMAGE]: ImageElement,
// customize your components by plugin key
});
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: '' }] }];
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: "" }] }];
const Plugins = ({
id = '',
placeholder = 'Write some markdown...',
sendMessages,
members = []
id = "",
placeholder = "Write some markdown...",
sendMessages,
members = [],
}) => {
const dispatch = useDispatch();
const enableMentions = members.length > 0;
const filesRef = useRef([]);
const contactData = useSelector((store) => store.contacts.byId);
const [msgs, setMsgs] = useState([]);
const [cmdKey, setCmdKey] = useState(false);
const editableRef = useRef(null);
const initialProps = {
...CONFIG.editableProps,
className: 'box',
placeholder
};
useEffect(() => {
const handlePasteEvent = (evt) => {
const files = [...evt.clipboardData.files];
if (files.length) {
const filesData = files.map((file) => {
const { size, type, name } = file;
console.log('paste event name', name);
const url = URL.createObjectURL(file);
return { size, type, name, url };
});
const [context, to] = id.split('_');
console.log('paste event', context, to, files, evt);
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
}
const dispatch = useDispatch();
const enableMentions = members.length > 0;
const filesRef = useRef([]);
const contactData = useSelector((store) => store.contacts.byId);
const [msgs, setMsgs] = useState([]);
const [cmdKey, setCmdKey] = useState(false);
const editableRef = useRef(null);
const initialProps = {
...CONFIG.editableProps,
className: "box",
placeholder,
};
window.addEventListener('paste', handlePasteEvent);
return () => {
window.removeEventListener('paste', handlePasteEvent);
};
// window.addEventListener("paste")
}, []);
useEffect(() => {
const handlePasteEvent = (evt) => {
const files = [...evt.clipboardData.files];
if (files.length) {
const filesData = files.map((file) => {
const { size, type, name } = file;
console.log("paste event name", name);
const url = URL.createObjectURL(file);
return { size, type, name, url };
});
const [context, to] = id.split("_");
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 = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${id}`);
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 pluginArr = [
createParagraphPlugin(),
createNodeIdPlugin(),
createSoftBreakPlugin(CONFIG.softBreak),
createTrailingBlockPlugin(CONFIG.trailingBlock),
createExitBreakPlugin(CONFIG.exitBreak)
];
const plugins = createPlugins(
enableMentions
? pluginArr.concat([
createComboboxPlugin(),
createMentionPlugin({
options: {
createMentionNode: (item) => {
console.log('mention', item);
const {
text,
data: { uid }
} = item;
return { value: `@${text}`, uid };
},
insertSpaceAfterMention: true
}
})
])
: pluginArr,
{
components
}
);
const handleChange = useCallback(
async (val) => {
console.log('tmps changed', val);
const tmps = [];
const getMixedText = (children) => {
const mentions = [];
const arr = children.map(({ type, text, uid }) => {
if (type == 'mention') {
mentions.push(uid);
return ` @${uid} `;
}
return text;
});
return { value: arr.join(''), mentions };
};
for (const v of val) {
if (v.type == 'img') {
// img
const url = v.url;
const file_path = decodeURIComponent(new URL(url).searchParams.get('file_path'));
console.log('files', filesRef.current, file_path);
const json = filesRef.current.find((f) => f.path == file_path) || {};
const { name, size, hash, path, ...rest } = json;
tmps.push({
type: 'file',
content: { name, size, hash, path },
properties: rest
});
} else {
// p
const { value, mentions } = getMixedText(v.children);
const prev = tmps[tmps.length - 1];
if (!prev) {
tmps.push([{ type: 'text', content: value, properties: { mentions } }]);
} else {
if (Array.isArray(prev)) {
tmps[tmps.length - 1].push({
type: 'text',
content: value,
properties: { mentions }
});
} else {
tmps.push([{ type: 'text', content: value, properties: { mentions } }]);
console.log("paste event", context, to, files, evt);
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
}
}
}
}
const arr = tmps.map((tmp) => {
return Array.isArray(tmp)
? {
type: 'text',
content: tmp.map((t) => t.content).join('\n'),
properties: {
mentions: tmp.map((t) => t.properties?.mentions || []).flat()
}
}
: tmp;
});
const msgs = arr.filter(({ content }) => !!content);
setMsgs(msgs);
console.log('tmps', tmps, arr, msgs);
},
[msgs]
);
};
window.addEventListener("paste", handlePasteEvent);
return () => {
window.removeEventListener("paste", handlePasteEvent);
};
// window.addEventListener("paste")
}, []);
return (
<Styled className="input" ref={editableRef}>
<Plate
onChange={handleChange}
id={`${TEXT_EDITOR_PREFIX}_${id}`}
editableProps={{ ...initialProps, style: { userSelect: 'text' } }}
initialValue={initialValue}
plugins={plugins}
>
{enableMentions ? (
<MentionCombobox
// component={StyledCombobox}
onRenderItem={({ item }) => {
console.log('wtf', item);
return <Contact uid={item.data.uid} interactive={false} />;
}}
items={members.map((id) => {
const data = contactData[id];
if (!data) return null;
const { uid, name, ...rest } = data;
return {
key: uid,
text: name,
data: {
uid,
...rest
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 = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${id}`);
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 pluginArr = [
createParagraphPlugin(),
createNodeIdPlugin(),
createSoftBreakPlugin(CONFIG.softBreak),
createTrailingBlockPlugin(CONFIG.trailingBlock),
createExitBreakPlugin(CONFIG.exitBreak),
];
const plugins = createPlugins(
enableMentions
? pluginArr.concat([
createComboboxPlugin(),
createMentionPlugin({
options: {
createMentionNode: (item) => {
console.log("mention", item);
const {
text,
data: { uid },
} = item;
return { value: `@${text}`, uid };
},
insertSpaceAfterMention: true,
},
}),
])
: pluginArr,
{
components,
}
);
const handleChange = useCallback(
async (val) => {
console.log("tmps changed", val);
const tmps = [];
const getMixedText = (children) => {
const mentions = [];
const arr = children.map(({ type, text, uid }) => {
if (type == "mention") {
mentions.push(uid);
return ` @${uid} `;
}
return text;
});
return { value: arr.join(""), mentions };
};
for (const v of val) {
if (v.type == "img") {
// img
const url = v.url;
const file_path = decodeURIComponent(
new URL(url).searchParams.get("file_path")
);
console.log("files", filesRef.current, file_path);
const json = filesRef.current.find((f) => f.path == file_path) || {};
const { name, size, hash, path, ...rest } = json;
tmps.push({
type: "file",
content: { name, size, hash, path },
properties: rest,
});
} else {
// p
const { value, mentions } = getMixedText(v.children);
const prev = tmps[tmps.length - 1];
if (!prev) {
tmps.push([
{ type: "text", content: value, properties: { mentions } },
]);
} else {
if (Array.isArray(prev)) {
tmps[tmps.length - 1].push({
type: "text",
content: value,
properties: { mentions },
});
} else {
tmps.push([
{ type: "text", content: value, properties: { mentions } },
]);
}
}
}
};
})}
/>
) : null}
</Plate>
</Styled>
);
}
const arr = tmps.map((tmp) => {
return Array.isArray(tmp)
? {
type: "text",
content: tmp.map((t) => t.content).join("\n"),
properties: {
mentions: tmp.map((t) => t.properties?.mentions || []).flat(),
},
}
: tmp;
});
const msgs = arr.filter(({ content }) => !!content);
setMsgs(msgs);
console.log("tmps", tmps, arr, msgs);
},
[msgs]
);
return (
<Styled className="input" ref={editableRef}>
<Plate
onChange={handleChange}
id={`${TEXT_EDITOR_PREFIX}_${id}`}
editableProps={{ ...initialProps, style: { userSelect: "text" } }}
initialValue={initialValue}
plugins={plugins}
>
{enableMentions ? (
<MentionCombobox
// component={StyledCombobox}
onRenderItem={({ item }) => {
console.log("wtf", item);
return <Contact uid={item.data.uid} interactive={false} />;
}}
items={members.map((id) => {
const data = contactData[id];
if (!data) return null;
const { uid, name, ...rest } = data;
return {
key: uid,
text: name,
data: {
uid,
...rest,
},
};
})}
/>
) : null}
</Plate>
</Styled>
);
};
export const useMixedEditor = (key) => {
const editorRef = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${key}`);
const focus = () => {
if (editorRef) {
ReactEditor.focus(editorRef);
}
};
const insertText = (txt) => {
if (editorRef) {
editorRef.insertText(txt);
}
};
return {
focus,
insertText
};
const editorRef = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${key}`);
const focus = () => {
if (editorRef) {
ReactEditor.focus(editorRef);
}
};
const insertText = (txt) => {
console.log("eref", editorRef);
if (editorRef) {
ReactEditor.focus(editorRef);
editorRef.insertText(txt);
}
};
return {
focus,
insertText,
};
};
export default Plugins;
+23 -4
View File
@@ -5,6 +5,8 @@ import { updateUploadFiles } from "../../../app/slices/ui";
import Tooltip from "../../component/Tooltip";
import AddIcon from "../../../assets/icons/add.solid.svg";
import MarkdownIcon from "../../../assets/icons/markdown.svg";
import FullscreenIcon from "../../../assets/icons/fullscreen.svg";
import ExitFullscreenIcon from "../../../assets/icons/fullscreen.exit.svg";
const Styled = styled.div`
display: flex;
align-items: center;
@@ -16,6 +18,7 @@ const Styled = styled.div`
.md {
cursor: pointer;
display: flex;
gap: 14px;
.markdown path {
fill: #22ccee;
}
@@ -39,10 +42,16 @@ const Styled = styled.div`
}
}
`;
export default function Toolbar({ toggleMode, mode, to, context }) {
export default function Toolbar({
toggleMarkdownFullscreen,
fullscreen,
toggleMode,
mode,
to,
context,
}) {
const dispatch = useDispatch();
const fileInputRef = useRef(null);
const handleUpload = (evt) => {
const files = [...evt.target.files];
console.log("files", files);
@@ -58,10 +67,20 @@ export default function Toolbar({ toggleMode, mode, to, context }) {
};
return (
<Styled className={mode}>
<div className="md" onClick={toggleMode}>
<div className="md">
<Tooltip placement="top" tip="Markdown">
<MarkdownIcon className={mode} />
<MarkdownIcon className={mode} onClick={toggleMode} />
</Tooltip>
{mode == "markdown" &&
(fullscreen ? (
<Tooltip placement="top" tip="Exit Fullscreen">
<ExitFullscreenIcon onClick={toggleMarkdownFullscreen} />
</Tooltip>
) : (
<Tooltip placement="top" tip="Fullscreen">
<FullscreenIcon onClick={toggleMarkdownFullscreen} />
</Tooltip>
))}
</div>
<Tooltip placement="top" tip="Upload">
<div className="add">
+13 -1
View File
@@ -32,6 +32,7 @@ function Send({
}) {
const editor = useMixedEditor(`${context}_${id}`);
const [markdownEditor, setMarkdownEditor] = useState(null);
const [markdownFullscreen, setMarkdownFullscreen] = useState(false);
const dispatch = useDispatch();
const addLocalFileMesage = useAddLocalFileMessage({ context, to: id });
// 谁发的
@@ -67,6 +68,7 @@ function Send({
// markdown insert emoji
markdownEditor.insertText(emoji);
} else {
console.log("emojii", emoji);
editor.insertText(emoji);
}
};
@@ -128,6 +130,9 @@ function Send({
const toggleMode = () => {
dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text));
};
const toggleMarkdownFullscreen = () => {
setMarkdownFullscreen((prev) => !prev);
};
const name =
context == "channel" ? channelsData[id]?.name : contactsData[id]?.name;
const placeholder = `Send to ${Types[context]}${name} `;
@@ -138,7 +143,11 @@ function Send({
: channelsData[id]?.members
: [];
return (
<StyledSend className={`send ${replying_mid ? "reply" : ""} ${context}`}>
<StyledSend
className={`send ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${
replying_mid ? "reply" : ""
} ${context}`}
>
{replying_mid && <Replying mid={replying_mid} id={id} />}
{mode == Modes.text && <UploadFileList context={context} id={id} />}
@@ -157,9 +166,12 @@ function Send({
to={id}
mode={mode}
toggleMode={toggleMode}
fullscreen={markdownFullscreen}
toggleMarkdownFullscreen={toggleMarkdownFullscreen}
/>
{mode == Modes.markdown && (
<MarkdownEditor
height={markdownFullscreen ? `calc(100vh - 168px)` : `30vh`}
placeholder={placeholder}
setEditorInstance={setMarkdownEditor}
sendMarkdown={sendMarkdown}
+3
View File
@@ -6,6 +6,9 @@ const StyledSend = styled.div`
border-radius: var(--br);
width: 100%;
width: -webkit-fill-available;
&.markdown.fullscreen {
margin-top: -35px;
}
.send_box {
display: flex;
justify-content: space-between;