forgeplus-react/src/modules/tpm/challengesnew/tpm-md-editor.js

491 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { Fragment, useEffect, useRef, useState } from 'react';
import { getUploadActionUrl, getUrl } from 'educoder';
import ResizeObserver from 'resize-observer-polyfill';
import { getImageUrl } from 'educoder';
import axios from 'axios';
import '../../courses/css/Courses.css';
import './css/TPMchallengesnew.css';
import 'codemirror/lib/codemirror.css';
import './css/newquestion.css';
const $ = window.$
const mdIcons = ["bold", "italic", "|", "list-ul", "list-ol", "|", "code", "code-block", "link", "|", "inline-latex", "latex", '|', "image", "table", '|', "line-break", "watch", "clear"];
const NULL_CH = '▁';
function md_add_data(k, mdu, d) {
window.sessionStorage.setItem(k + mdu, d);
}
// 清空保存的数据
function md_clear_data(k, mdu, id) {
window.sessionStorage.removeItem(k + mdu);
var id1 = "#e_tip_" + id;
var id2 = "#e_tips_" + id;
if (k == 'content') {
$(id2).html(" ");
} else {
$(id1).html(" ");
}
}
window.md_clear_data = md_clear_data
function md_rec_data(k, mdu, id) {
if (window.sessionStorage.getItem(k + mdu) !== null) {
var editor = $("#e_tips_" + id).data('editor');
editor.setValue(window.sessionStorage.getItem(k + mdu));
// /shixuns/b5hjq9zm/challenges/3977/tab=3 setValue可能导致editor样式问题
md_clear_data(k, mdu, id);
}
}
window.md_rec_data = md_rec_data;
function md_elocalStorage(editor, mdu, id) {
let oc = window.sessionStorage.getItem('content' + mdu)
if (oc !== null && oc !== editor.getValue()) {
$("#e_tips_" + id).data('editor', editor);
let h = '您上次有已保存的数据,是否<a style="cursor: pointer;" class="link-color-blue" onclick="md_rec_data(\'content\',\'' + mdu + '\',\'' + id + '\')">恢复</a> ? / <a style="cursor: pointer;" class="link-color-blue" onclick="md_clear_data(\'content\',\'' + mdu + '\',\'' + id + '\')">不恢复</a>';
$("#e_tips_" + id).html(h)
}
let tid = setInterval(function () {
let d = new Date();
let h = d.getHours();
let m = d.getMinutes();
let s = d.getSeconds();
h = h < 10 ? '0' + h : h;
m = m < 10 ? '0' + m : m;
s = s < 10 ? '0' + s : s;
if (editor.getValue().trim() !== "") {
md_add_data("content", mdu, editor.getValue());
let id2 = "#e_tips_" + id;
let textStart = " 数据已于 ";
let text = textStart + h + ':' + m + ':' + s + " 保存 ";
// 占位符
let oldHtml = $(id2).html();
if (oldHtml && oldHtml !== ' ' && oldHtml.startsWith(textStart) === false) {
$(id2).html(oldHtml.split(' (')[0] + ` (${text})`);
} else {
$(id2).html(text);
}
}
}, 10000)
return tid
}
export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, className = '', noStorage = false, imageExpand = true, placeholder = '', width = '100%', height = 400, initValue = '', emoji, watch, showNullButton = false, showResizeBar = false, startInit = true , forMember = true , isCanAtme = false ,changeAtWhoLoginList }) => {
const editorEl = useRef();
const resizeBarEl = useRef();
const [editorInstance, setEditorInstance] = useState();
const [atWhoVisible, setAtWhoVisible] = useState(false);
const [atWhoLoginListState, setAtWhoLoginListState] = useState([]);
const [users, setUsers] = useState([]);
const atWhoLoginList = useRef([]);
const containerId = `mdEditor_${mdID}`;
const editorBodyId = `mdEditors_${mdID}`;
const tipId = `e_tips_mdEditor_${mdID}`;
//todo需要替换成可@用户列表接口,由于forge接口名称冲突src\forge\UsersList\fork_users.js接口/members.json需要换成/forks.json
useEffect(()=>{
isCanAtme && axios.get('/users/list.json',{
params: {
search: 'admin',
},
}).then(response=>{
if(response && response.status === 200){
setUsers(response.data.users);
}
})
},[])
function onLayout() {
let ro;
if (editorEl.current) {
ro = new ResizeObserver(entries => {
for (let entry of entries) {
if (entry.target.offsetHeight > 0 || entry.target.offsetWidth > 0) {
editorInstance.resize();
editorInstance.cm.refresh();
editorInstance.cm.focus();
}
}
})
ro.observe(editorEl.current);
}
return ro;
}
function selectAtWho(username){
setAtWhoVisible(false);
const cm = editorInstance.cm;
//获取鼠标所在行的行数和ch
const cursor = cm.doc.getCursor();
const line = cursor.line;//行
const ch = cursor.ch;//列
//替换最后的内容
// cm.getLine(line).endsWith("@") ? cm.replaceRange(username+" ",{line,ch},{line,ch}) : cm.replaceRange("@"+username+" ",{line,ch},{line,ch})
cm.replaceRange(username+" ",{line,ch},{line,ch});
//鼠标聚焦
cm.focus();
//将此user的login存储到atWhoLoginList集合中
const list = new Set(atWhoLoginList.current);
users.map((item)=>{
item.username === username && list.add(item.login);
})
atWhoLoginList.current = Array.from(list);
setAtWhoLoginListState(Array.from(list));
}
function onMouseOver(key){
document.getElementsByClassName("at_who active")[0].className="at_who";
document.getElementsByClassName("at_who")[key].className="at_who active";
}
//markdown编辑器中输入的键盘监听事件
function mdKeyDown(){
document.onkeydown = e=>{
if (e.key === "@") {
// 输入@键后在对应的位置显示可选的项目成员
setAtWhoVisible(true);
//获取光标位置
const cssStyle = document.getElementsByClassName("CodeMirror cm-s-default CodeMirror-wrap")[0].firstChild.style;
//设置弹框位置
document.getElementById("at_who_list").style.top = (parseInt(cssStyle.getPropertyValue("top").replace("px",""))+60)+"px";
document.getElementById("at_who_list").style.left = (parseInt(cssStyle.getPropertyValue("left").replace("px",""))+20)+"px";
//将第一个用户默认选中
const at_who_divs = document.getElementsByClassName("at_who");
at_who_divs[0].className = "at_who active";
} else {
setAtWhoVisible(false);
}
//处理本来@了某人 -> 删掉 -> 撤回 的情况
if(e.code === "KeyZ" && users.length != 0){
const codemirror = editorInstance.cm;
let value = codemirror.getValue();
//处理初始内容就自带@谁的情况
if(initValue){
const del = [];
users.map(item=>{
if(initValue.indexOf(item.username)!=-1 && initValue.charAt(initValue.indexOf(item.username)-1) === "@" && initValue.indexOf(`@${item.username}`)===value.indexOf(`@${item.username}`)){
//初始内容中有符合@+名字的格式并且当前内容未删除初始内容
del[del.length] = `@${item.username}`;
}
})
del.length!=0 && del.map(str=>{
value = value.replace(str,"");
})
}
//判断value是否包含@符号
value.indexOf("@") != -1 && users.map(item =>{
if(value.indexOf(item.username)!=-1 && value.charAt(value.indexOf(item.username)-1) ==="@"){
//将此user的login存储到atWhoLoginList集合中
const list = new Set(atWhoLoginList.current);
list.add(item.login);
atWhoLoginList.current = Array.from(list);
setAtWhoLoginListState(Array.from(list));
}
})
}
}
}
//弹出可选@用户列表之后的键盘监听事件
function atWhoKeyDown(){
//监听上下和enter键
document.onkeydown = (e) =>{
const atWhoListDiv = document.getElementById("at_who_list");
const atWhoDivs = document.getElementsByClassName("at_who");
let index;
for(let i = 0; i<atWhoDivs.length;i++){
atWhoDivs[i].className === "at_who active" && (index = i);
}
if(e.key === "ArrowUp" && index > 0){
// index >=4 && (atWhoListDiv.scrollTop -=40)
atWhoListDiv.scrollTop -= 40;
atWhoDivs[index].className = "at_who";
atWhoDivs[index-1].className = "at_who active";
}
if(e.key === "ArrowDown" && index < atWhoDivs.length-1){
// index >=3 && (atWhoListDiv.scrollTop +=40)
atWhoListDiv.scrollTop += 40;
atWhoDivs[index].className = "at_who";
atWhoDivs[index+1].className = "at_who active";
}
if(e.key === "Enter"){
//阻止默认事件
e.preventDefault();
//找到classname为at_who active的div执行click事件
document.getElementsByClassName("at_who active")[0].click();
}
}
}
useEffect(()=>{
console.log('@谁列表发生变化,atWhoLoginList.current',atWhoLoginList.current,'atWhoLoginListState: ',atWhoLoginListState);
changeAtWhoLoginList && changeAtWhoLoginList(atWhoLoginListState);
},[atWhoLoginListState])
const atWhoList = (
<div className="at_who_list" id="at_who_list" >
{users && users.map((item,key)=>{
return(
<div key={key} className="at_who" onClick={()=>{selectAtWho(item.username)}} onMouseOver={()=>{onMouseOver(key)}}>
{item.image_url && <img src={getImageUrl(item.image_url)}></img>}
<span>{item.username}</span>
</div>
)
})}
</div>
)
useEffect(()=>{
document.addEventListener('click',()=>{setAtWhoVisible(false)});
})
useEffect(()=>{
//当atWhoVisible为true的时候失焦监听上下和enter键
if(atWhoVisible){
document.activeElement.id !== "blur_atWho" && document.getElementById("blur_atWho").focus();
document.addEventListener("keydown",atWhoKeyDown());
}
},[atWhoVisible])
useEffect(() => {
if (editorInstance) {
return
}
if (!startInit) { return }
const editor_instance = window.editormd(containerId, {
width,
height,
path: getUrl("/editormd/lib/"),
markdown: initValue,
syncScrolling: "single",
tex: true,
tocm: true,
emoji: !!emoji,
taskList: true,
codeFold: true,
searchReplace: true,
htmlDecode: "style,script,iframe",
sequenceDiagram: true,
autoFocus: false,
watch: watch === undefined ? true : watch,
saveHTMLToTextarea: true,
dialogMaskOpacity: 0.6,
placeholder: placeholder,
imageUpload: true,
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp", "JPG", "JPEG", "GIF", "PNG", "BMP", "WEBP"],
imageUploadURL: getUploadActionUrl(),
toolbarIcons: function () {
return showNullButton ? [...mdIcons, 'null-button'] : mdIcons
},
toolbarIconsClass: {
"line-break": "fa-minus",
"fullScreen":"iconfont icon-fangdaicon font-14"
},
toolbarCustomIcons: {
"inline-latex": "<a title='行内公式' class='latex' ><i name='inline-latex' class='fa iconfont icon-hangneigongshi font-14'></i></a>",
"latex": "<a title='多行公式' class='latex' ><i name='latex' class='fa iconfont icon-duohanggongshi font-16'></i></a>",
"null-button": "<a class='pr' title='增加填空'><i class='border-left'><span></span></i><span name='null-button' class='fa fillTip'>点击插入填空项</span><i class='iconfont fa icon-edit font-16' name='null-button'></i></a>",
},
toolbarHandlers: {
"line-break": function (cm, icon, cursor, selection) {
cm.replaceSelection("<br/>")
},
"null-button": function (cm, icon, cursor, selection) {
if (selection === "") {
cm.setCursor(cursor.line, cursor.ch + 1)
}
cm.replaceSelection(NULL_CH)
},
"fullScreen":function(cm,icon,cursor,selection){
icon.addClass("none");
console.log(cm,icon)
},
"inline-latex": function (cm, icon, cursor, selection) {
cm.replaceSelection("$$" + selection + "$$");
cm.setCursor(cursor.line, cursor.ch + 2);
cm.focus()
},
"latex": function (cm, icon, cursor, selection) {
cm.replaceSelection("```latex\n\n" + selection + "```");
cm.setCursor(cursor.line + 1, 0);
cm.focus()
},
},
lang: {
toolbar: {
"latex": "多行公式",
"line-break": "换行",
"fullScreen":"开启全屏"
}
},
onload: function () {
setEditorInstance(this)
}
})
}, [containerId, editorInstance, startInit])
const cmEl = editorInstance && editorInstance.cm
useEffect(() => {
if (cmEl) {
let tid = null
let ro
if (onCMBlur) {
editorInstance.cm.on('blur', () => { onCMBlur(editorInstance.getValue()) })
}
if (onCMBeforeChange) {
editorInstance.cm.on('beforeChange', (cm, change) => {
onCMBeforeChange(cm, change)
})
}
if (!noStorage) {
tid = md_elocalStorage(editorInstance, `MDEditor__${containerId}`, containerId)
}
//isCanAtme:只有issue和合并请求以及评论部分可以@他人操作
//当光标或选中内容时触发绑定@事件
isCanAtme && editorInstance.cm.on("focus", () => {
document.addEventListener("keydown", mdKeyDown());
});
isCanAtme && editorInstance.cm.on("blur", () => {
document.removeEventListener("keydown",mdKeyDown());
});
editorInstance.cm.on("change", (cm) => {
onChange && onChange(cm.getValue());
if(atWhoLoginList.current.length != 0){
const codemirror = editorInstance.cm;
let value = codemirror.getValue();
//处理初始内容就自带@谁的情况
if(initValue){
const del = [];
users.map(item=>{
if(initValue.indexOf(item.username)!=-1 && initValue.charAt(initValue.indexOf(item.username)-1) === "@" && initValue.indexOf(`@${item.username}`)===value.indexOf(`@${item.username}`)){
//初始内容中有符合@+名字的格式并且当前内容未删除初始内容
del[del.length] = `@${item.username}`;
}
})
del.length!=0 && del.map(str=>{
value = value.replace(str,"");
})
}
//以username为主键login为value的map集合
let atWhoMap = new Map();
Array.from(atWhoLoginList.current).map(item=>{
users.map(i=>{
if(i.login === item){
atWhoMap.set(i.username,i.login);
}
})
});
if(value.indexOf("@") === -1){
//已经有要@的列表,但是没有@符号 -> 清空@集合
atWhoLoginList.current = [];
setAtWhoLoginListState([]);
return;
}
const cursor = codemirror.doc.getCursor();
const line = cursor.line;
const ch = cursor.ch;
const lineContent = codemirror.getLine(line);
if(lineContent && lineContent.indexOf("@") != -1){//此行有@谁
Array.from(atWhoMap.keys()).map(username=>{
//判断lineContent是不是以列表中的某个username结尾
if(lineContent.endsWith(username)){
codemirror.setSelection({line,ch:ch-username.length-1},{line,ch});
return;
}
//处理有名字但是无@符号,有@但是名字对不上的情况
const a = value.indexOf(username)===-1;
const b = value.charAt(value.indexOf(username)-1) !="@";
if(value.indexOf(username)===-1 || value.charAt(value.indexOf(username)-1) !="@"){
//符合任意一种情况->踢掉这个人 不给他发消息
const list = new Set(atWhoLoginList.current);
list.delete(atWhoMap.get(username));
atWhoLoginList.current = Array.from(list);
setAtWhoLoginListState(Array.from(list));
}
})
}
}
});
ro = onLayout()
return () => {
if (!noStorage) {
clearInterval(tid)
}
if (ro) {
ro.unobserve(editorEl.current)
}
}
}
}, [cmEl])
useEffect(() => {
if (editorInstance && initValue !== undefined) {
if (initValue !== null && initValue !== editorInstance.getValue()) {
editorInstance.setValue(initValue.toString())
}
}
}, [editorInstance, initValue, containerId])
useEffect(() => {
if (resizeBarEl.current) {
let el = resizeBarEl.current
let dragging = false
let startY = 0
function onMouseDown(e) {
dragging = true
startY = e.pageY
}
function onMouseUp() {
dragging = false
}
function onMouseMove(e) {
if (dragging) {
let delta = e.pageY - startY
if (delta < 0) {
delta = 0
}
if (delta > 300) {
delta = 300
}
let resizeH = height + delta + 'px'
editorInstance.resize('', resizeH)
}
}
el.addEventListener('mousedown', onMouseDown)
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
return () => {
el.removeEventListener('mousedown', onMouseDown)
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
}
}, [
editorInstance, resizeBarEl
])
return (
<Fragment>
<div ref={editorEl} className={`df ${className} ${imageExpand && 'editormd-image-click-expand'} `}>
<div className={`edu-back-greyf5 radius4 editormd ${error ? 'error' : ''}`} id={containerId} >
<textarea style={{ display: 'none' }} id={editorBodyId} name="content"></textarea>
<div className="CodeMirror cm-s-defualt"></div>
<input id ="blur_atWho" className="blur_atWho"/>
{atWhoVisible && atWhoList}
</div>
</div>
{showResizeBar ? <a ref={resizeBarEl} className='editor-resize'></a> : null}
<div className={"fr rememberTip"}>
{noStorage === true ? null : <div id={tipId} className="edu-txt-right color-grey-cd font-12"></div>}
</div>
</Fragment>
)
}