492 lines
18 KiB
JavaScript
492 lines
18 KiB
JavaScript
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}`;
|
||
|
||
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=>{
|
||
console.log("markdown",atWhoVisible);
|
||
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) =>{
|
||
console.log("atwho列表",atWhoVisible);
|
||
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>
|
||
)
|
||
} |