diff --git a/src/modules/tpm/challengesnew/tpm-md-editor.js b/src/modules/tpm/challengesnew/tpm-md-editor.js index 157f0e94e..af89ed0b2 100644 --- a/src/modules/tpm/challengesnew/tpm-md-editor.js +++ b/src/modules/tpm/challengesnew/tpm-md-editor.js @@ -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 = '您上次有已保存的数据,是否恢复 ? / 不恢复'; $("#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 = ( +
+ { + screenIssues && screenIssues.map((i,k)=>{ + return( +
  • selectQuoteIssue(i)} onMouseOver={()=>onMouseOverQuote(k)}> + #{i.project_issues_index} + {i.subject} +
  • + ) + }) + } +
    + ) + // 点击选择要引用的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; i0){ + 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=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 = ( +
    + {users && users.map((item,key)=>{ + return( +
    {selectAtWho(item.username)}} onMouseOver={()=>{onMouseOver(key)}}> + {item.image_url && } + {item.username} +
    + ) + })} +
    + ) + 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": "", @@ -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; i0){ + 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=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{ + 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 ( -
    + {atWhoVisible && atWhoList} +
    + {quoteVisible && QuoteDiv }
    @@ -266,7 +774,7 @@ export default ({ mdID, onChange, onCMBeforeChange, onCMBlur, error = false, cla
    {showResizeBar ? : null}
    - {noStorage == true ? null :
    } + {noStorage === true ? null :
    }
    ) diff --git a/src/modules/tpm/challengesnew/tpm-md-editor1.js b/src/modules/tpm/challengesnew/tpm-md-editor1.js new file mode 100644 index 000000000..157f0e94e --- /dev/null +++ b/src/modules/tpm/challengesnew/tpm-md-editor1.js @@ -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 = '您上次有已保存的数据,是否恢复 ? / 不恢复'; + $("#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": "", + "latex": "", + "null-button": "点击插入填空项", + }, + toolbarHandlers: { + "line-break": function (cm, icon, cursor, selection) { + cm.replaceSelection("
    ") + }, + "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 ( + +
    +
    + +
    +
    +
    + {showResizeBar ? : null} +
    + {noStorage == true ? null :
    } +
    +
    + ) +} \ No newline at end of file