forked from Gitlink/forgeplus-react
This commit is contained in:
parent
0accc419de
commit
7b430d8257
|
|
@ -1,16 +1,19 @@
|
|||
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react'
|
||||
import { getUploadActionUrl, getUrl } from 'educoder'
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { getUploadActionUrl, getUrl } from 'educoder';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
// import myxss from '../../../common/filterXss';
|
||||
import xss from 'xss';
|
||||
import '../../courses/css/Courses.css'
|
||||
import './css/TPMchallengesnew.css'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
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 mdIcons = ["bold", "italic", "|", "list-ul", "list-ol", "|", "code", "code-block", "link", "|", "inline-latex", "latex", '|', "image", "table", '|', "line-break", "watch", "clear","fullscreen"];
|
||||
|
||||
const NULL_CH = '▁'
|
||||
const NULL_CH = '▁';
|
||||
|
||||
function md_add_data(k, mdu, d) {
|
||||
window.sessionStorage.setItem(k + mdu, d);
|
||||
|
|
@ -39,10 +42,9 @@ function md_rec_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()) {
|
||||
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)
|
||||
|
|
@ -56,15 +58,15 @@ function md_elocalStorage(editor, mdu, id) {
|
|||
m = m < 10 ? '0' + m : m;
|
||||
s = s < 10 ? '0' + s : s;
|
||||
|
||||
if (editor.getValue().trim() != "") {
|
||||
if (editor.getValue().trim() !== "") {
|
||||
md_add_data("content", mdu, editor.getValue());
|
||||
let id2 = "#e_tips_" + id;
|
||||
|
||||
let textStart = " 数据已于 "
|
||||
let textStart = " 数据已于 ";
|
||||
let text = textStart + h + ':' + m + ':' + s + " 保存 ";
|
||||
// 占位符
|
||||
let oldHtml = $(id2).html();
|
||||
if (oldHtml && oldHtml != ' ' && oldHtml.startsWith(textStart) == false) {
|
||||
if (oldHtml && oldHtml !== ' ' && oldHtml.startsWith(textStart) === false) {
|
||||
$(id2).html(oldHtml.split(' (')[0] + ` (${text})`);
|
||||
} else {
|
||||
$(id2).html(text);
|
||||
|
|
@ -74,33 +76,257 @@ function md_elocalStorage(editor, mdu, id) {
|
|||
return tid
|
||||
}
|
||||
|
||||
export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, className = '', noStorage = false, imageExpand = true, placeholder = '', width = '100%', height = 400, initValue = '', emoji, watch=true, showNullButton = false, showResizeBar = false, startInit = true , forMember = true , isCanAtme = false , isQuoteIssue = false , changeAtWhoLoginList, owner, projectsId , isFocus = true}) => {
|
||||
|
||||
const editorEl = useRef();
|
||||
const resizeBarEl = useRef();
|
||||
const [editorInstance, setEditorInstance] = useState();
|
||||
const [atWhoVisible, setAtWhoVisible] = useState(false);
|
||||
const [atWhoLoginListState, setAtWhoLoginListState] = useState([]);
|
||||
//调用member.json接口获取到的用户列表
|
||||
const [users, setUsers] = useState([]);
|
||||
//可以@的全部用户
|
||||
const [allUsers, setAllUsers] = useState([]);
|
||||
const atWhoLoginList = useRef([]);
|
||||
const atWhoVisibleRef = useRef(false);
|
||||
const containerId = `mdEditor_${mdID}`;
|
||||
const editorBodyId = `mdEditors_${mdID}`;
|
||||
const tipId = `e_tips_mdEditor_${mdID}`;
|
||||
|
||||
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 }) => {
|
||||
// quote相关
|
||||
const [issues, setIssues ] = useState([]);
|
||||
const [quoteVisible, setQuoteVisible] = useState(false);
|
||||
const [screenIssues, setScreenIssues ] = useState([]);
|
||||
const quoteVisibleRef = useRef(false);
|
||||
const chooseIssuesList = useRef([]);
|
||||
|
||||
const editorEl = useRef()
|
||||
const resizeBarEl = useRef()
|
||||
const [editorInstance, setEditorInstance] = useState()
|
||||
const containerId = `mdEditor_${mdID}`
|
||||
const editorBodyId = `mdEditors_${mdID}`
|
||||
const tipId = `e_tips_mdEditor_${mdID}`
|
||||
useEffect(()=>{
|
||||
//请求members接口获取全部可@列表
|
||||
isCanAtme && axios.get(`/${owner}/${projectsId}/members.json`).then(response=>{
|
||||
if(response.data.total_count !== 0){
|
||||
setAllUsers(response.data.users);
|
||||
setUsers(response.data.users);
|
||||
}
|
||||
})
|
||||
//点击其他地方关闭弹框
|
||||
document.addEventListener('click',()=>{
|
||||
atWhoVisibleRef.current = false;
|
||||
setAtWhoVisible(false);
|
||||
quoteVisibleRef.current = false;
|
||||
setQuoteVisible(false);
|
||||
})
|
||||
|
||||
isQuoteIssue && getIssueList();
|
||||
},[])
|
||||
|
||||
// 请求issues接口获取全部可quote的issue列表
|
||||
function getIssueList(keyword){
|
||||
axios.get(`/v1/${owner}/${projectsId}/issues`,{params:{
|
||||
only_name:true,keyword,sort_direction:"desc",sort_by:"issues.created_on"
|
||||
}}).then(result=>{
|
||||
if(result){
|
||||
!keyword && setIssues(result.data.issues);
|
||||
let data = result.data.issues;
|
||||
setScreenIssues(data && data.length > 0 ? data:undefined);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// # 引用的弹框div
|
||||
const QuoteDiv = (
|
||||
<div id="quoteDiv" className="quoteDiv">
|
||||
{
|
||||
screenIssues && screenIssues.map((i,k)=>{
|
||||
return(
|
||||
<li className={`quote ${k === 0 && "active"}`} onClick={()=>selectQuoteIssue(i)} onMouseOver={()=>onMouseOverQuote(k)}>
|
||||
<span style={{minWidth:"40px"}}><span className="issueIndex" title={i.project_issues_index}>#{i.project_issues_index}</span></span>
|
||||
<span className="issueName task-hide">{i.subject}</span>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
// 点击选择要引用的issue
|
||||
function selectQuoteIssue(i){
|
||||
quoteVisibleRef.current = false;
|
||||
setQuoteVisible(false);
|
||||
const cm = editorInstance.cm;
|
||||
//获取鼠标所在行的行数和ch
|
||||
const cursor = cm.doc.getCursor();
|
||||
const line = cursor.line;//行
|
||||
const ch = cursor.ch;//列
|
||||
const startIndex = cm.getRange({line,ch:0},{line,ch}).lastIndexOf("#");
|
||||
//替换内容
|
||||
cm.replaceRange(`[#${i.project_issues_index}](/${owner}/${projectsId}/issues/${i.project_issues_index}) `,{line,ch:startIndex},{line,ch});
|
||||
//鼠标聚焦
|
||||
cm.focus();
|
||||
// 保存引用的issue
|
||||
const list = new Set(chooseIssuesList.current);
|
||||
list.add(i.project_issues_index);
|
||||
chooseIssuesList.current = Array.from(list);
|
||||
}
|
||||
function onMouseOverQuote(key){
|
||||
document.getElementsByClassName("quote active")[0] && (document.getElementsByClassName("quote active")[0].className="quote");
|
||||
document.getElementsByClassName("quote")[key] && (document.getElementsByClassName("quote")[key].className="quote active");
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
if(cmEl){
|
||||
if(quoteVisibleRef.current){
|
||||
const atWhoListDiv = document.getElementById("quoteDiv");
|
||||
const atWhoDivs = document.getElementsByClassName("quote");
|
||||
cmEl.addKeyMap({
|
||||
'Up':()=>{
|
||||
let index;
|
||||
for(let i = 0; i<atWhoDivs.length;i++){
|
||||
atWhoDivs[i].className === "quote active" && (index = i);
|
||||
}
|
||||
if(index>0){
|
||||
index <=atWhoDivs.length-4 && (atWhoListDiv.scrollTop -=40)
|
||||
atWhoDivs[index].className = "quote";
|
||||
atWhoDivs[index-1].className = "quote active";
|
||||
}
|
||||
},
|
||||
'Down':()=>{
|
||||
let index;
|
||||
for(let i = 0; i<atWhoDivs.length;i++){
|
||||
atWhoDivs[i].className === "quote active" && (index = i);
|
||||
}
|
||||
if(index<atWhoDivs.length-1){
|
||||
index >=3 && (atWhoListDiv.scrollTop +=40)
|
||||
atWhoDivs[index].className = "quote";
|
||||
atWhoDivs[index+1].className = "quote active";
|
||||
}
|
||||
},
|
||||
'Enter':()=>{
|
||||
//找到classname为quote active的div,执行click事件
|
||||
if(document.getElementsByClassName("quote active")[0]){
|
||||
document.getElementsByClassName("quote active")[0].click();
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
//移除上下、enter键监听
|
||||
cmEl.removeKeyMap();
|
||||
}
|
||||
}
|
||||
},[quoteVisible])
|
||||
|
||||
function onLayout() {
|
||||
let ro
|
||||
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()
|
||||
editorInstance.resize();
|
||||
editorInstance.cm.refresh();
|
||||
isFocus && editorInstance.cm.focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
ro.observe(editorEl.current)
|
||||
ro.observe(editorEl.current);
|
||||
}
|
||||
return ro
|
||||
return ro;
|
||||
}
|
||||
|
||||
function selectAtWho(username){
|
||||
atWhoVisibleRef.current = false;
|
||||
setAtWhoVisible(false);
|
||||
const cm = editorInstance.cm;
|
||||
//获取鼠标所在行的行数和ch
|
||||
const cursor = cm.doc.getCursor();
|
||||
const line = cursor.line;//行
|
||||
const ch = cursor.ch;//列
|
||||
const startIndex = cm.getRange({line,ch:0},{line,ch}).lastIndexOf("@");
|
||||
let selectUserLogin = undefined;
|
||||
users.map((item)=>{
|
||||
item.username === username && (selectUserLogin = item.login);
|
||||
})
|
||||
//替换内容
|
||||
cm.replaceRange("[@"+username+"]"+`(/${selectUserLogin}) `,{line,ch:startIndex},{line,ch});
|
||||
//鼠标聚焦
|
||||
cm.focus();
|
||||
//将此user的login存储到atWhoLoginList集合中
|
||||
const list = new Set(atWhoLoginList.current);
|
||||
list.add(selectUserLogin);
|
||||
atWhoLoginList.current = Array.from(list);
|
||||
setAtWhoLoginListState(Array.from(list));
|
||||
}
|
||||
|
||||
function onMouseOver(key){
|
||||
document.getElementsByClassName("at_who active")[0] && (document.getElementsByClassName("at_who active")[0].className="at_who");
|
||||
document.getElementsByClassName("at_who")[key] && (document.getElementsByClassName("at_who")[key].className="at_who active");
|
||||
}
|
||||
|
||||
//markdown编辑器中输入的键盘监听事件
|
||||
function mdKeyDown(e){ //获取光标位置
|
||||
const cssStyle = document.getElementsByClassName("CodeMirror cm-s-default CodeMirror-wrap")[0].firstChild.style;
|
||||
//设置弹框位置
|
||||
const newTop = 62
|
||||
const newLeft = 20;
|
||||
const codemirror = editorInstance.cm;
|
||||
let value = codemirror.getValue();
|
||||
if (e.shiftKey && e.code === "Digit2") {
|
||||
// 输入@键后在对应的位置显示可选的项目成员
|
||||
atWhoVisibleRef.current = true;
|
||||
setAtWhoVisible(true);
|
||||
document.getElementById("at_who_list").style.top = parseInt(cssStyle.getPropertyValue("top").replace("px","")) + newTop +"px";
|
||||
document.getElementById("at_who_list").style.left = parseInt(cssStyle.getPropertyValue("left").replace("px",""))+newLeft+"px";
|
||||
}
|
||||
//处理本来@了某人 -> 删掉 -> 撤回 的情况
|
||||
if(e.ctrlKey && e.code === "KeyZ" && allUsers.length !== 0){
|
||||
//处理初始内容就自带@谁的情况
|
||||
if(initValue){
|
||||
const del = [];
|
||||
allUsers.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}](/${item.login})`;
|
||||
}
|
||||
})
|
||||
del.length!=0 && del.map(str=>{
|
||||
value = value.replace(str,"");
|
||||
})
|
||||
}
|
||||
//判断value是否包含@符号
|
||||
value.indexOf("@") != -1 && allUsers.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));
|
||||
}
|
||||
})
|
||||
}
|
||||
if(e.shiftKey && e.code === "Digit3" && isQuoteIssue){
|
||||
// 输入#键后在对应的位置显示可选的项目issue
|
||||
quoteVisibleRef.current = true;
|
||||
setQuoteVisible(true);
|
||||
document.getElementById("quoteDiv").style.top = parseInt(cssStyle.getPropertyValue("top").replace("px","")) + newTop +"px";
|
||||
document.getElementById("quoteDiv").style.left = parseInt(cssStyle.getPropertyValue("left").replace("px",""))+newLeft+"px";
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
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 ${key===0 && `active`}`} onClick={()=>{selectAtWho(item.username)}} onMouseOver={()=>{onMouseOver(key)}}>
|
||||
{item.image_url && <img src={getImageUrl(`/${item.image_url}`)}></img>}
|
||||
<span>{item.username}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (editorInstance) {
|
||||
return
|
||||
|
|
@ -121,8 +347,7 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
htmlDecode: "style,script,iframe",
|
||||
sequenceDiagram: true,
|
||||
autoFocus: false,
|
||||
watch: watch === undefined ? true : watch,
|
||||
|
||||
watch: watch,
|
||||
saveHTMLToTextarea: true,
|
||||
dialogMaskOpacity: 0.6,
|
||||
placeholder: placeholder,
|
||||
|
|
@ -134,6 +359,7 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
},
|
||||
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>",
|
||||
|
|
@ -150,6 +376,9 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
}
|
||||
cm.replaceSelection(NULL_CH)
|
||||
},
|
||||
"fullScreen":function(cm,icon,cursor,selection){
|
||||
icon.addClass("none");
|
||||
},
|
||||
"inline-latex": function (cm, icon, cursor, selection) {
|
||||
cm.replaceSelection("$$" + selection + "$$");
|
||||
cm.setCursor(cursor.line, cursor.ch + 2);
|
||||
|
|
@ -164,7 +393,8 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
lang: {
|
||||
toolbar: {
|
||||
"latex": "多行公式",
|
||||
"line-break": "换行"
|
||||
"line-break": "换行",
|
||||
"fullScreen":"开启全屏"
|
||||
}
|
||||
},
|
||||
onload: function () {
|
||||
|
|
@ -172,15 +402,78 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
}
|
||||
})
|
||||
|
||||
|
||||
}, [containerId, editorInstance, startInit])
|
||||
|
||||
const cmEl = editorInstance && editorInstance.cm
|
||||
|
||||
useEffect(()=>{
|
||||
if(atWhoVisibleRef.current){
|
||||
// 添加上下键、enter键监听事件
|
||||
cmEl.addKeyMap({
|
||||
'Up':()=>{
|
||||
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(index>0){
|
||||
index <=atWhoDivs.length-4 && (atWhoListDiv.scrollTop -=40)
|
||||
atWhoDivs[index].className = "at_who";
|
||||
atWhoDivs[index-1].className = "at_who active";
|
||||
}
|
||||
},
|
||||
'Down':()=>{
|
||||
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(index<atWhoDivs.length-1){
|
||||
index >=3 && (atWhoListDiv.scrollTop +=40)
|
||||
atWhoDivs[index].className = "at_who";
|
||||
atWhoDivs[index+1].className = "at_who active";
|
||||
}
|
||||
},
|
||||
'Enter':()=>{
|
||||
//找到classname为at_who active的div,执行click事件
|
||||
if(document.getElementsByClassName("at_who active")[0]){
|
||||
document.getElementsByClassName("at_who active")[0].click()
|
||||
}else{
|
||||
const cm = editorInstance.cm;
|
||||
const cursor = cm.doc.getCursor();
|
||||
const line = cursor.line;//行
|
||||
const ch = cursor.ch;//列
|
||||
//添加换行
|
||||
cm.replaceRange("\n",{line,ch},{line,ch});
|
||||
setAtWhoVisible(false);
|
||||
atWhoVisibleRef.current = false;
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
//移除上下、enter键监听
|
||||
cmEl && cmEl.removeKeyMap();
|
||||
}
|
||||
},[atWhoVisible])
|
||||
|
||||
useEffect(()=>{
|
||||
//当users数组发生变化时改变框的位置
|
||||
if(atWhoVisibleRef.current && users){
|
||||
//获取光标位置
|
||||
const cssStyle = document.getElementsByClassName("CodeMirror cm-s-default CodeMirror-wrap")[0].firstChild.style;
|
||||
//设置弹框位置
|
||||
const newLeft = placeholder === "添加评论..."? 80: 10;
|
||||
document.getElementById("at_who_list").style.left = (parseInt(cssStyle.getPropertyValue("left").replace("px",""))+newLeft)+"px";
|
||||
}
|
||||
},[users])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (cmEl) {
|
||||
let tid = null
|
||||
let ro
|
||||
let ro;
|
||||
if (onCMBlur) {
|
||||
editorInstance.cm.on('blur', () => { onCMBlur(editorInstance.getValue()) })
|
||||
}
|
||||
|
|
@ -192,12 +485,147 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
if (!noStorage) {
|
||||
tid = md_elocalStorage(editorInstance, `MDEditor__${containerId}`, containerId)
|
||||
}
|
||||
if (onChange) {
|
||||
editorInstance.cm.on('change', (cm) => {
|
||||
let v = xss(cm.getValue());
|
||||
onChange(v);
|
||||
})
|
||||
}
|
||||
//isCanAtme:只有issue和合并请求以及评论部分可以@他人操作
|
||||
//绑定@事件
|
||||
(isCanAtme || isQuoteIssue) && editorInstance.cm.on("focus", () => {
|
||||
document.addEventListener("keydown", mdKeyDown);
|
||||
});
|
||||
(isCanAtme || isQuoteIssue) && editorInstance.cm.on("blur", () => {
|
||||
document.removeEventListener("keydown",mdKeyDown);
|
||||
});
|
||||
editorInstance.cm.on("change", (cm) => {
|
||||
//调用父组件的onchange方法,将输入内容传入父级组件
|
||||
// let reg = /alert\((.*?)\)/g;
|
||||
let v = cm.getValue();
|
||||
// if(v){
|
||||
// let matchvalue = v.match(reg);
|
||||
// if(matchvalue && matchvalue.length>0){
|
||||
// for(var x=0;x<matchvalue.length;x++){
|
||||
// v = v.replace(matchvalue[x],"");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// xss()防止恶意代码:如alert等
|
||||
onChange && onChange(xss(v));
|
||||
if(quoteVisibleRef.current){
|
||||
const cur = cm.doc.getCursor();
|
||||
const line = cur.line;
|
||||
const ch = cur.ch;
|
||||
let rangeCont = cmEl.getRange({line,ch:0},{line,ch});
|
||||
// #后第一个字符为空时隐藏列表,否则不隐藏
|
||||
if(rangeCont.indexOf("#")===-1){
|
||||
setQuoteVisible(false);
|
||||
quoteVisibleRef.current = false;
|
||||
}else{
|
||||
rangeCont = rangeCont.substring(rangeCont.lastIndexOf("#")+1);
|
||||
if(rangeCont === " "){
|
||||
setQuoteVisible(false);
|
||||
quoteVisibleRef.current = false;
|
||||
}else{
|
||||
getIssueList(rangeCont);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(atWhoVisibleRef.current){
|
||||
//搜索用户(弹框之后用户输入用户名信息)
|
||||
const cur = cm.doc.getCursor();
|
||||
const line = cur.line;
|
||||
const ch = cur.ch;
|
||||
let rangeCont = cmEl.getRange({line,ch:0},{line,ch});
|
||||
//处理已经弹出列表框,但用户删除@符号
|
||||
if(rangeCont.indexOf("@")===-1){
|
||||
setAtWhoVisible(false);
|
||||
atWhoVisibleRef.current = false;
|
||||
}else{
|
||||
rangeCont = rangeCont.substring(rangeCont.lastIndexOf("@")+1);
|
||||
rangeCont ? axios.get(`/${owner}/${projectsId}/members.json`,{
|
||||
params: {
|
||||
search: rangeCont,
|
||||
},
|
||||
}).then(response=>{
|
||||
if(response && response.data && response.data.total_count !== 0){
|
||||
setUsers(response.data.users);
|
||||
}else{
|
||||
setUsers(undefined);
|
||||
}
|
||||
}):setUsers(allUsers)
|
||||
}
|
||||
}
|
||||
|
||||
changeValueTime("#",issues,chooseIssuesList.current);
|
||||
|
||||
//当内容发生改变并且有已@列表时
|
||||
if(atWhoLoginList.current.length != 0){
|
||||
const codemirror = editorInstance.cm;
|
||||
//startValue:触发change方法时的内容,value:处理了初始内容带@用户的情况
|
||||
let value = codemirror.getValue();
|
||||
//处理初始内容就自带@谁的情况
|
||||
if(initValue){
|
||||
const del = [];
|
||||
allUsers.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}](/${item.login})`;
|
||||
}
|
||||
})
|
||||
del.length!=0 && del.map(str=>{
|
||||
value = value.replace(str,"");
|
||||
})
|
||||
}
|
||||
//以username为主键,login为value的map集合
|
||||
let atWhoMap = new Map();
|
||||
Array.from(atWhoLoginList.current).map(item=>{
|
||||
allUsers.map(i=>{
|
||||
if(i.login === item){
|
||||
atWhoMap.set(i.username,i.login);
|
||||
}
|
||||
})
|
||||
});
|
||||
const cursor = codemirror.doc.getCursor();
|
||||
const line = cursor.line;
|
||||
const ch = cursor.ch;
|
||||
//处理全部内容中不包含“@”的情况
|
||||
if(value.indexOf("@") === -1){
|
||||
atWhoLoginList.current = [];
|
||||
setAtWhoLoginListState([]);
|
||||
}
|
||||
|
||||
//截取第一个字符到光标的内容
|
||||
const curAfterCont = codemirror.getRange({line,ch:0},{line,ch});
|
||||
const content = codemirror.getLine(line);
|
||||
//处理光标所在行 有“@”的情况
|
||||
if(content && content.indexOf("@") !== -1){
|
||||
Array.from(atWhoMap.keys()).map(username=>{
|
||||
//判断content是不是以列表中的某个username结尾
|
||||
const userCont = `[@${username}](/${atWhoMap.get(username)})`;
|
||||
//删除空格->选中@用户区域
|
||||
if(curAfterCont.endsWith(userCont)){
|
||||
codemirror.setSelection({line,ch:curAfterCont.lastIndexOf("@")-1},{line,ch});
|
||||
}
|
||||
//处理已经有@列表但是value中不包含完整[@用户名](/login)的情况
|
||||
if(value.indexOf(userCont)===-1){
|
||||
//符合情况->踢掉这个人 不给他发消息
|
||||
const list = new Set(atWhoLoginList.current);
|
||||
list.delete(atWhoMap.get(username));
|
||||
atWhoLoginList.current = Array.from(list);
|
||||
setAtWhoLoginListState(Array.from(list));
|
||||
}
|
||||
})
|
||||
}else{
|
||||
//处理所在行没有“@”的情况
|
||||
Array.from(atWhoMap.keys()).map(username=>{
|
||||
const userCont = `[@${username}](/${atWhoMap.get(username)})`;
|
||||
if(value.indexOf(userCont)===-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) {
|
||||
|
|
@ -210,9 +638,89 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
}
|
||||
}, [cmEl])
|
||||
|
||||
function changeValueTime(str,alllist,chooselsit){
|
||||
if(chooselsit.length != 0){
|
||||
const codemirror = editorInstance.cm;
|
||||
//startValue:触发change方法时的内容,value:处理了初始内容带@用户的情况
|
||||
let value = codemirror.getValue();
|
||||
//处理初始内容就自带@谁的情况
|
||||
if(initValue){
|
||||
const del = [];
|
||||
alllist.map(item=>{
|
||||
let strings = item.username;
|
||||
if(str==="#"){
|
||||
strings = item.project_issues_index;
|
||||
}
|
||||
if(initValue.indexOf(strings)!=-1 && initValue.charAt(initValue.indexOf(strings)-1) === str && initValue.indexOf(`${str}${strings}`)===value.indexOf(`${str}${strings}`)){
|
||||
//初始内容中有符合@+名字的格式并且当前内容未删除初始内容
|
||||
del[del.length] = `[${str}${strings}](/${strings})`;
|
||||
if(str==="#"){
|
||||
del[del.length] = `[${str}${strings}](${owner}/${projectsId}/issues/${strings})`;
|
||||
}
|
||||
}
|
||||
})
|
||||
del.length!=0 && del.map(str=>{
|
||||
value = value.replace(str,"");
|
||||
})
|
||||
}
|
||||
//以project_issues_index为主键和value的map集合
|
||||
let atWhoMap = new Map();
|
||||
Array.from(chooselsit).map(item=>{
|
||||
alllist.map(i=>{
|
||||
if(i.project_issues_index === item){
|
||||
atWhoMap.set(i.project_issues_index,i.project_issues_index);
|
||||
}
|
||||
})
|
||||
});
|
||||
const cursor = codemirror.doc.getCursor();
|
||||
const line = cursor.line;
|
||||
const ch = cursor.ch;
|
||||
//处理全部内容中不包含“@”的情况
|
||||
if(value.indexOf(str) === -1){
|
||||
if(str==="#"){
|
||||
chooseIssuesList.current = [];
|
||||
}
|
||||
}
|
||||
//截取第一个字符到光标的内容
|
||||
const curAfterCont = codemirror.getRange({line,ch:0},{line,ch});
|
||||
const content = codemirror.getLine(line);
|
||||
//处理光标所在行 有“@”的情况
|
||||
if(content && content.indexOf(str) !== -1){
|
||||
Array.from(atWhoMap.keys()).map(index=>{
|
||||
//判断content是不是以列表中的某个username结尾
|
||||
const userCont = `[${str}${index}](/${owner}/${projectsId}/issues/${atWhoMap.get(index)})`;
|
||||
//删除空格->选中@用户区域
|
||||
if(curAfterCont.endsWith(userCont)){
|
||||
codemirror.setSelection({line,ch:curAfterCont.lastIndexOf(str)-1},{line,ch});
|
||||
}
|
||||
if(value.indexOf(userCont)===-1){
|
||||
//符合情况->从数组中删除
|
||||
if(str==="#"){
|
||||
const list = new Set(chooseIssuesList.current);
|
||||
list.delete(atWhoMap.get(index));
|
||||
chooseIssuesList.current = Array.from(list);
|
||||
}
|
||||
}
|
||||
})
|
||||
}else{
|
||||
//处理所在行没有“@”的情况
|
||||
Array.from(atWhoMap.keys()).map(index=>{
|
||||
const userCont = `[${str}${index}](/${owner}/${projectsId}/issues/${atWhoMap.get(index)})`;
|
||||
if(value.indexOf(userCont)===-1){
|
||||
if(str==="#"){
|
||||
const list = new Set(chooseIssuesList.current);
|
||||
list.delete(atWhoMap.get(index));
|
||||
chooseIssuesList.current = Array.from(list);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (editorInstance && initValue !== undefined) {
|
||||
if (initValue != null && initValue != editorInstance.getValue()) {
|
||||
if (initValue !== null && initValue !== editorInstance.getValue()) {
|
||||
editorInstance.setValue(initValue.toString())
|
||||
}
|
||||
}
|
||||
|
|
@ -222,7 +730,7 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
if (resizeBarEl.current) {
|
||||
let el = resizeBarEl.current
|
||||
let dragging = false
|
||||
let startY = 0
|
||||
let startY = 0
|
||||
function onMouseDown(e) {
|
||||
dragging = true
|
||||
startY = e.pageY
|
||||
|
|
@ -252,13 +760,13 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
document.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
editorInstance, resizeBarEl
|
||||
])
|
||||
}, [ editorInstance, resizeBarEl ])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div ref={editorEl} className={`df ${className} ${imageExpand && 'editormd-image-click-expand'} `} >
|
||||
{atWhoVisible && atWhoList}
|
||||
<div ref={editorEl} className={`df editormd-editing ${className} ${imageExpand && 'editormd-image-click-expand'} `}>
|
||||
{quoteVisible && QuoteDiv }
|
||||
<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>
|
||||
|
|
@ -266,7 +774,7 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
|
|||
</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>}
|
||||
{noStorage === true ? null : <div id={tipId} className="edu-txt-right color-grey-cd font-12"></div>}
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,273 @@
|
|||
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react'
|
||||
import { getUploadActionUrl, getUrl } from 'educoder'
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import xss from 'xss';
|
||||
import '../../courses/css/Courses.css'
|
||||
import './css/TPMchallengesnew.css'
|
||||
import 'codemirror/lib/codemirror.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 }) => {
|
||||
|
||||
const editorEl = useRef()
|
||||
const resizeBarEl = useRef()
|
||||
const [editorInstance, setEditorInstance] = useState()
|
||||
const containerId = `mdEditor_${mdID}`
|
||||
const editorBodyId = `mdEditors_${mdID}`
|
||||
const tipId = `e_tips_mdEditor_${mdID}`
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
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)
|
||||
},
|
||||
"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": "换行"
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
if (onChange) {
|
||||
editorInstance.cm.on('change', (cm) => {
|
||||
let v = xss(cm.getValue());
|
||||
onChange(v);
|
||||
})
|
||||
}
|
||||
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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue