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
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 4C9 3.44772 8.55228 3 8 3C7.44772 3 7 3.44772 7 4V6.5C7 6.77614 6.77614 7 6.5 7H4C3.44772 7 3 7.44772 3 8C3 8.55228 3.44772 9 4 9H6.5C7.88071 9 9 7.88071 9 6.5V4ZM9 20C9 20.5523 8.55228 21 8 21C7.44772 21 7 20.5523 7 20V17.5C7 17.2239 6.77614 17 6.5 17H4C3.44772 17 3 16.5523 3 16C3 15.4477 3.44772 15 4 15H6.5C7.88071 15 9 16.1193 9 17.5V20ZM16 3C15.4477 3 15 3.44772 15 4V6.5C15 7.88071 16.1193 9 17.5 9H20C20.5523 9 21 8.55228 21 8C21 7.44772 20.5523 7 20 7H17.5C17.2239 7 17 6.77614 17 6.5V4C17 3.44772 16.5523 3 16 3ZM15 20C15 20.5523 15.4477 21 16 21C16.5523 21 17 20.5523 17 20V17.5C17 17.2239 17.2239 17 17.5 17H20C20.5523 17 21 16.5523 21 16C21 15.4477 20.5523 15 20 15H17.5C16.1193 15 15 16.1193 15 17.5V20Z" fill="#667085"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 6C5 5.44772 5.44772 5 6 5H8C8.55228 5 9 4.55228 9 4C9 3.44772 8.55228 3 8 3H6C4.34315 3 3 4.34315 3 6V8C3 8.55228 3.44772 9 4 9C4.55228 9 5 8.55228 5 8V6ZM5 18C5 18.5523 5.44772 19 6 19H8C8.55228 19 9 19.4477 9 20C9 20.5523 8.55228 21 8 21H6C4.34315 21 3 19.6569 3 18V16C3 15.4477 3.44772 15 4 15C4.55228 15 5 15.4477 5 16V18ZM18 5C18.5523 5 19 5.44772 19 6V8C19 8.55228 19.4477 9 20 9C20.5523 9 21 8.55228 21 8V6C21 4.34315 19.6569 3 18 3H16C15.4477 3 15 3.44772 15 4C15 4.55228 15.4477 5 16 5H18ZM19 18C19 18.5523 18.5523 19 18 19H16C15.4477 19 15 19.4477 15 20C15 20.5523 15.4477 21 16 21H18C19.6569 21 21 19.6569 21 18V16C21 15.4477 20.5523 15 20 15C19.4477 15 19 15.4477 19 16V18Z" fill="#667085"/>
</svg>

After

Width:  |  Height:  |  Size: 819 B

+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;