Markdown2Html/src/App.js
2024-08-06 10:13:26 +08:00

468 lines
13 KiB
JavaScript

import React, { Component } from "react";
import CodeMirror from "@uiw/react-codemirror";
import "codemirror/addon/search/searchcursor";
import "codemirror/keymap/sublime";
import "antd/dist/antd.css";
import { observer, inject } from "mobx-react";
import classnames from "classnames";
import throttle from "lodash.throttle";
import Dialog from "./layout/Dialog";
import Navbar from "./layout/Navbar";
import Toobar from "./layout/Toolbar";
import Footer from "./layout/Footer";
import Sidebar from "./layout/Sidebar";
import StyleEditor from "./layout/StyleEditor";
import EditorMenu from "./layout/EditorMenu";
import SearchBox from "./component/SearchBox";
import "./App.css";
import "./utils/mdMirror.css";
import {
LAYOUT_ID,
BOX_ID,
IMAGE_HOSTING_NAMES,
IMAGE_HOSTING_TYPE,
MJX_DATA_FORMULA,
MJX_DATA_FORMULA_TYPE
} from "./utils/constant";
import {
markdownParser,
markdownParserWechat,
updateMathjax
} from "./utils/helper";
import pluginCenter from "./utils/pluginCenter";
import appContext from "./utils/appContext";
import { uploadAdaptor } from "./utils/imageHosting";
import bindHotkeys, { betterTab, rightClick } from "./utils/hotkey";
@inject("content")
@inject("navbar")
@inject("footer")
@inject("view")
@inject("dialog")
@inject("imageHosting")
@observer
class App extends Component {
constructor(props) {
super(props);
this.scale = 1;
this.handleUpdateMathjax = throttle(updateMathjax, 1500);
this.state = {
focus: false
};
}
componentDidMount() {
document.addEventListener("fullscreenchange", this.solveScreenChange);
document.addEventListener("webkitfullscreenchange", this.solveScreenChange);
document.addEventListener("mozfullscreenchange", this.solveScreenChange);
document.addEventListener("MSFullscreenChange", this.solveScreenChange);
try {
window.MathJax = {
tex: {
inlineMath: [["$", "$"]],
displayMath: [["$$", "$$"]],
tags: "ams"
},
svg: {
fontCache: "none"
},
options: {
renderActions: {
addMenu: [0, "", ""],
addContainer: [
190,
doc => {
for (const math of doc.math) {
this.addContainer(math, doc);
}
},
this.addContainer
]
}
}
};
// eslint-disable-next-line
require("mathjax/es5/tex-svg-full");
pluginCenter.mathjax = true;
} catch (e) {
console.log(e);
}
this.setEditorContent();
this.setCustomImageHosting();
}
componentDidUpdate() {
if (pluginCenter.mathjax) {
this.handleUpdateMathjax();
}
}
componentWillUnmount() {
document.removeEventListener("fullscreenchange", this.solveScreenChange);
document.removeEventListener(
"webkitfullscreenchange",
this.solveScreenChange
);
document.removeEventListener("mozfullscreenchange", this.solveScreenChange);
document.removeEventListener("MSFullscreenChange", this.solveScreenChange);
}
setCustomImageHosting = () => {
if (this.props.useImageHosting === undefined) {
return;
}
const {
url,
name,
isSmmsOpen,
isQiniuyunOpen,
isAliyunOpen,
isGiteeOpen,
isGitHubOpen
} = this.props.useImageHosting;
if (name) {
this.props.imageHosting.setHostingUrl(url);
this.props.imageHosting.setHostingName(name);
this.props.imageHosting.addImageHosting(name);
}
if (isSmmsOpen) {
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.smms);
}
if (isAliyunOpen) {
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.aliyun);
}
if (isQiniuyunOpen) {
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.qiniuyun);
}
if (isGiteeOpen) {
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.gitee);
}
if (isGitHubOpen) {
this.props.imageHosting.addImageHosting(IMAGE_HOSTING_NAMES.github);
}
// 第一次进入没有默认图床时
if (window.localStorage.getItem(IMAGE_HOSTING_TYPE) === null) {
let type;
if (name) {
type = name;
} else if (isSmmsOpen) {
type = IMAGE_HOSTING_NAMES.smms;
} else if (isAliyunOpen) {
type = IMAGE_HOSTING_NAMES.aliyun;
} else if (isQiniuyunOpen) {
type = IMAGE_HOSTING_NAMES.qiniuyun;
} else if (isGiteeOpen) {
type = IMAGE_HOSTING_NAMES.isGitee;
}
this.props.imageHosting.setType(type);
window.localStorage.setItem(IMAGE_HOSTING_TYPE, type);
}
};
setEditorContent = () => {
const { defaultText } = this.props;
if (defaultText) {
this.props.content.setContent(defaultText);
}
};
setCurrentIndex(index) {
this.index = index;
}
solveScreenChange = () => {
const { isImmersiveEditing } = this.props.view;
this.props.view.setImmersiveEditing(!isImmersiveEditing);
};
getInstance = instance => {
instance.editor.on("inputRead", function(cm, event) {
if (event.origin === "paste") {
var text = event.text[0]; // pasted string
var new_text = ""; // any operations here
cm.refresh();
const { length } = cm.getSelections();
// my first idea was
// note: for multiline strings may need more complex calculations
cm.replaceRange(new_text, event.from, {
line: event.from.line,
ch: event.from.ch + text.length
});
// first solution did'nt work (before i guess to call refresh) so i tried that way, works too
if (length === 1) {
cm.execCommand("undo");
}
// cm.setCursor(event.from);
cm.replaceSelection(new_text);
}
});
if (instance) {
this.props.content.setMarkdownEditor(instance.editor);
}
};
handleScroll = () => {
if (this.props.navbar.isSyncScroll) {
const { markdownEditor } = this.props.content;
const cmData = markdownEditor.getScrollInfo();
const editorToTop = cmData.top;
const editorScrollHeight = cmData.height - cmData.clientHeight;
this.scale =
(this.previewWrap.offsetHeight -
this.previewContainer.offsetHeight +
55) /
editorScrollHeight;
if (this.index === 1) {
this.previewContainer.scrollTop = editorToTop * this.scale;
} else {
this.editorTop = this.previewContainer.scrollTop / this.scale;
markdownEditor.scrollTo(null, this.editorTop);
}
}
};
handleChange = editor => {
if (this.state.focus) {
const content = editor.getValue();
this.props.content.setContent(content);
this.props.onTextChange && this.props.onTextChange(content);
}
};
handleFocus = editor => {
this.setState({
focus: true
});
this.props.onTextFocus && this.props.onTextFocus(editor.getValue());
};
handleBlur = editor => {
this.setState({
focus: false
});
this.props.onTextBlur && this.props.onTextBlur(editor.getValue());
};
getStyleInstance = instance => {
if (instance) {
this.styleEditor = instance.editor;
this.styleEditor.on("keyup", (cm, e) => {
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
cm.showHint(e);
}
});
}
};
handleDrop = (instance, e) => {
// e.preventDefault();
// console.log(e.dataTransfer.files[0]);
if (!(e.dataTransfer && e.dataTransfer.files)) {
return;
}
for (let i = 0; i < e.dataTransfer.files.length; i++) {
// console.log(e.dataTransfer.files[i]);
uploadAdaptor({
file: e.dataTransfer.files[i],
content: this.props.content
});
}
};
handlePaste = (instance, e) => {
const cbData = e.clipboardData;
const insertPasteContent = (cm, content) => {
const { length } = cm.getSelections();
cm.replaceSelections(Array(length).fill(content));
this.setState(
{
focus: true
},
() => {
this.handleChange(cm);
}
);
};
if (e.clipboardData && e.clipboardData.files) {
for (let i = 0; i < e.clipboardData.files.length; i++) {
uploadAdaptor({
file: e.clipboardData.files[i],
content: this.props.content
});
}
}
if (cbData) {
const html = cbData.getData("text/html");
const text = cbData.getData("TEXT");
insertPasteContent(instance, text);
console.log(html);
if (html) {
console.log("htmsdkskdkskdk");
this.props.footer.setPasteHtmlChecked(true);
this.props.footer.setPasteHtml(html);
this.props.footer.setPasteText(text);
} else {
this.props.footer.setPasteHtmlChecked(false);
}
}
};
addContainer(math, doc) {
const tag = "span";
const spanClass = math.display
? "span-block-equation"
: "span-inline-equation";
const cls = math.display ? "block-equation" : "inline-equation";
math.typesetRoot.className = cls;
math.typesetRoot.setAttribute(MJX_DATA_FORMULA, math.math);
math.typesetRoot.setAttribute(MJX_DATA_FORMULA_TYPE, cls);
math.typesetRoot = doc.adaptor.node(
tag,
{ class: spanClass, style: "cursor:pointer" },
[math.typesetRoot]
);
}
render() {
const { codeNum, previewType } = this.props.navbar;
const {
isEditAreaOpen,
isPreviewAreaOpen,
isStyleEditorOpen,
isImmersiveEditing
} = this.props.view;
const { isSearchOpen } = this.props.dialog;
const parseHtml =
codeNum === 0
? markdownParserWechat.render(this.props.content.content)
: markdownParser.render(this.props.content.content);
const mdEditingClass = classnames({
"nice-md-editing": !isImmersiveEditing,
"nice-md-editing-immersive": isImmersiveEditing,
"nice-md-editing-hide": !isEditAreaOpen
});
const styleEditingClass = classnames({
"nice-style-editing": true,
"nice-style-editing-hide": isImmersiveEditing
});
const richTextClass = classnames({
"nice-marked-text": true,
"nice-marked-text-pc": previewType === "pc",
"nice-marked-text-hide": isImmersiveEditing || !isPreviewAreaOpen
});
const richTextBoxClass = classnames({
"nice-wx-box": true,
"nice-wx-box-pc": previewType === "pc"
});
const textContainerClass = classnames({
"nice-text-container": !isImmersiveEditing,
"nice-text-container-immersive": isImmersiveEditing
});
return (
<appContext.Consumer>
{({
defaultTitle,
onStyleChange,
onStyleBlur,
onStyleFocus,
token
}) => (
<div className="nice-app">
<Navbar title={defaultTitle} token={token} />
<Toobar token={token} />
<div className={textContainerClass}>
<div
id="nice-md-editor"
className={mdEditingClass}
onMouseOver={e => this.setCurrentIndex(1, e)}
>
{isSearchOpen && <SearchBox />}
<CodeMirror
value={this.props.content.content}
options={{
theme: "md-mirror",
keyMap: "sublime",
mode: "markdown",
lineWrapping: true,
lineNumbers: false,
extraKeys: {
...bindHotkeys(this.props.content, this.props.dialog),
Tab: betterTab,
RightClick: rightClick
}
}}
onChange={this.handleChange}
onScroll={this.handleScroll}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onDrop={this.handleDrop}
onPaste={this.handlePaste}
ref={this.getInstance}
/>
</div>
<div
id="nice-rich-text"
className={richTextClass}
onMouseOver={e => this.setCurrentIndex(2, e)}
>
<Sidebar />
<div
id={BOX_ID}
className={richTextBoxClass}
onScroll={this.handleScroll}
ref={node => {
this.previewContainer = node;
}}
>
<section
id={LAYOUT_ID}
data-tool="markdown2html编辑器"
data-website="https://md.luckday.cn"
dangerouslySetInnerHTML={{
__html: parseHtml
}}
ref={node => {
this.previewWrap = node;
}}
/>
</div>
</div>
{isStyleEditorOpen && (
<div id="nice-style-editor" className={styleEditingClass}>
<StyleEditor
onStyleChange={onStyleChange}
onStyleBlur={onStyleBlur}
onStyleFocus={onStyleFocus}
/>
</div>
)}
<Dialog />
<EditorMenu />
</div>
<Footer />
</div>
)}
</appContext.Consumer>
);
}
}
export default App;