From e47d6e1158a4b088ad93dfa5f1fa02df4ede6948 Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Fri, 13 Sep 2024 21:50:42 +0200 Subject: [PATCH] Hide the delete icon when there is one answer field left only. --- amd/build/ui.min.js | 2 +- amd/build/ui.min.js.map | 2 +- amd/src/ui.js | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/amd/build/ui.min.js b/amd/build/ui.min.js index 0ccc55a..d83dde5 100644 --- a/amd/build/ui.min.js +++ b/amd/build/ui.min.js @@ -5,6 +5,6 @@ define("tiny_cloze/ui",["exports","core/modal_events","core/modal","core/modal_f * @module tiny_cloze/ui * @copyright 2023 MoodleDACH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.resolveSubquestion=_exports.onSubmit=_exports.onInit=_exports.onBeforeGetContent=_exports.displayDialogue=void 0,_modal_events=_interopRequireDefault(_modal_events),_modal2=_interopRequireDefault(_modal2),_modal_factory=_interopRequireDefault(_modal_factory),_mustache=_interopRequireDefault(_mustache);const isNull=a=>null==a,strdecode=t=>String(t).replace(/\\(#|\}|~)/g,"$1"),strencode=t=>String(t).replace(/(#|\}|~)/g,"\\$1"),indexOfNode=(list,node)=>{for(let i=0;i{const attrSel=' selected="selected"';let isSel="="===s?attrSel:"",html='");return FRACTIONS.forEach((item=>{isSel=item.value.toString()===s?attrSel:"",html+='")})),isSel=""!==s&&-1===html.indexOf(attrSel)?attrSel:"",html+='"),html},isCustomGrade=s=>{if("="===s||""===s)return!1;let found=!1;return FRACTIONS.forEach((item=>{item.value.toString()===s&&(found=!0)})),!found},markerClass="cloze-question-marker",markerSpan='',reQtype=/\{([0-9]*):(MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?):(.*?)\}/g,CSS={ANSWER:"tiny_cloze_answer",ANSWERS:"tiny_cloze_answers",ADD:"tiny_cloze_add",CANCEL:"tiny_cloze_cancel",DELETE:"tiny_cloze_delete",FEEDBACK:"tiny_cloze_feedback",FRACTION:"tiny_cloze_fraction",FRAC_CUSTOM:"tiny_cloze_frac_custom",LEFT:"tiny_cloze_col0",LOWER:"tiny_cloze_down",RIGHT:"tiny_cloze_col1",MARKS:"tiny_cloze_marks",DUPLICATE:"tiny_cloze_duplicate",RAISE:"tiny_cloze_up",SUBMIT:"tiny_cloze_submit",SUMMARY:"tiny_cloze_summary",TOLERANCE:"tiny_cloze_tolerance",TYPE:"tiny_cloze_qtype"},TEMPLATE={FORM:'

{{name}} ({{qtype}})

{{STR.addmoreanswerblanks}}
    {{#answerdata}}
  1. {{STR.addmoreanswerblanks}}{{STR.delete}}{{STR.up}}{{STR.down}}
    {{#numerical}}
    {{/numerical}}
    %
  2. {{/answerdata}}
',TYPE:'

{{STR.chooseqtypetoadd}}

{{#types}}
{{/types}}
',FOOTER:''},FRACTIONS=[{value:100},{value:50},{value:0}],STR={},getQuestionTypes=function(){return[{type:"MULTICHOICE",abbr:["MC"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.selectinline,STR.singleyes]},{type:"MULTICHOICE_H",abbr:["MCH"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.horizontal,STR.singleyes]},{type:"MULTICHOICE_V",abbr:["MCV"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.vertical,STR.singleyes]},{type:"MULTICHOICE_S",abbr:["MCS"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.selectinline,STR.shuffle,STR.singleyes]},{type:"MULTICHOICE_HS",abbr:["MCHS"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.horizontal,STR.shuffle,STR.singleyes]},{type:"MULTICHOICE_VS",abbr:["MCVS"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.vertical,STR.shuffle,STR.singleyes]},{type:"MULTIRESPONSE",abbr:["MR"],name:STR.multiresponse,summary:STR.summary_multichoice,options:[STR.multi_vertical,STR.singleno]},{type:"MULTIRESPONSE_H",abbr:["MRH"],name:STR.multiresponse,summary:STR.summary_multichoice,options:[STR.multi_horizontal,STR.singleno]},{type:"MULTIRESPONSE_S",abbr:["MRS"],name:STR.multiresponse,summary:STR.summary_multichoice,options:[STR.multi_vertical,STR.shuffle,STR.singleno]},{type:"MULTIRESPONSE_HS",abbr:["MRHS"],name:STR.multiresponse,summary:STR.summary_multichoice,options:[STR.multi_horizontal,STR.shuffle,STR.singleno]},{type:"NUMERICAL",abbr:["NM"],name:STR.numerical,summary:STR.summary_numerical},{type:"SHORTANSWER",abbr:["SA","MW"],name:STR.shortanswer,summary:STR.summary_shortanswer,options:[STR.caseno]},{type:"SHORTANSWER_C",abbr:["SAC","MWC"],name:STR.shortanswer,summary:STR.summary_shortanswer,options:[STR.caseyes]}]};let _editor=null,_form=null,_answerdata=[],_qtype=null,_selectedOffset=-1,_marks=1,_modal=null,_firstAnswer=null;_exports.onInit=function(ed){_editor=ed,_addMarkers(),(async()=>{(0,_str.get_strings)([{key:"answer",component:"question"},{key:"chooseqtypetoadd",component:"question"},{key:"defaultmark",component:"question"},{key:"feedback",component:"question"},{key:"correct",component:"question"},{key:"incorrect",component:"question"},{key:"addmoreanswerblanks",component:"qtype_calculated"},{key:"delete",component:"core"},{key:"up",component:"core"},{key:"down",component:"core"},{key:"tolerance",component:"qtype_calculated"},{key:"grade",component:"grades"},{key:"caseno",component:"mod_quiz"},{key:"caseyes",component:"mod_quiz"},{key:"answersingleno",component:"qtype_multichoice"},{key:"answersingleyes",component:"qtype_multichoice"},{key:"layoutselectinline",component:"qtype_multianswer"},{key:"layouthorizontal",component:"qtype_multianswer"},{key:"layoutvertical",component:"qtype_multianswer"},{key:"shufflewithin",component:"mod_quiz"},{key:"layoutmultiple_horizontal",component:"qtype_multianswer"},{key:"layoutmultiple_vertical",component:"qtype_multianswer"},{key:"pluginnamesummary",component:"qtype_multichoice"},{key:"pluginnamesummary",component:"qtype_shortanswer"},{key:"pluginnamesummary",component:"qtype_numerical"},{key:"multichoice",component:_common.component},{key:"multiresponse",component:_common.component},{key:"numerical",component:"mod_quiz"},{key:"shortanswer",component:"mod_quiz"},{key:"cancel",component:"core"},{key:"select",component:_common.component},{key:"insert",component:_common.component},{key:"pluginname",component:_common.component},{key:"customgrade",component:_common.component},{key:"err_custom_rate",component:_common.component},{key:"err_empty_answer",component:_common.component},{key:"err_none_correct",component:_common.component},{key:"err_not_numeric",component:_common.component}]).then((function(){const args=Array.from(arguments);return["answer","chooseqtypetoadd","defaultmark","feedback","correct","incorrect","addmoreanswerblanks","delete","up","down","tolerance","grade","caseno","caseyes","singleno","singleyes","selectinline","horizontal","vertical","shuffle","multi_horizontal","multi_vertical","summary_multichoice","summary_shortanswer","summary_numerical","multichoice","multiresponse","numerical","shortanswer","btn_cancel","btn_select","btn_insert","title","custom_grade","err_custom_rate","err_empty_answer","err_none_correct","err_not_numeric"].map(((l,i)=>(STR[l]=args[0][i],""))),""})).catch((()=>""))})()};const _createModal=async function(){const cfg={title:STR.title,templateContext:{elementid:_editor.id},removeOnClose:!0,large:!0};_modal="function"==typeof _modal2.default.create?await _modal2.default.create(cfg):await _modal_factory.default.create(cfg)};_exports.displayDialogue=async function(){await _createModal();var subquestion=resolveSubquestion();subquestion?(_firstAnswer=null,_selectedOffset=indexOfNode(_editor.dom.select("."+markerClass),subquestion),_parseSubquestion(subquestion.innerHTML),_setDialogueContent(_qtype)):(_firstAnswer=_editor.selection.getContent(),_selectedOffset=-1,_setDialogueContent())};const _addMarkers=function(){let m,content=_editor.getContent(),newContent="";if(-1===content.indexOf(markerClass)){do{if(m=content.match(reQtype),!m){newContent+=content;break}const pos=content.indexOf(m[0]);newContent+=content.substring(0,pos)+markerSpan+content.substring(pos,pos+m[0].length),content=content.substring(pos+m[0].length);let level=(m[0].match(/\{/g)||[]).length;if(1!==level){for(;level>1;){const a=content.indexOf("{"),b=content.indexOf("}");a>-1&&b>-1&&a-1?(newContent=content.substring(0,b),content=content.substring(b+1),level--):level=1}newContent+="
"}else newContent+=""}while(m);_editor.setContent(newContent)}},_removeMarkers=function(){for(const span of _editor.dom.select("span."+markerClass))_editor.dom.setOuterHTML(span,span.classList.contains("new")?"":span.innerHTML)};_exports.onBeforeGetContent=function(content){if(!isNull(content.source_view)&&!0===content.source_view){var onClose=function(){_editor.off("close",onClose),_addMarkers()};_editor.on("CloseWindow",(()=>{onClose()})),_modal||_removeMarkers()}};_exports.onSubmit=function(){_removeMarkers()};const _setDialogueContent=function(qtype,nomodalevents){const footer=_mustache.default.render(TEMPLATE.FOOTER,{cancel:STR.btn_cancel,submit:qtype?STR.btn_insert:STR.btn_select});let contentText;contentText=qtype?_mustache.default.render(TEMPLATE.FORM,{CSS:CSS,STR:STR,answerdata:_answerdata,elementid:getUuid(),qtype:_qtype,name:getQuestionTypes().filter((q=>_qtype===q.type))[0].name,marks:_marks,numerical:"NUMERICAL"===_qtype||"NM"===_qtype}):_mustache.default.render(TEMPLATE.TYPE,{CSS:CSS,STR:STR,qtype:_qtype,types:getQuestionTypes()}),_modal.setBody(contentText),_modal.setFooter(footer),_modal.show();const $root=_modal.getRoot();if(_form=$root.get(0).querySelector("form"),!nomodalevents){if(_modal.registerEventListeners(),_modal.registerCloseOnSave(),_modal.registerCloseOnCancel(),$root.on(_modal_events.default.cancel,_cancel),!qtype)return void $root.on(_modal_events.default.save,_choiceHandler);$root.on(_modal_events.default.save,_setSubquestion)}const getTarget=e=>{let p=e.target;for(;!isNull(p)&&1===p.nodeType&&"A"!==p.tagName;)p=p.parentNode;return isNull(p.classList)?null:p};_form.addEventListener("click",(e=>{const p=getTarget(e);if(!isNull(p))return p.classList.contains(CSS.DELETE)?(e.preventDefault(),void _deleteAnswer(p)):p.classList.contains(CSS.ADD)?(e.preventDefault(),void _addAnswer(p)):p.classList.contains(CSS.LOWER)?(e.preventDefault(),void _lowerAnswer(p)):void(p.classList.contains(CSS.RAISE)&&(e.preventDefault(),_raiseAnswer(p)))})),_form.addEventListener("keyup",(e=>{const p=getTarget(e);isNull(p)||(p.classList.contains(CSS.ANSWER)||p.classList.contains(CSS.FEEDBACK))&&(e.preventDefault(),_addAnswer(p))})),_form.querySelectorAll("."+CSS.FRACTION).forEach((sel=>{sel.addEventListener("change",(e=>{const id=e.target.getAttribute("id");"__custom__"===e.target.value?document.getElementById(id+"_custom").parentNode.classList.remove("hidden"):document.getElementById(id+"_custom").parentNode.classList.add("hidden")}))}))},_choiceHandler=function(e){e.preventDefault();let qtype=_form.querySelector("input[name=qtype]:checked");qtype&&(_qtype=qtype.value);const max=-1!==_qtype.indexOf("SHORTANSWER")||"NUMERICAL"===_qtype?1:3,blankAnswer={id:getUuid(),answer:"",feedback:"",fraction:100,fractionOptions:getFractionOptions(""),tolerance:0,isCustomGrade:!1};_answerdata=[];for(let x=0;x(_setDialogueContent(_qtype),_form.querySelector("."+CSS.ANSWER).focus(),""))).catch((()=>""))},_parseSubquestion=function(question){_answerdata=[];const parts=reQtype.exec(question);if(reQtype.lastIndex=0,!parts)return;_marks=parts[1],_qtype=parts[2],_qtype.length<5&&getQuestionTypes().forEach((l=>{for(const a of l.abbr)if(a===_qtype)return void(_qtype=l.type)}));const answers=parts[7].match(/(\\.|[^~])*/g);answers&&answers.forEach((function(answer){const options=/^(%(-?[.0-9]+)%|(=?))((\\.|[^#])*)#?(.*)/.exec(answer);if(options&&options[4]){let frac="";if(options[3]?frac="="===options[3]?"=":100:options[2]&&(frac=options[2]),"NUMERICAL"===_qtype||"NM"===_qtype){const tolerance=/^([^:]*):?(.*)/.exec(options[4])[2]||0;return void _answerdata.push({id:getUuid(),answer:strdecode(options[4].replace(/:.*/,"")),feedback:strdecode(options[6]),tolerance:tolerance,fraction:frac,fractionOptions:getFractionOptions(frac),isCustomGrade:isCustomGrade(frac)})}_answerdata.push({answer:strdecode(options[4]),id:getUuid(),feedback:strdecode(options[6]),fraction:frac,fractionOptions:getFractionOptions(frac),isCustomGrade:isCustomGrade(frac)})}}))},_addAnswer=function(a){let index=indexOfNode(_form.querySelectorAll("."+CSS.ADD),a);-1===index&&(index=0);let fraction="",answer="",feedback="",tolerance=0;a.closest("li")&&(fraction=a.closest("li").querySelector("."+CSS.FRACTION).value,"__custom__"===fraction&&(fraction=a.closest("li").querySelector("."+CSS.FRAC_CUSTOM).value),answer=a.closest("li").querySelector("."+CSS.ANSWER).value,feedback=a.closest("li").querySelector("."+CSS.FEEDBACK).value,a.closest("li").querySelector("."+CSS.TOLERANCE)&&(tolerance=a.closest("li").querySelector("."+CSS.TOLERANCE).value)),_processFormData(),_answerdata.splice(index,0,{id:getUuid(),answer:answer,feedback:feedback,fraction:fraction,fractionOptions:getFractionOptions(fraction),tolerance:tolerance,isCustomGrade:isCustomGrade(fraction)}),_setDialogueContent(_qtype,!0),_form.querySelectorAll("."+CSS.ANSWER).item(index).focus()},_deleteAnswer=function(a){let index=indexOfNode(_form.querySelectorAll("."+CSS.DELETE),a);-1===index&&(index=indexOfNode(_form.querySelectorAll("li"),a.closest("li"))),_processFormData(),_answerdata.splice(index,1),_setDialogueContent(_qtype,!0);const answers=_form.querySelectorAll("."+CSS.ANSWER);index=Math.min(index,answers.length-1),answers.item(index).focus()},_lowerAnswer=function(a){const li=a.closest("li");li.before(li.nextSibling),li.querySelector("."+CSS.ANSWER).focus()},_raiseAnswer=function(a){const li=a.closest("li");li.after(li.previousSibling),li.querySelector("."+CSS.ANSWER).focus()},_cancel=function(e){e.preventDefault();for(const span of _editor.dom.select("."+markerClass+".new"))span.remove();_modal.destroy(),_editor.focus(),_modal=null},_setSubquestion=function(e){e.preventDefault();const errMsg=_form.querySelector(".msg-error"),formErrors=_processFormData(!0);if(formErrors.length>0)return errMsg.innerHTML="
  • "+formErrors.join("
  • ")+"
",void errMsg.classList.remove("hidden");errMsg.classList.add("hidden");let question="{"+_marks+":"+_qtype+":";for(let i=0;i<_answerdata.length;i++)""!==_answerdata[i].raw&&(question+=_answerdata[i].fraction&&!isNaN(_answerdata[i].fraction)?"%"+_answerdata[i].fraction+"%":_answerdata[i].fraction,question+=strencode(_answerdata[i].answer),"NM"!==_qtype&&"NUMERICAL"!==_qtype||(question+=":"+_answerdata[i].tolerance),_answerdata[i].feedback&&(question+="#"+strencode(_answerdata[i].feedback)),i<_answerdata.length-1&&(question+="~"));"~"===question.slice(-1)&&(question=question.substring(0,question.length-1)),question+="}",_modal.destroy(),_modal=null,_editor.focus(),_selectedOffset>-1?_editor.dom.select("."+markerClass)[_selectedOffset].innerHTML=question:_editor.insertContent(markerSpan+question+"")},_processFormData=function(validate){_answerdata=[];let globalErrors=[];const answers=_form.querySelectorAll("."+CSS.ANSWER),feedbacks=_form.querySelectorAll("."+CSS.FEEDBACK),fractions=_form.querySelectorAll("."+CSS.FRACTION),customGrades=_form.querySelectorAll("."+CSS.FRAC_CUSTOM),tolerances=_form.querySelectorAll("."+CSS.TOLERANCE);for(let i=0;i0?tolerances.item(i).value:0,isCustomGrade:"__custom__"===fractions.item(i).value};"NM"!==_qtype&&"NUMERICAL"!==_qtype||(tolerances.item(i).classList.remove("error"),currentAnswer.answer=Number(currentAnswer.answer),currentAnswer.tolerance=Number(currentAnswer.tolerance)),_answerdata.push(currentAnswer)}if(_marks=_form.querySelector("."+CSS.MARKS).value,validate){const{hasCorrectAnswer:hasCorrectAnswer,errors:errors}=_validateAnswers();for(let i=0;i<_answerdata.length;i++)for(const err of _answerdata[i].hasErrors){if(hasCorrectAnswer&&("empty_answer"===err||"correct_but_empty"===err))break;"answer_not_numeric"===err||"empty_answer"===err||"correct_but_empty"===err?answers.item(i).classList.add("error"):"tolerance_not_numeric"===err?tolerances.item(i).classList.add("error"):"error_custom_rate"===err&&customGrades.item(i).classList.add("error")}globalErrors=_translateGlobalErrors(hasCorrectAnswer,errors),globalErrors.length>0&&_form.querySelector("input.error").focus()}return globalErrors},_validateAnswers=function(){let errors=[],hasCorrect=!1;for(let i=0;i<_answerdata.length;i++)_answerdata[i].hasErrors=[],""===_answerdata[i].raw&&_answerdata[i].hasErrors.push("empty_answer"),"NM"!==_qtype&&"NUMERICAL"!==_qtype||(isNaN(_answerdata[i].answer)&&""!==_answerdata[i].raw&&_answerdata[i].hasErrors.push("answer_not_numeric"),isNaN(_answerdata[i].tolerance)&&_answerdata[i].hasErrors.push("tolerance_not_numeric")),_answerdata[i].isCustomGrade&&(isNaN(_answerdata[i].fraction)||_answerdata[i].fraction<-100||_answerdata[i].fraction>100||""===_answerdata[i].fraction.trim())&&_answerdata[i].hasErrors.push("error_custom_rate"),"100"!==_answerdata[i].fraction&&"="!==_answerdata[i].fraction||(""!==_answerdata[i].raw?(_answerdata[i].isCorrect=!0,hasCorrect=!0):_answerdata[i].hasErrors.push("correct_but_empty")),errors=errors.concat(_answerdata[i].hasErrors);return{hasCorrectAnswer:hasCorrect,errors:_combineGlobalErrors(hasCorrect,errors)}},_translateGlobalErrors=function(hasCorrectAnswer,errors){const errTranslated=[],trMsg={emptyanswer:STR.err_empty_answer,answernotnumeric:STR.err_not_numeric,tolerancenotnumeric:STR.err_not_numeric,errorcustomrate:STR.err_custom_rate,nonecorrect:STR.err_none_correct};for(const err of errors){if(hasCorrectAnswer&&"empty_answer"===err||"correct_but_empty"===err)continue;const key=err.replace(/_/g,"");errTranslated.push(trMsg[key])}return errTranslated},_combineGlobalErrors=function(hasCorrectAnswer,errors){const errUnique=errors.filter(((value,index,array)=>array.indexOf(value)===index));if(hasCorrectAnswer){const i=errUnique.indexOf("empty_answer");i>-1&&errUnique.splice(i,1)}else errUnique.includes("correct_but_empty")||errUnique.push("none_correct");return errUnique},resolveSubquestion=function(){let span=_editor.selection.getStart();return!isNull(span.classList)&&span.classList.contains(markerClass)?span:(_editor.dom.getParents(span,(elm=>!(isNull(elm.classList)||!elm.classList.contains(markerClass))&&elm)),!1)};_exports.resolveSubquestion=resolveSubquestion})); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.resolveSubquestion=_exports.onSubmit=_exports.onInit=_exports.onBeforeGetContent=_exports.displayDialogue=void 0,_modal_events=_interopRequireDefault(_modal_events),_modal2=_interopRequireDefault(_modal2),_modal_factory=_interopRequireDefault(_modal_factory),_mustache=_interopRequireDefault(_mustache);const isNull=a=>null==a,strdecode=t=>String(t).replace(/\\(#|\}|~)/g,"$1"),strencode=t=>String(t).replace(/(#|\}|~)/g,"\\$1"),indexOfNode=(list,node)=>{for(let i=0;i{const attrSel=' selected="selected"';let isSel="="===s?attrSel:"",html='");return FRACTIONS.forEach((item=>{isSel=item.value.toString()===s?attrSel:"",html+='")})),isSel=""!==s&&-1===html.indexOf(attrSel)?attrSel:"",html+='"),html},isCustomGrade=s=>{if("="===s||""===s)return!1;let found=!1;return FRACTIONS.forEach((item=>{item.value.toString()===s&&(found=!0)})),!found},markerClass="cloze-question-marker",markerSpan='',reQtype=/\{([0-9]*):(MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?):(.*?)\}/g,CSS={ANSWER:"tiny_cloze_answer",ANSWERS:"tiny_cloze_answers",ADD:"tiny_cloze_add",CANCEL:"tiny_cloze_cancel",DELETE:"tiny_cloze_delete",FEEDBACK:"tiny_cloze_feedback",FRACTION:"tiny_cloze_fraction",FRAC_CUSTOM:"tiny_cloze_frac_custom",LEFT:"tiny_cloze_col0",LOWER:"tiny_cloze_down",RIGHT:"tiny_cloze_col1",MARKS:"tiny_cloze_marks",DUPLICATE:"tiny_cloze_duplicate",RAISE:"tiny_cloze_up",SUBMIT:"tiny_cloze_submit",SUMMARY:"tiny_cloze_summary",TOLERANCE:"tiny_cloze_tolerance",TYPE:"tiny_cloze_qtype"},TEMPLATE={FORM:'

{{name}} ({{qtype}})

{{STR.addmoreanswerblanks}}
    {{#answerdata}}
  1. {{STR.addmoreanswerblanks}}{{STR.delete}}{{STR.up}}{{STR.down}}
    {{#numerical}}
    {{/numerical}}
    %
  2. {{/answerdata}}
',TYPE:'

{{STR.chooseqtypetoadd}}

{{#types}}
{{/types}}
',FOOTER:''},FRACTIONS=[{value:100},{value:50},{value:0}],STR={},getQuestionTypes=function(){return[{type:"MULTICHOICE",abbr:["MC"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.selectinline,STR.singleyes]},{type:"MULTICHOICE_H",abbr:["MCH"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.horizontal,STR.singleyes]},{type:"MULTICHOICE_V",abbr:["MCV"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.vertical,STR.singleyes]},{type:"MULTICHOICE_S",abbr:["MCS"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.selectinline,STR.shuffle,STR.singleyes]},{type:"MULTICHOICE_HS",abbr:["MCHS"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.horizontal,STR.shuffle,STR.singleyes]},{type:"MULTICHOICE_VS",abbr:["MCVS"],name:STR.multichoice,summary:STR.summary_multichoice,options:[STR.vertical,STR.shuffle,STR.singleyes]},{type:"MULTIRESPONSE",abbr:["MR"],name:STR.multiresponse,summary:STR.summary_multichoice,options:[STR.multi_vertical,STR.singleno]},{type:"MULTIRESPONSE_H",abbr:["MRH"],name:STR.multiresponse,summary:STR.summary_multichoice,options:[STR.multi_horizontal,STR.singleno]},{type:"MULTIRESPONSE_S",abbr:["MRS"],name:STR.multiresponse,summary:STR.summary_multichoice,options:[STR.multi_vertical,STR.shuffle,STR.singleno]},{type:"MULTIRESPONSE_HS",abbr:["MRHS"],name:STR.multiresponse,summary:STR.summary_multichoice,options:[STR.multi_horizontal,STR.shuffle,STR.singleno]},{type:"NUMERICAL",abbr:["NM"],name:STR.numerical,summary:STR.summary_numerical},{type:"SHORTANSWER",abbr:["SA","MW"],name:STR.shortanswer,summary:STR.summary_shortanswer,options:[STR.caseno]},{type:"SHORTANSWER_C",abbr:["SAC","MWC"],name:STR.shortanswer,summary:STR.summary_shortanswer,options:[STR.caseyes]}]};let _editor=null,_form=null,_answerdata=[],_qtype=null,_selectedOffset=-1,_marks=1,_modal=null,_firstAnswer=null;_exports.onInit=function(ed){_editor=ed,_addMarkers(),(async()=>{(0,_str.get_strings)([{key:"answer",component:"question"},{key:"chooseqtypetoadd",component:"question"},{key:"defaultmark",component:"question"},{key:"feedback",component:"question"},{key:"correct",component:"question"},{key:"incorrect",component:"question"},{key:"addmoreanswerblanks",component:"qtype_calculated"},{key:"delete",component:"core"},{key:"up",component:"core"},{key:"down",component:"core"},{key:"tolerance",component:"qtype_calculated"},{key:"grade",component:"grades"},{key:"caseno",component:"mod_quiz"},{key:"caseyes",component:"mod_quiz"},{key:"answersingleno",component:"qtype_multichoice"},{key:"answersingleyes",component:"qtype_multichoice"},{key:"layoutselectinline",component:"qtype_multianswer"},{key:"layouthorizontal",component:"qtype_multianswer"},{key:"layoutvertical",component:"qtype_multianswer"},{key:"shufflewithin",component:"mod_quiz"},{key:"layoutmultiple_horizontal",component:"qtype_multianswer"},{key:"layoutmultiple_vertical",component:"qtype_multianswer"},{key:"pluginnamesummary",component:"qtype_multichoice"},{key:"pluginnamesummary",component:"qtype_shortanswer"},{key:"pluginnamesummary",component:"qtype_numerical"},{key:"multichoice",component:_common.component},{key:"multiresponse",component:_common.component},{key:"numerical",component:"mod_quiz"},{key:"shortanswer",component:"mod_quiz"},{key:"cancel",component:"core"},{key:"select",component:_common.component},{key:"insert",component:_common.component},{key:"pluginname",component:_common.component},{key:"customgrade",component:_common.component},{key:"err_custom_rate",component:_common.component},{key:"err_empty_answer",component:_common.component},{key:"err_none_correct",component:_common.component},{key:"err_not_numeric",component:_common.component}]).then((function(){const args=Array.from(arguments);return["answer","chooseqtypetoadd","defaultmark","feedback","correct","incorrect","addmoreanswerblanks","delete","up","down","tolerance","grade","caseno","caseyes","singleno","singleyes","selectinline","horizontal","vertical","shuffle","multi_horizontal","multi_vertical","summary_multichoice","summary_shortanswer","summary_numerical","multichoice","multiresponse","numerical","shortanswer","btn_cancel","btn_select","btn_insert","title","custom_grade","err_custom_rate","err_empty_answer","err_none_correct","err_not_numeric"].map(((l,i)=>(STR[l]=args[0][i],""))),""})).catch((()=>""))})()};const _createModal=async function(){const cfg={title:STR.title,templateContext:{elementid:_editor.id},removeOnClose:!0,large:!0};_modal="function"==typeof _modal2.default.create?await _modal2.default.create(cfg):await _modal_factory.default.create(cfg)};_exports.displayDialogue=async function(){await _createModal();var subquestion=resolveSubquestion();subquestion?(_firstAnswer=null,_selectedOffset=indexOfNode(_editor.dom.select("."+markerClass),subquestion),_parseSubquestion(subquestion.innerHTML),_setDialogueContent(_qtype)):(_firstAnswer=_editor.selection.getContent(),_selectedOffset=-1,_setDialogueContent())};const _addMarkers=function(){let m,content=_editor.getContent(),newContent="";if(-1===content.indexOf(markerClass)){do{if(m=content.match(reQtype),!m){newContent+=content;break}const pos=content.indexOf(m[0]);newContent+=content.substring(0,pos)+markerSpan+content.substring(pos,pos+m[0].length),content=content.substring(pos+m[0].length);let level=(m[0].match(/\{/g)||[]).length;if(1!==level){for(;level>1;){const a=content.indexOf("{"),b=content.indexOf("}");a>-1&&b>-1&&a-1?(newContent=content.substring(0,b),content=content.substring(b+1),level--):level=1}newContent+="
"}else newContent+=""}while(m);_editor.setContent(newContent)}},_removeMarkers=function(){for(const span of _editor.dom.select("span."+markerClass))_editor.dom.setOuterHTML(span,span.classList.contains("new")?"":span.innerHTML)};_exports.onBeforeGetContent=function(content){if(!isNull(content.source_view)&&!0===content.source_view){var onClose=function(){_editor.off("close",onClose),_addMarkers()};_editor.on("CloseWindow",(()=>{onClose()})),_modal||_removeMarkers()}};_exports.onSubmit=function(){_removeMarkers()};const _setDialogueContent=function(qtype,nomodalevents){const footer=_mustache.default.render(TEMPLATE.FOOTER,{cancel:STR.btn_cancel,submit:qtype?STR.btn_insert:STR.btn_select});let contentText;contentText=qtype?_mustache.default.render(TEMPLATE.FORM,{CSS:CSS,STR:STR,answerdata:_answerdata,elementid:getUuid(),qtype:_qtype,name:getQuestionTypes().filter((q=>_qtype===q.type))[0].name,marks:_marks,numerical:"NUMERICAL"===_qtype||"NM"===_qtype}):_mustache.default.render(TEMPLATE.TYPE,{CSS:CSS,STR:STR,qtype:_qtype,types:getQuestionTypes()}),_modal.setBody(contentText),_modal.setFooter(footer),_modal.show();const $root=_modal.getRoot();if(_form=$root.get(0).querySelector("form"),_toggleDeleteIcon(),!nomodalevents){if(_modal.registerEventListeners(),_modal.registerCloseOnSave(),_modal.registerCloseOnCancel(),$root.on(_modal_events.default.cancel,_cancel),!qtype)return void $root.on(_modal_events.default.save,_choiceHandler);$root.on(_modal_events.default.save,_setSubquestion)}const getTarget=e=>{let p=e.target;for(;!isNull(p)&&1===p.nodeType&&"A"!==p.tagName;)p=p.parentNode;return isNull(p.classList)?null:p};_form.addEventListener("click",(e=>{const p=getTarget(e);if(!isNull(p))return p.classList.contains(CSS.DELETE)?(e.preventDefault(),void _deleteAnswer(p)):p.classList.contains(CSS.ADD)?(e.preventDefault(),void _addAnswer(p)):p.classList.contains(CSS.LOWER)?(e.preventDefault(),void _lowerAnswer(p)):void(p.classList.contains(CSS.RAISE)&&(e.preventDefault(),_raiseAnswer(p)))})),_form.addEventListener("keyup",(e=>{const p=getTarget(e);isNull(p)||(p.classList.contains(CSS.ANSWER)||p.classList.contains(CSS.FEEDBACK))&&(e.preventDefault(),_addAnswer(p))})),_form.querySelectorAll("."+CSS.FRACTION).forEach((sel=>{sel.addEventListener("change",(e=>{const id=e.target.getAttribute("id");"__custom__"===e.target.value?document.getElementById(id+"_custom").parentNode.classList.remove("hidden"):document.getElementById(id+"_custom").parentNode.classList.add("hidden")}))}))},_toggleDeleteIcon=function(){const deleteIcons=_form.querySelectorAll("."+CSS.DELETE);if(1!==deleteIcons.length)for(let i=0;i(_setDialogueContent(_qtype),_form.querySelector("."+CSS.ANSWER).focus(),""))).catch((()=>""))},_parseSubquestion=function(question){_answerdata=[];const parts=reQtype.exec(question);if(reQtype.lastIndex=0,!parts)return;_marks=parts[1],_qtype=parts[2],_qtype.length<5&&getQuestionTypes().forEach((l=>{for(const a of l.abbr)if(a===_qtype)return void(_qtype=l.type)}));const answers=parts[7].match(/(\\.|[^~])*/g);answers&&answers.forEach((function(answer){const options=/^(%(-?[.0-9]+)%|(=?))((\\.|[^#])*)#?(.*)/.exec(answer);if(options&&options[4]){let frac="";if(options[3]?frac="="===options[3]?"=":100:options[2]&&(frac=options[2]),"NUMERICAL"===_qtype||"NM"===_qtype){const tolerance=/^([^:]*):?(.*)/.exec(options[4])[2]||0;return void _answerdata.push({id:getUuid(),answer:strdecode(options[4].replace(/:.*/,"")),feedback:strdecode(options[6]),tolerance:tolerance,fraction:frac,fractionOptions:getFractionOptions(frac),isCustomGrade:isCustomGrade(frac)})}_answerdata.push({answer:strdecode(options[4]),id:getUuid(),feedback:strdecode(options[6]),fraction:frac,fractionOptions:getFractionOptions(frac),isCustomGrade:isCustomGrade(frac)})}}))},_addAnswer=function(a){let index=indexOfNode(_form.querySelectorAll("."+CSS.ADD),a);-1===index&&(index=0);let fraction="",answer="",feedback="",tolerance=0;a.closest("li")&&(fraction=a.closest("li").querySelector("."+CSS.FRACTION).value,"__custom__"===fraction&&(fraction=a.closest("li").querySelector("."+CSS.FRAC_CUSTOM).value),answer=a.closest("li").querySelector("."+CSS.ANSWER).value,feedback=a.closest("li").querySelector("."+CSS.FEEDBACK).value,a.closest("li").querySelector("."+CSS.TOLERANCE)&&(tolerance=a.closest("li").querySelector("."+CSS.TOLERANCE).value)),_processFormData(),_answerdata.splice(index,0,{id:getUuid(),answer:answer,feedback:feedback,fraction:fraction,fractionOptions:getFractionOptions(fraction),tolerance:tolerance,isCustomGrade:isCustomGrade(fraction)}),_setDialogueContent(_qtype,!0),_toggleDeleteIcon(),_form.querySelectorAll("."+CSS.ANSWER).item(index).focus()},_deleteAnswer=function(a){let index=indexOfNode(_form.querySelectorAll("."+CSS.DELETE),a);-1===index&&(index=indexOfNode(_form.querySelectorAll("li"),a.closest("li"))),_processFormData(),_answerdata.splice(index,1),_setDialogueContent(_qtype,!0);const answers=_form.querySelectorAll("."+CSS.ANSWER);index=Math.min(index,answers.length-1),answers.item(index).focus(),_toggleDeleteIcon()},_lowerAnswer=function(a){const li=a.closest("li");li.before(li.nextSibling),li.querySelector("."+CSS.ANSWER).focus()},_raiseAnswer=function(a){const li=a.closest("li");li.after(li.previousSibling),li.querySelector("."+CSS.ANSWER).focus()},_cancel=function(e){e.preventDefault();for(const span of _editor.dom.select("."+markerClass+".new"))span.remove();_modal.destroy(),_editor.focus(),_modal=null},_setSubquestion=function(e){e.preventDefault();const errMsg=_form.querySelector(".msg-error"),formErrors=_processFormData(!0);if(formErrors.length>0)return errMsg.innerHTML="
  • "+formErrors.join("
  • ")+"
",void errMsg.classList.remove("hidden");errMsg.classList.add("hidden");let question="{"+_marks+":"+_qtype+":";for(let i=0;i<_answerdata.length;i++)""!==_answerdata[i].raw&&(question+=_answerdata[i].fraction&&!isNaN(_answerdata[i].fraction)?"%"+_answerdata[i].fraction+"%":_answerdata[i].fraction,question+=strencode(_answerdata[i].answer),"NM"!==_qtype&&"NUMERICAL"!==_qtype||(question+=":"+_answerdata[i].tolerance),_answerdata[i].feedback&&(question+="#"+strencode(_answerdata[i].feedback)),i<_answerdata.length-1&&(question+="~"));"~"===question.slice(-1)&&(question=question.substring(0,question.length-1)),question+="}",_modal.destroy(),_modal=null,_editor.focus(),_selectedOffset>-1?_editor.dom.select("."+markerClass)[_selectedOffset].innerHTML=question:_editor.insertContent(markerSpan+question+"")},_processFormData=function(validate){_answerdata=[];let globalErrors=[];const answers=_form.querySelectorAll("."+CSS.ANSWER),feedbacks=_form.querySelectorAll("."+CSS.FEEDBACK),fractions=_form.querySelectorAll("."+CSS.FRACTION),customGrades=_form.querySelectorAll("."+CSS.FRAC_CUSTOM),tolerances=_form.querySelectorAll("."+CSS.TOLERANCE);for(let i=0;i0?tolerances.item(i).value:0,isCustomGrade:"__custom__"===fractions.item(i).value};"NM"!==_qtype&&"NUMERICAL"!==_qtype||(tolerances.item(i).classList.remove("error"),currentAnswer.answer=Number(currentAnswer.answer),currentAnswer.tolerance=Number(currentAnswer.tolerance)),_answerdata.push(currentAnswer)}if(_marks=_form.querySelector("."+CSS.MARKS).value,validate){const{hasCorrectAnswer:hasCorrectAnswer,errors:errors}=_validateAnswers();for(let i=0;i<_answerdata.length;i++)for(const err of _answerdata[i].hasErrors){if(hasCorrectAnswer&&("empty_answer"===err||"correct_but_empty"===err))break;"answer_not_numeric"===err||"empty_answer"===err||"correct_but_empty"===err?answers.item(i).classList.add("error"):"tolerance_not_numeric"===err?tolerances.item(i).classList.add("error"):"error_custom_rate"===err&&customGrades.item(i).classList.add("error")}globalErrors=_translateGlobalErrors(hasCorrectAnswer,errors),globalErrors.length>0&&_form.querySelector("input.error").focus()}return globalErrors},_validateAnswers=function(){let errors=[],hasCorrect=!1;for(let i=0;i<_answerdata.length;i++)_answerdata[i].hasErrors=[],""===_answerdata[i].raw&&_answerdata[i].hasErrors.push("empty_answer"),"NM"!==_qtype&&"NUMERICAL"!==_qtype||(isNaN(_answerdata[i].answer)&&""!==_answerdata[i].raw&&_answerdata[i].hasErrors.push("answer_not_numeric"),isNaN(_answerdata[i].tolerance)&&_answerdata[i].hasErrors.push("tolerance_not_numeric")),_answerdata[i].isCustomGrade&&(isNaN(_answerdata[i].fraction)||_answerdata[i].fraction<-100||_answerdata[i].fraction>100||""===_answerdata[i].fraction.trim())&&_answerdata[i].hasErrors.push("error_custom_rate"),"100"!==_answerdata[i].fraction&&"="!==_answerdata[i].fraction||(""!==_answerdata[i].raw?(_answerdata[i].isCorrect=!0,hasCorrect=!0):_answerdata[i].hasErrors.push("correct_but_empty")),errors=errors.concat(_answerdata[i].hasErrors);return{hasCorrectAnswer:hasCorrect,errors:_combineGlobalErrors(hasCorrect,errors)}},_translateGlobalErrors=function(hasCorrectAnswer,errors){const errTranslated=[],trMsg={emptyanswer:STR.err_empty_answer,answernotnumeric:STR.err_not_numeric,tolerancenotnumeric:STR.err_not_numeric,errorcustomrate:STR.err_custom_rate,nonecorrect:STR.err_none_correct};for(const err of errors){if(hasCorrectAnswer&&"empty_answer"===err||"correct_but_empty"===err)continue;const key=err.replace(/_/g,"");errTranslated.push(trMsg[key])}return errTranslated},_combineGlobalErrors=function(hasCorrectAnswer,errors){const errUnique=errors.filter(((value,index,array)=>array.indexOf(value)===index));if(hasCorrectAnswer){const i=errUnique.indexOf("empty_answer");i>-1&&errUnique.splice(i,1)}else errUnique.includes("correct_but_empty")||errUnique.push("none_correct");return errUnique},resolveSubquestion=function(){let span=_editor.selection.getStart();return!isNull(span.classList)&&span.classList.contains(markerClass)?span:(_editor.dom.getParents(span,(elm=>!(isNull(elm.classList)||!elm.classList.contains(markerClass))&&elm)),!1)};_exports.resolveSubquestion=resolveSubquestion})); //# sourceMappingURL=ui.min.js.map \ No newline at end of file diff --git a/amd/build/ui.min.js.map b/amd/build/ui.min.js.map index 4fe70ae..d6700ce 100644 --- a/amd/build/ui.min.js.map +++ b/amd/build/ui.min.js.map @@ -1 +1 @@ -{"version":3,"file":"ui.min.js","sources":["../src/ui.js"],"sourcesContent":["// This file is part of Moodle - https://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Plugin tiny_cloze for TinyMCE v6 in Moodle.\n *\n * @module tiny_cloze/ui\n * @copyright 2023 MoodleDACH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalEvents from 'core/modal_events';\nimport Modal from 'core/modal';\nimport ModalFactory from 'core/modal_factory';\nimport Mustache from 'core/mustache';\nimport {get_strings as getStrings} from 'core/str';\nimport {component} from './common';\n\n// Helper functions.\nconst isNull = a => a === null || a === undefined;\nconst strdecode = t => String(t).replace(/\\\\(#|\\}|~)/g, '$1');\nconst strencode = t => String(t).replace(/(#|\\}|~)/g, '\\\\$1');\nconst indexOfNode = (list, node) => {\n for (let i = 0; i < list.length; i++) {\n if (list[i] === node) {\n return i;\n }\n }\n return -1;\n};\nconst getUuid = function() {\n if (!isNull(crypto.randomUUID)) {\n return crypto.randomUUID();\n }\n return 'ed-cloze-' + Math.floor(Math.random() * 100000).toString();\n};\n// Grade Selector value when custom percentage is selected.\nconst selectCustomPercent = '__custom__';\n// This is a specific helper function to return the options html for the fraction select element.\nconst getFractionOptions = s => {\n const attrSel = ' selected=\"selected\"';\n let isSel = s === '=' ? attrSel : '';\n let html = ``;\n FRACTIONS.forEach(item => {\n isSel = item.value.toString() === s ? attrSel : '';\n html += ``;\n });\n isSel = s !== '' && html.indexOf(attrSel) === -1 ? attrSel : '';\n html += ``;\n return html;\n};\n// Check if the value is a custom grade value (in order to show the input field).\nconst isCustomGrade = s => {\n if (s === '=' || s === '') {\n return false;\n }\n let found = false;\n FRACTIONS.forEach(item => {\n if (item.value.toString() === s) {\n found = true;\n }\n });\n return !found;\n};\n// Marker class and the whole span element that is used to encapsulate the cloze question text.\nconst markerClass = 'cloze-question-marker';\nconst markerSpan = '';\n// Regex to recognize the question string in the text e.g. {1:NUMERICAL:...} or {:MULTICHOICE:...}\n// eslint-disable-next-line max-len\nconst reQtype = /\\{([0-9]*):(MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?):(.*?)\\}/g;\n\n// CSS classes that are used in the modal dialogue.\nconst CSS = {\n ANSWER: 'tiny_cloze_answer',\n ANSWERS: 'tiny_cloze_answers',\n ADD: 'tiny_cloze_add',\n CANCEL: 'tiny_cloze_cancel',\n DELETE: 'tiny_cloze_delete',\n FEEDBACK: 'tiny_cloze_feedback',\n FRACTION: 'tiny_cloze_fraction',\n FRAC_CUSTOM: 'tiny_cloze_frac_custom',\n LEFT: 'tiny_cloze_col0',\n LOWER: 'tiny_cloze_down',\n RIGHT: 'tiny_cloze_col1',\n MARKS: 'tiny_cloze_marks',\n DUPLICATE: 'tiny_cloze_duplicate',\n RAISE: 'tiny_cloze_up',\n SUBMIT: 'tiny_cloze_submit',\n SUMMARY: 'tiny_cloze_summary',\n TOLERANCE: 'tiny_cloze_tolerance',\n TYPE: 'tiny_cloze_qtype'\n};\nconst TEMPLATE = {\n FORM: '
' +\n '

{{name}} ({{qtype}})

' +\n '
' +\n '
' +\n '
' +\n '' +\n '' +\n '' +\n '\"{{STR.addmoreanswerblanks}}\"' +\n '
' +\n '
' +\n '
' +\n '
' +\n '
    {{#answerdata}}' +\n '
  1. ' +\n '
    ' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '' +\n '\"{{STR.addmoreanswerblanks}}\"' +\n '' +\n '\"{{STR.delete}}\"' +\n '' +\n '\"{{STR.up}}\"' +\n '' +\n '\"{{STR.down}}\"' +\n '
    ' +\n '
    ' +\n '{{#numerical}}' +\n '
    ' +\n '
    ' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '{{/numerical}}' +\n '
    ' +\n '
    ' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '%' +\n '
    ' +\n '
  2. ' +\n '{{/answerdata}}
' +\n '
' +\n '
',\n TYPE: '
' +\n '

{{STR.chooseqtypetoadd}}

' +\n '
' +\n '
' +\n '{{#types}}' +\n '
' +\n '' +\n '
' +\n '{{/types}}
' +\n '
',\n FOOTER: '' +\n '',\n};\n const FRACTIONS = [\n {value: 100},\n {value: 50},\n {value: 0},\n ];\n\n// Language strings used in the modal dialogue.\nconst STR = {};\nconst getStr = async() => {\n getStrings([\n {key: 'answer', component: 'question'},\n {key: 'chooseqtypetoadd', component: 'question'},\n {key: 'defaultmark', component: 'question'},\n {key: 'feedback', component: 'question'},\n {key: 'correct', component: 'question'},\n {key: 'incorrect', component: 'question'},\n {key: 'addmoreanswerblanks', component: 'qtype_calculated'},\n {key: 'delete', component: 'core'},\n {key: 'up', component: 'core'},\n {key: 'down', component: 'core'},\n {key: 'tolerance', component: 'qtype_calculated'},\n {key: 'grade', component: 'grades'},\n {key: 'caseno', component: 'mod_quiz'},\n {key: 'caseyes', component: 'mod_quiz'},\n {key: 'answersingleno', component: 'qtype_multichoice'},\n {key: 'answersingleyes', component: 'qtype_multichoice'},\n {key: 'layoutselectinline', component: 'qtype_multianswer'},\n {key: 'layouthorizontal', component: 'qtype_multianswer'},\n {key: 'layoutvertical', component: 'qtype_multianswer'},\n {key: 'shufflewithin', component: 'mod_quiz'},\n {key: 'layoutmultiple_horizontal', component: 'qtype_multianswer'},\n {key: 'layoutmultiple_vertical', component: 'qtype_multianswer'},\n {key: 'pluginnamesummary', component: 'qtype_multichoice'},\n {key: 'pluginnamesummary', component: 'qtype_shortanswer'},\n {key: 'pluginnamesummary', component: 'qtype_numerical'},\n {key: 'multichoice', component},\n {key: 'multiresponse', component},\n {key: 'numerical', component: 'mod_quiz'},\n {key: 'shortanswer', component: 'mod_quiz'},\n {key: 'cancel', component: 'core'},\n {key: 'select', component},\n {key: 'insert', component},\n {key: 'pluginname', component},\n {key: 'customgrade', component},\n {key: 'err_custom_rate', component},\n {key: 'err_empty_answer', component},\n {key: 'err_none_correct', component},\n {key: 'err_not_numeric', component},\n ]).then(function() {\n const args = Array.from(arguments);\n [\n 'answer',\n 'chooseqtypetoadd',\n 'defaultmark',\n 'feedback',\n 'correct',\n 'incorrect',\n 'addmoreanswerblanks',\n 'delete',\n 'up',\n 'down',\n 'tolerance',\n 'grade',\n 'caseno',\n 'caseyes',\n 'singleno',\n 'singleyes',\n 'selectinline',\n 'horizontal',\n 'vertical',\n 'shuffle',\n 'multi_horizontal',\n 'multi_vertical',\n 'summary_multichoice',\n 'summary_shortanswer',\n 'summary_numerical',\n 'multichoice',\n 'multiresponse',\n 'numerical',\n 'shortanswer',\n 'btn_cancel',\n 'btn_select',\n 'btn_insert',\n 'title',\n 'custom_grade',\n 'err_custom_rate',\n 'err_empty_answer',\n 'err_none_correct',\n 'err_not_numeric',\n ].map((l, i) => {\n STR[l] = args[0][i];\n return ''; // Make the linter happy.\n });\n return ''; // Make the linter happy.\n }).catch(() => {\n return '';\n });\n};\nconst getQuestionTypes = function() {\n return [\n {\n 'type': 'MULTICHOICE',\n 'abbr': ['MC'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.selectinline, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_H',\n 'abbr': ['MCH'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.horizontal, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_V',\n 'abbr': ['MCV'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.vertical, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_S',\n 'abbr': ['MCS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.selectinline, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_HS',\n 'abbr': ['MCHS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.horizontal, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_VS',\n 'abbr': ['MCVS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.vertical, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTIRESPONSE',\n 'abbr': ['MR'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_vertical, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_H',\n 'abbr': ['MRH'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_horizontal, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_S',\n 'abbr': ['MRS'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_vertical, STR.shuffle, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_HS',\n 'abbr': ['MRHS'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_horizontal, STR.shuffle, STR.singleno],\n },\n {\n 'type': 'NUMERICAL',\n 'abbr': ['NM'],\n 'name': STR.numerical,\n 'summary': STR.summary_numerical,\n },\n {\n 'type': 'SHORTANSWER',\n 'abbr': ['SA', 'MW'],\n 'name': STR.shortanswer,\n 'summary': STR.summary_shortanswer,\n 'options': [STR.caseno],\n },\n {\n 'type': 'SHORTANSWER_C',\n 'abbr': ['SAC', 'MWC'],\n 'name': STR.shortanswer,\n 'summary': STR.summary_shortanswer,\n 'options': [STR.caseyes],\n },\n ];\n};\n\n/**\n * The editor instance that is injected via the onInit() function.\n *\n * @type {tinymce.Editor}\n * @private\n */\nlet _editor = null;\n\n/**\n * A reference to the currently open form.\n *\n * @param _form\n * @type {Node}\n * @private\n */\nlet _form = null;\n\n/**\n * An array containing the current answers options\n *\n * @param _answerdata\n * @type {Array}\n * @private\n */\nlet _answerdata = [];\n\n/**\n * The sub question type to be edited\n *\n * @param _qtype\n * @type {string|null}\n * @private\n */\nlet _qtype = null;\n\n/**\n * Remember the pos of the selected node.\n * @type {number}\n * @private\n */\nlet _selectedOffset = -1;\n\n/**\n * The maximum marks for the sub question\n *\n * @param _marks\n * @type {Integer}\n * @private\n */\nlet _marks = 1;\n\n/**\n * The modal dialogue to be displayed when designing the cloze question types.\n * @type {Modal|null}\n */\nlet _modal = null;\n\n/**\n * If its a normal selection of text, use it for the first answer field.\n * @type {string|null}\n */\nlet _firstAnswer = null;\n\n/**\n * Inject the editor instance and add markers to the cloze question texts.\n * @param {tinymce.Editor} ed\n */\nconst onInit = function(ed) {\n _editor = ed; // The current editor instance.\n // Add the marker spans.\n _addMarkers();\n // And get the language strings.\n getStr();\n};\n\n/**\n * Create the modal.\n * @return {Promise}\n * @private\n */\nconst _createModal = async function() {\n // Create the modal dialogue. Depending on whether we have a selected node or not, the content is different.\n const cfg = {\n title: STR.title,\n templateContext: {\n elementid: _editor.id\n },\n removeOnClose: true,\n large: true,\n };\n if (typeof Modal.create === 'function') {\n _modal = await Modal.create(cfg);\n } else {\n _modal = await ModalFactory.create(cfg);\n }\n};\n\n/**\n * Display modal dialogue to edit a cloze question. Either a form is displayed to edit subquestion or a list\n * of possible questions is show.\n *\n * @method displayDialogue\n * @private\n */\nconst displayDialogue = async function() {\n await _createModal();\n\n // Resolve whether cursor is in a subquestion.\n var subquestion = resolveSubquestion();\n if (subquestion) {\n _firstAnswer = null;\n // Subquestion found, remember which node of the marker nodes is selected.\n _selectedOffset = indexOfNode(_editor.dom.select('.' + markerClass), subquestion);\n _parseSubquestion(subquestion.innerHTML);\n _setDialogueContent(_qtype);\n } else {\n // No subquestion found, no offset to remember.\n _firstAnswer = _editor.selection.getContent();\n _selectedOffset = -1;\n _setDialogueContent();\n }\n};\n\n/**\n * Search for cloze questions based on a regular expression. All the matching snippets at least contain the cloze\n * question definition. Although Moodle does not support encapsulated other functions within curly brackets, we\n * still try to find the correct closing bracket. The so extracted cloze question is surrounded by a marker span\n * element, that contains attributes so that the content inside the span cannot be modified by the editor (in the\n * textarea). Also, this makes it a lot easier to select the question, edit it in the dialogue and replace the result\n * in the existing text area.\n */\nconst _addMarkers = function() {\n\n let content = _editor.getContent();\n let newContent = '';\n\n // Check if there is already a marker span. In this case we do not have to do anything.\n if (content.indexOf(markerClass) !== -1) {\n return;\n }\n\n let m;\n do {\n m = content.match(reQtype);\n if (!m) { // No match of a cloze question, then we are done.\n newContent += content;\n break;\n }\n // Copy the current match to the new string preceded with the .\n const pos = content.indexOf(m[0]);\n newContent += content.substring(0, pos) + markerSpan + content.substring(pos, pos + m[0].length);\n content = content.substring(pos + m[0].length);\n\n // Count the { in the string, should be just one (the very first one at position 0).\n let level = (m[0].match(/\\{/g) || []).length;\n if (level === 1) {\n // If that's the case, we close the span and the cloze question text is the innerHTML of that marker span.\n newContent += '';\n continue; // Look for the next matching cloze question.\n }\n // If there are more { than } in the string, then we did not find the corresponding } that belongs to the cloze string.\n while (level > 1) {\n const a = content.indexOf('{');\n const b = content.indexOf('}');\n if (a > -1 && b > -1 && a < b) { // The { is before another } so remember to find as many } until we back at level 1.\n level++;\n newContent = content.substring(0, a);\n content = content.substring(a + 1);\n } else if (b > -1) { // We found a closing } to a previously {.\n newContent = content.substring(0, b);\n content = content.substring(b + 1);\n level--;\n } else {\n level = 1; // Should not happen, just to stop the endless loop.\n }\n }\n newContent += '
';\n } while (m);\n _editor.setContent(newContent);\n};\n\n/**\n * Look for the marker span elements around a cloze question and remove that span. Also, the marker for a new\n * node to be inserted would be removed here as well.\n */\nconst _removeMarkers = function() {\n for (const span of _editor.dom.select('span.' + markerClass)) {\n _editor.dom.setOuterHTML(span, span.classList.contains('new') ? '' : span.innerHTML);\n }\n};\n\n/**\n * When the source code view dialogue is show, we must remove the spans around the cloze question strings\n * from the editor content and add them again when the dialogue is closed.\n * Since this event is also triggered when the editor data is saved, we use this function to remove the\n * highlighting content at that time.\n * @param {object} content\n */\nconst onBeforeGetContent = function(content) {\n if (!isNull(content.source_view) && content.source_view === true) {\n // If the user clicks on 'Cancel' or the close button on the html\n // source code dialog view, make sure we re-add the visual styling.\n var onClose = function() {\n _editor.off('close', onClose);\n _addMarkers();\n };\n _editor.on('CloseWindow', () => {\n onClose();\n });\n // Remove markers only if modal is not called, otherwise we will lose our new question marker.\n if (!_modal) {\n _removeMarkers();\n }\n }\n};\n\n/**\n * Fires when the form containing the editor is submitted.\n */\nconst onSubmit = function() {\n _removeMarkers();\n};\n\n/**\n * Set the dialogue content for the tool, attaching any required events. Either the modal dialogue displays\n * a list of the question types for the form for a particular question to edit. The set content is also\n * called when the form has changed (up or down move, deletion and adding a response). We must be aware of that\n * an event to the dialogue buttons must be attached once only. Therefore, when the form content is modified, only\n * the form events for the answers are set again, the general events are nor (nomodalevents is true then).\n *\n * @method _setDialogueContent\n * @param {String} qtype The question type to be used\n * @param {boolean} nomodalevents Optional do not attach events.\n * @private\n */\nconst _setDialogueContent = function(qtype, nomodalevents) {\n const footer = Mustache.render(TEMPLATE.FOOTER, {\n cancel: STR.btn_cancel,\n submit: !qtype ? STR.btn_select : STR.btn_insert,\n });\n let contentText;\n if (!qtype) {\n contentText = Mustache.render(TEMPLATE.TYPE, {\n CSS: CSS,\n STR: STR,\n qtype: _qtype,\n types: getQuestionTypes()\n });\n } else {\n contentText = Mustache.render(TEMPLATE.FORM, {\n CSS: CSS,\n STR: STR,\n answerdata: _answerdata,\n elementid: getUuid(),\n qtype: _qtype,\n name: getQuestionTypes().filter(q => _qtype === q.type)[0].name,\n marks: _marks,\n numerical: (_qtype === 'NUMERICAL' || _qtype === 'NM')\n });\n }\n _modal.setBody(contentText);\n _modal.setFooter(footer);\n _modal.show();\n const $root = _modal.getRoot();\n _form = $root.get(0).querySelector('form');\n\n if (!nomodalevents) {\n _modal.registerEventListeners();\n _modal.registerCloseOnSave();\n _modal.registerCloseOnCancel();\n $root.on(ModalEvents.cancel, _cancel);\n\n if (!qtype) { // For the question list we need the choice handler only, and we are done.\n $root.on(ModalEvents.save, _choiceHandler);\n return;\n } // Handler to add the question string to the editor content.\n $root.on(ModalEvents.save, _setSubquestion);\n }\n // The form needs events for the icons to move up/down, add or delete a response.\n const getTarget = e => {\n let p = e.target;\n while (!isNull(p) && p.nodeType === 1 && p.tagName !== 'A') {\n p = p.parentNode;\n }\n if (isNull(p.classList)) {\n return null;\n }\n return p;\n };\n\n _form.addEventListener('click', e => {\n const p = getTarget(e);\n if (isNull(p)) {\n return;\n }\n if (p.classList.contains(CSS.DELETE)) {\n e.preventDefault();\n _deleteAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.ADD)) {\n e.preventDefault();\n _addAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.LOWER)) {\n e.preventDefault();\n _lowerAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.RAISE)) {\n e.preventDefault();\n _raiseAnswer(p);\n }\n });\n _form.addEventListener('keyup', e => {\n const p = getTarget(e);\n if (isNull(p)) {\n return;\n }\n if (p.classList.contains(CSS.ANSWER) || p.classList.contains(CSS.FEEDBACK)) {\n e.preventDefault();\n _addAnswer(p);\n }\n });\n _form.querySelectorAll('.' + CSS.FRACTION).forEach((sel) => {\n sel.addEventListener('change', e => {\n const id = e.target.getAttribute('id');\n if (e.target.value === selectCustomPercent) {\n document.getElementById(id + '_custom').parentNode.classList.remove('hidden');\n } else {\n document.getElementById(id + '_custom').parentNode.classList.add('hidden');\n }\n });\n });\n};\n\n/**\n * Handle question choice.\n *\n * @method _choiceHandler\n * @private\n * @param {Event} e Event from button click in chooser\n */\nconst _choiceHandler = function(e) {\n e.preventDefault();\n let qtype = _form.querySelector('input[name=qtype]:checked');\n if (qtype) {\n _qtype = qtype.value;\n }\n // For numerical and short answer questions we offer one response field only. All other\n // question types have three empty response fields.\n const max = (_qtype.indexOf('SHORTANSWER') !== -1 || _qtype === 'NUMERICAL') ? 1 : 3;\n const blankAnswer = {\n id: getUuid(),\n answer: '',\n feedback: '',\n fraction: 100,\n fractionOptions: getFractionOptions(''),\n tolerance: 0,\n isCustomGrade: false,\n };\n _answerdata = [];\n for (let x = 0; x < max; x++) {\n _answerdata.push({...blankAnswer, id: getUuid()});\n }\n // The first response field gets the default grade correct.\n _answerdata[0].fractionOptions = getFractionOptions('=');\n // In case the user seleced some text, this is used as the first answer.\n if (_firstAnswer) {\n _answerdata[0].answer = _firstAnswer;\n }\n _modal.destroy();\n // Our choice is stored in _qtype. We need to create the modal dialogue with the form now.\n _createModal().then(() => {\n _setDialogueContent(_qtype);\n _form.querySelector('.' + CSS.ANSWER).focus();\n return ''; // Make the linter happy.\n }).catch(() => {\n return '';\n });\n};\n\n/**\n * Parse question and set properties found.\n *\n * @method _parseSubquestion\n * @private\n * @param {String} question The question string\n */\nconst _parseSubquestion = function(question) {\n _answerdata = []; // Flush answers to have an empty dialogue if something goes wrong parsing the question string.\n const parts = reQtype.exec(question);\n reQtype.lastIndex = 0; // Reset lastIndex so that the next match starts from the beginning of the question string.\n if (!parts) {\n return;\n }\n _marks = parts[1];\n _qtype = parts[2];\n // Convert the short notation to the long form e.g. SA to SHORTANSWER.\n if (_qtype.length < 5) {\n getQuestionTypes().forEach(l => {\n for (const a of l.abbr) {\n if (a === _qtype) {\n _qtype = l.type;\n return;\n }\n }\n });\n }\n const answers = parts[7].match(/(\\\\.|[^~])*/g);\n if (!answers) {\n return;\n }\n answers.forEach(function(answer) {\n const options = /^(%(-?[.0-9]+)%|(=?))((\\\\.|[^#])*)#?(.*)/.exec(answer);\n if (options && options[4]) {\n let frac = '';\n if (options[3]) {\n frac = options[3] === '=' ? '=' : 100;\n } else if (options[2]) {\n frac = options[2];\n }\n if (_qtype === 'NUMERICAL' || _qtype === 'NM') {\n const tolerance = /^([^:]*):?(.*)/.exec(options[4])[2] || 0;\n _answerdata.push({\n id: getUuid(),\n answer: strdecode(options[4].replace(/:.*/, '')),\n feedback: strdecode(options[6]),\n tolerance: tolerance,\n fraction: frac,\n fractionOptions: getFractionOptions(frac),\n isCustomGrade: isCustomGrade(frac),\n });\n return;\n }\n _answerdata.push({\n answer: strdecode(options[4]),\n id: getUuid(),\n feedback: strdecode(options[6]),\n fraction: frac,\n fractionOptions: getFractionOptions(frac),\n isCustomGrade: isCustomGrade(frac),\n });\n }\n });\n};\n\n/**\n * Insert a new set of answer blanks below the button.\n *\n * @method _addAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _addAnswer = function(a) {\n let index = indexOfNode(_form.querySelectorAll('.' + CSS.ADD), a);\n if (index === -1) {\n index = 0;\n }\n let fraction = '';\n let answer = '';\n let feedback = '';\n let tolerance = 0;\n if (a.closest('li')) {\n fraction = a.closest('li').querySelector('.' + CSS.FRACTION).value;\n if (fraction === selectCustomPercent) {\n fraction = a.closest('li').querySelector('.' + CSS.FRAC_CUSTOM).value;\n }\n answer = a.closest('li').querySelector('.' + CSS.ANSWER).value;\n feedback = a.closest('li').querySelector('.' + CSS.FEEDBACK).value;\n if (a.closest('li').querySelector('.' + CSS.TOLERANCE)) {\n tolerance = a.closest('li').querySelector('.' + CSS.TOLERANCE).value;\n }\n }\n _processFormData();\n _answerdata.splice(index, 0, {\n id: getUuid(),\n answer: answer,\n feedback: feedback,\n fraction: fraction,\n fractionOptions: getFractionOptions(fraction),\n tolerance: tolerance,\n isCustomGrade: isCustomGrade(fraction)\n });\n _setDialogueContent(_qtype, true);\n _form.querySelectorAll('.' + CSS.ANSWER).item(index).focus();\n};\n\n/**\n * Delete set of answer next to the button.\n *\n * @method _deleteAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _deleteAnswer = function(a) {\n let index = indexOfNode(_form.querySelectorAll('.' + CSS.DELETE), a);\n if (index === -1) {\n index = indexOfNode(_form.querySelectorAll('li'), a.closest('li'));\n }\n _processFormData();\n _answerdata.splice(index, 1);\n _setDialogueContent(_qtype, true);\n const answers = _form.querySelectorAll('.' + CSS.ANSWER);\n index = Math.min(index, answers.length - 1);\n answers.item(index).focus();\n};\n\n/**\n * Lower answer option\n *\n * @method _lowerAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _lowerAnswer = function(a) {\n const li = a.closest('li');\n li.before(li.nextSibling);\n li.querySelector('.' + CSS.ANSWER).focus();\n};\n\n/**\n * Raise answer option\n *\n * @method _raiseAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _raiseAnswer = function(a) {\n const li = a.closest('li');\n li.after(li.previousSibling);\n li.querySelector('.' + CSS.ANSWER).focus();\n};\n\n/**\n * Reset and hide form.\n *\n * @method _cancel\n * @param {Event} e Event from button click\n * @private\n */\nconst _cancel = function(e) {\n e.preventDefault();\n // In case there is a marker where the new question should be inserted in the text it needs to be removed.\n for (const span of _editor.dom.select('.' + markerClass + '.new')) {\n span.remove();\n }\n _modal.destroy();\n _editor.focus();\n _modal = null;\n};\n\n/**\n * Insert question string into editor content and reset and hide form. If the form contains an error\n * nothing happens.\n *\n * @method _setSubquestion\n * @param {Event} e Event from button click\n * @private\n */\nconst _setSubquestion = function(e) {\n e.preventDefault();\n // Check if there are any errors and if so, fill the error container with the\n // messages and return without going any further and closing the dialogue.\n const errMsg = _form.querySelector('.msg-error');\n const formErrors = _processFormData(true);\n if (formErrors.length > 0) {\n errMsg.innerHTML = '
  • ' + formErrors.join('
  • ') + '
';\n errMsg.classList.remove('hidden');\n return;\n } else {\n errMsg.classList.add('hidden');\n }\n // Build the parser function from the data, that is going to be placed into the editor content.\n let question = '{' + _marks + ':' + _qtype + ':';\n\n // Filter all empty responses\n for (let i = 0; i < _answerdata.length; i++) {\n if (_answerdata[i].raw === '') {\n continue;\n }\n question += _answerdata[i].fraction && !isNaN(_answerdata[i].fraction)\n ? '%' + _answerdata[i].fraction + '%' : _answerdata[i].fraction;\n question += strencode(_answerdata[i].answer);\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n question += ':' + _answerdata[i].tolerance;\n }\n if (_answerdata[i].feedback) {\n question += '#' + strencode(_answerdata[i].feedback);\n }\n if (i < _answerdata.length - 1) {\n question += '~';\n }\n }\n if (question.slice(-1) === '~') {\n question = question.substring(0, question.length - 1);\n }\n question += '}';\n\n _modal.destroy();\n _modal = null;\n _editor.focus();\n if (_selectedOffset > -1) { // We have to replace one of the marker spans (the innerHTML contains the question string).\n _editor.dom.select('.' + markerClass)[_selectedOffset].innerHTML = question;\n } else {\n // Just add the question text with markup.\n _editor.insertContent(markerSpan + question + '');\n }\n};\n\n/**\n * Read the form data, process it and store the result in the internal _answerdata array.\n * Also, if validation is enabled, the fields are checked for invalid values e.g.\n * - answer field is empty (if a correct answer is contained, empty fields are eliminated).\n * - custom_grade field whenin use and does not contain a number.\n * - no field is marked as a correct answer.\n * - tolerance field must be in percentage of min -100 and max 100.\n * Any field with an error is maked and the first field containing an error gets the focus.\n *\n * @method _processFormData\n * @param {boolean} validate\n * @return {Array}\n * @private\n */\nconst _processFormData = function(validate) {\n _answerdata = [];\n let globalErrors = [];\n const answers = _form.querySelectorAll('.' + CSS.ANSWER);\n const feedbacks = _form.querySelectorAll('.' + CSS.FEEDBACK);\n const fractions = _form.querySelectorAll('.' + CSS.FRACTION);\n const customGrades = _form.querySelectorAll('.' + CSS.FRAC_CUSTOM);\n const tolerances = _form.querySelectorAll('.' + CSS.TOLERANCE);\n // Remove any error classes.\n for (let i = 0; i < answers.length; i++) {\n answers.item(i).classList.remove('error');\n customGrades.item(i).classList.remove('error');\n const currentAnswer = {\n raw: answers.item(i).value.trim(),\n answer: answers.item(i).value.trim(),\n id: getUuid(),\n feedback: feedbacks.item(i).value,\n fraction: fractions.item(i).value === selectCustomPercent ? customGrades.item(i).value : fractions.item(i).value,\n fractionOptions: getFractionOptions(fractions.item(i).value),\n tolerance: tolerances.length > 0 ? tolerances.item(i).value : 0,\n isCustomGrade: fractions.item(i).value === selectCustomPercent\n };\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n tolerances.item(i).classList.remove('error');\n // In numeric questions convert answer and tolerance to numeric values (this filters non numeric values).\n currentAnswer.answer = Number(currentAnswer.answer);\n currentAnswer.tolerance = Number(currentAnswer.tolerance);\n }\n _answerdata.push(currentAnswer);\n }\n _marks = _form.querySelector('.' + CSS.MARKS).value;\n\n if (validate) {\n const {hasCorrectAnswer, errors} = _validateAnswers();\n for (let i = 0; i < _answerdata.length; i++) {\n for (const err of _answerdata[i].hasErrors) {\n if (hasCorrectAnswer && (err === 'empty_answer' || err === 'correct_but_empty')) {\n break;\n }\n if (err === 'answer_not_numeric' || err === 'empty_answer' || err === 'correct_but_empty') {\n answers.item(i).classList.add('error');\n } else if (err === 'tolerance_not_numeric') {\n tolerances.item(i).classList.add('error');\n } else if (err === 'error_custom_rate') {\n customGrades.item(i).classList.add('error');\n }\n }\n }\n globalErrors = _translateGlobalErrors(hasCorrectAnswer, errors);\n // If we have errors, we focus the first field that contains an error.\n if (globalErrors.length > 0) {\n _form.querySelector('input.error').focus();\n }\n }\n return globalErrors;\n};\n\n/**\n * Validates the answer array. Checks for each question if the data from the form is\n * incomplete or has other errors. These are flagged accordingly in the array element.\n * The retruned object contains the properties:\n * - hasCorrectAnswer {boolean} is true if there is at least one correct answer.\n * - errors {Array} list of strings that contain an error code that is globaly used for error messages.\n *\n * @return {Array}\n * @private\n */\nconst _validateAnswers = function() {\n let errors = [];\n let hasCorrect = false;\n for (let i = 0; i < _answerdata.length; i++) {\n _answerdata[i].hasErrors = [];\n // Check if we have an empty answer string.\n if (_answerdata[i].raw === '') {\n _answerdata[i].hasErrors.push('empty_answer');\n }\n // When there are numeric questions, check that the answer and tolerance is a valid number.\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n if (isNaN(_answerdata[i].answer) && _answerdata[i].raw !== '') {\n _answerdata[i].hasErrors.push('answer_not_numeric');\n }\n if (isNaN(_answerdata[i].tolerance)) {\n _answerdata[i].hasErrors.push('tolerance_not_numeric');\n }\n }\n // Check the custom grade, that must be a percentage number between -100 and 100.\n if (_answerdata[i].isCustomGrade &&\n (isNaN(_answerdata[i].fraction) || _answerdata[i].fraction < -100 || _answerdata[i].fraction > 100\n || _answerdata[i].fraction.trim() === '')\n ) {\n _answerdata[i].hasErrors.push('error_custom_rate');\n }\n // We found a correct answer, when grade is marked as 100 or \"=\" and the answer is not empty.\n if (_answerdata[i].fraction === '100' || _answerdata[i].fraction === '=') {\n if (_answerdata[i].raw !== '') {\n _answerdata[i].isCorrect = true;\n hasCorrect = true;\n } else {\n _answerdata[i].hasErrors.push('correct_but_empty');\n }\n }\n errors = errors.concat(_answerdata[i].hasErrors);\n }\n\n return {\n hasCorrectAnswer: hasCorrect,\n errors: _combineGlobalErrors(hasCorrect, errors),\n };\n};\n\n/**\n * Translate the errors into a readable string for a list that is used on top of the\n * input fields, to indicate what part of the data is incorrect.\n *\n * @param {Boolean} hasCorrectAnswer\n * @param {Array} errors\n * @return {Array}\n * @private\n */\nconst _translateGlobalErrors = function(hasCorrectAnswer, errors) {\n const errTranslated = [];\n // Translate the error strings into a string that can be displayed in the form.\n const trMsg = {\n emptyanswer: STR.err_empty_answer,\n answernotnumeric: STR.err_not_numeric,\n tolerancenotnumeric: STR.err_not_numeric,\n errorcustomrate: STR.err_custom_rate,\n nonecorrect: STR.err_none_correct,\n };\n for (const err of errors) {\n // If there's at least one correct answer, we filter out all empty answers and therefore do not\n // show the error message.\n if (hasCorrectAnswer && err === 'empty_answer' || err === 'correct_but_empty') {\n continue;\n }\n // Remove underscore (we do this only because of the js linter).\n const key = err.replace(/_/g, '');\n errTranslated.push(trMsg[key]);\n }\n return errTranslated;\n};\n\n/**\n * Combine the error list from the answers to a global list.\n *\n * @param {Boolean} hasCorrectAnswer\n * @param {Array} errors\n * @return {Array}\n * @private\n */\nconst _combineGlobalErrors = function(hasCorrectAnswer, errors) {\n // Unique errors for the global error list.\n const errUnique = errors.filter((value, index, array) => array.indexOf(value) === index);\n // If we have a correct answer, do not show the empty answer error, because empty responses are filtered.\n if (hasCorrectAnswer) {\n const i = errUnique.indexOf('empty_answer');\n if (i > -1) {\n errUnique.splice(i, 1);\n }\n } else if (!errUnique.includes('correct_but_empty')) {\n errUnique.push('none_correct');\n }\n return errUnique;\n};\n\n/**\n * Check whether cursor is in a subquestion and return subquestion text if\n * true.\n *\n * @method resolveSubquestion\n * @return {Mixed} The selected node of with the subquestion if found, false otherwise.\n */\nconst resolveSubquestion = function() {\n let span = _editor.selection.getStart();\n if (!isNull(span.classList) && span.classList.contains(markerClass)) {\n return span;\n }\n _editor.dom.getParents(span, elm => {\n // Are we in a span that encapsulates the cloze question?\n if (!isNull(elm.classList) && elm.classList.contains(markerClass)) {\n return elm;\n }\n return false;\n });\n return false;\n};\n\nexport {\n displayDialogue,\n resolveSubquestion,\n onInit,\n onBeforeGetContent,\n onSubmit,\n};\n"],"names":["isNull","a","strdecode","t","String","replace","strencode","indexOfNode","list","node","i","length","getUuid","crypto","randomUUID","Math","floor","random","toString","getFractionOptions","s","attrSel","isSel","html","STR","incorrect","correct","FRACTIONS","forEach","item","value","indexOf","custom_grade","isCustomGrade","found","markerClass","markerSpan","reQtype","CSS","ANSWER","ANSWERS","ADD","CANCEL","DELETE","FEEDBACK","FRACTION","FRAC_CUSTOM","LEFT","LOWER","RIGHT","MARKS","DUPLICATE","RAISE","SUBMIT","SUMMARY","TOLERANCE","TYPE","TEMPLATE","FORM","M","util","image_url","FOOTER","getQuestionTypes","multichoice","summary_multichoice","selectinline","singleyes","horizontal","vertical","shuffle","multiresponse","multi_vertical","singleno","multi_horizontal","numerical","summary_numerical","shortanswer","summary_shortanswer","caseno","caseyes","_editor","_form","_answerdata","_qtype","_selectedOffset","_marks","_modal","_firstAnswer","ed","_addMarkers","async","key","component","then","args","Array","from","arguments","map","l","catch","getStr","_createModal","cfg","title","templateContext","elementid","id","removeOnClose","large","Modal","create","ModalFactory","subquestion","resolveSubquestion","dom","select","_parseSubquestion","innerHTML","_setDialogueContent","selection","getContent","m","content","newContent","match","pos","substring","level","b","setContent","_removeMarkers","span","setOuterHTML","classList","contains","source_view","onClose","off","on","qtype","nomodalevents","footer","Mustache","render","cancel","btn_cancel","submit","btn_insert","btn_select","contentText","answerdata","name","filter","q","type","marks","types","setBody","setFooter","show","$root","getRoot","get","querySelector","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","ModalEvents","_cancel","save","_choiceHandler","_setSubquestion","getTarget","e","p","target","nodeType","tagName","parentNode","addEventListener","preventDefault","_deleteAnswer","_addAnswer","_lowerAnswer","_raiseAnswer","querySelectorAll","sel","getAttribute","document","getElementById","remove","add","max","blankAnswer","answer","feedback","fraction","fractionOptions","tolerance","x","push","destroy","focus","question","parts","exec","lastIndex","abbr","answers","options","frac","index","closest","_processFormData","splice","min","li","before","nextSibling","after","previousSibling","errMsg","formErrors","join","raw","isNaN","slice","insertContent","validate","globalErrors","feedbacks","fractions","customGrades","tolerances","currentAnswer","trim","Number","hasCorrectAnswer","errors","_validateAnswers","err","hasErrors","_translateGlobalErrors","hasCorrect","isCorrect","concat","_combineGlobalErrors","errTranslated","trMsg","emptyanswer","err_empty_answer","answernotnumeric","err_not_numeric","tolerancenotnumeric","errorcustomrate","err_custom_rate","nonecorrect","err_none_correct","errUnique","array","includes","getStart","getParents","elm"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"ui.min.js","sources":["../src/ui.js"],"sourcesContent":["// This file is part of Moodle - https://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Plugin tiny_cloze for TinyMCE v6 in Moodle.\n *\n * @module tiny_cloze/ui\n * @copyright 2023 MoodleDACH\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ModalEvents from 'core/modal_events';\nimport Modal from 'core/modal';\nimport ModalFactory from 'core/modal_factory';\nimport Mustache from 'core/mustache';\nimport {get_strings as getStrings} from 'core/str';\nimport {component} from './common';\n\n// Helper functions.\nconst isNull = a => a === null || a === undefined;\nconst strdecode = t => String(t).replace(/\\\\(#|\\}|~)/g, '$1');\nconst strencode = t => String(t).replace(/(#|\\}|~)/g, '\\\\$1');\nconst indexOfNode = (list, node) => {\n for (let i = 0; i < list.length; i++) {\n if (list[i] === node) {\n return i;\n }\n }\n return -1;\n};\nconst getUuid = function() {\n if (!isNull(crypto.randomUUID)) {\n return crypto.randomUUID();\n }\n return 'ed-cloze-' + Math.floor(Math.random() * 100000).toString();\n};\n// Grade Selector value when custom percentage is selected.\nconst selectCustomPercent = '__custom__';\n// This is a specific helper function to return the options html for the fraction select element.\nconst getFractionOptions = s => {\n const attrSel = ' selected=\"selected\"';\n let isSel = s === '=' ? attrSel : '';\n let html = ``;\n FRACTIONS.forEach(item => {\n isSel = item.value.toString() === s ? attrSel : '';\n html += ``;\n });\n isSel = s !== '' && html.indexOf(attrSel) === -1 ? attrSel : '';\n html += ``;\n return html;\n};\n// Check if the value is a custom grade value (in order to show the input field).\nconst isCustomGrade = s => {\n if (s === '=' || s === '') {\n return false;\n }\n let found = false;\n FRACTIONS.forEach(item => {\n if (item.value.toString() === s) {\n found = true;\n }\n });\n return !found;\n};\n// Marker class and the whole span element that is used to encapsulate the cloze question text.\nconst markerClass = 'cloze-question-marker';\nconst markerSpan = '';\n// Regex to recognize the question string in the text e.g. {1:NUMERICAL:...} or {:MULTICHOICE:...}\n// eslint-disable-next-line max-len\nconst reQtype = /\\{([0-9]*):(MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?):(.*?)\\}/g;\n\n// CSS classes that are used in the modal dialogue.\nconst CSS = {\n ANSWER: 'tiny_cloze_answer',\n ANSWERS: 'tiny_cloze_answers',\n ADD: 'tiny_cloze_add',\n CANCEL: 'tiny_cloze_cancel',\n DELETE: 'tiny_cloze_delete',\n FEEDBACK: 'tiny_cloze_feedback',\n FRACTION: 'tiny_cloze_fraction',\n FRAC_CUSTOM: 'tiny_cloze_frac_custom',\n LEFT: 'tiny_cloze_col0',\n LOWER: 'tiny_cloze_down',\n RIGHT: 'tiny_cloze_col1',\n MARKS: 'tiny_cloze_marks',\n DUPLICATE: 'tiny_cloze_duplicate',\n RAISE: 'tiny_cloze_up',\n SUBMIT: 'tiny_cloze_submit',\n SUMMARY: 'tiny_cloze_summary',\n TOLERANCE: 'tiny_cloze_tolerance',\n TYPE: 'tiny_cloze_qtype'\n};\nconst TEMPLATE = {\n FORM: '
' +\n '

{{name}} ({{qtype}})

' +\n '
' +\n '
' +\n '
' +\n '' +\n '' +\n '' +\n '\"{{STR.addmoreanswerblanks}}\"' +\n '
' +\n '
' +\n '
' +\n '
' +\n '
    {{#answerdata}}' +\n '
  1. ' +\n '
    ' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '' +\n '\"{{STR.addmoreanswerblanks}}\"' +\n '' +\n '\"{{STR.delete}}\"' +\n '' +\n '\"{{STR.up}}\"' +\n '' +\n '\"{{STR.down}}\"' +\n '
    ' +\n '
    ' +\n '{{#numerical}}' +\n '
    ' +\n '
    ' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '{{/numerical}}' +\n '
    ' +\n '
    ' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '' +\n '' +\n '
    ' +\n '
    ' +\n '%' +\n '
    ' +\n '
  2. ' +\n '{{/answerdata}}
' +\n '
' +\n '
',\n TYPE: '
' +\n '

{{STR.chooseqtypetoadd}}

' +\n '
' +\n '
' +\n '{{#types}}' +\n '
' +\n '' +\n '
' +\n '{{/types}}
' +\n '
',\n FOOTER: '' +\n '',\n};\n const FRACTIONS = [\n {value: 100},\n {value: 50},\n {value: 0},\n ];\n\n// Language strings used in the modal dialogue.\nconst STR = {};\nconst getStr = async() => {\n getStrings([\n {key: 'answer', component: 'question'},\n {key: 'chooseqtypetoadd', component: 'question'},\n {key: 'defaultmark', component: 'question'},\n {key: 'feedback', component: 'question'},\n {key: 'correct', component: 'question'},\n {key: 'incorrect', component: 'question'},\n {key: 'addmoreanswerblanks', component: 'qtype_calculated'},\n {key: 'delete', component: 'core'},\n {key: 'up', component: 'core'},\n {key: 'down', component: 'core'},\n {key: 'tolerance', component: 'qtype_calculated'},\n {key: 'grade', component: 'grades'},\n {key: 'caseno', component: 'mod_quiz'},\n {key: 'caseyes', component: 'mod_quiz'},\n {key: 'answersingleno', component: 'qtype_multichoice'},\n {key: 'answersingleyes', component: 'qtype_multichoice'},\n {key: 'layoutselectinline', component: 'qtype_multianswer'},\n {key: 'layouthorizontal', component: 'qtype_multianswer'},\n {key: 'layoutvertical', component: 'qtype_multianswer'},\n {key: 'shufflewithin', component: 'mod_quiz'},\n {key: 'layoutmultiple_horizontal', component: 'qtype_multianswer'},\n {key: 'layoutmultiple_vertical', component: 'qtype_multianswer'},\n {key: 'pluginnamesummary', component: 'qtype_multichoice'},\n {key: 'pluginnamesummary', component: 'qtype_shortanswer'},\n {key: 'pluginnamesummary', component: 'qtype_numerical'},\n {key: 'multichoice', component},\n {key: 'multiresponse', component},\n {key: 'numerical', component: 'mod_quiz'},\n {key: 'shortanswer', component: 'mod_quiz'},\n {key: 'cancel', component: 'core'},\n {key: 'select', component},\n {key: 'insert', component},\n {key: 'pluginname', component},\n {key: 'customgrade', component},\n {key: 'err_custom_rate', component},\n {key: 'err_empty_answer', component},\n {key: 'err_none_correct', component},\n {key: 'err_not_numeric', component},\n ]).then(function() {\n const args = Array.from(arguments);\n [\n 'answer',\n 'chooseqtypetoadd',\n 'defaultmark',\n 'feedback',\n 'correct',\n 'incorrect',\n 'addmoreanswerblanks',\n 'delete',\n 'up',\n 'down',\n 'tolerance',\n 'grade',\n 'caseno',\n 'caseyes',\n 'singleno',\n 'singleyes',\n 'selectinline',\n 'horizontal',\n 'vertical',\n 'shuffle',\n 'multi_horizontal',\n 'multi_vertical',\n 'summary_multichoice',\n 'summary_shortanswer',\n 'summary_numerical',\n 'multichoice',\n 'multiresponse',\n 'numerical',\n 'shortanswer',\n 'btn_cancel',\n 'btn_select',\n 'btn_insert',\n 'title',\n 'custom_grade',\n 'err_custom_rate',\n 'err_empty_answer',\n 'err_none_correct',\n 'err_not_numeric',\n ].map((l, i) => {\n STR[l] = args[0][i];\n return ''; // Make the linter happy.\n });\n return ''; // Make the linter happy.\n }).catch(() => {\n return '';\n });\n};\nconst getQuestionTypes = function() {\n return [\n {\n 'type': 'MULTICHOICE',\n 'abbr': ['MC'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.selectinline, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_H',\n 'abbr': ['MCH'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.horizontal, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_V',\n 'abbr': ['MCV'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.vertical, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_S',\n 'abbr': ['MCS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.selectinline, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_HS',\n 'abbr': ['MCHS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.horizontal, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_VS',\n 'abbr': ['MCVS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.vertical, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTIRESPONSE',\n 'abbr': ['MR'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_vertical, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_H',\n 'abbr': ['MRH'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_horizontal, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_S',\n 'abbr': ['MRS'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_vertical, STR.shuffle, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_HS',\n 'abbr': ['MRHS'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_horizontal, STR.shuffle, STR.singleno],\n },\n {\n 'type': 'NUMERICAL',\n 'abbr': ['NM'],\n 'name': STR.numerical,\n 'summary': STR.summary_numerical,\n },\n {\n 'type': 'SHORTANSWER',\n 'abbr': ['SA', 'MW'],\n 'name': STR.shortanswer,\n 'summary': STR.summary_shortanswer,\n 'options': [STR.caseno],\n },\n {\n 'type': 'SHORTANSWER_C',\n 'abbr': ['SAC', 'MWC'],\n 'name': STR.shortanswer,\n 'summary': STR.summary_shortanswer,\n 'options': [STR.caseyes],\n },\n ];\n};\n\n/**\n * The editor instance that is injected via the onInit() function.\n *\n * @type {tinymce.Editor}\n * @private\n */\nlet _editor = null;\n\n/**\n * A reference to the currently open form.\n *\n * @param _form\n * @type {Node}\n * @private\n */\nlet _form = null;\n\n/**\n * An array containing the current answers options\n *\n * @param _answerdata\n * @type {Array}\n * @private\n */\nlet _answerdata = [];\n\n/**\n * The sub question type to be edited\n *\n * @param _qtype\n * @type {string|null}\n * @private\n */\nlet _qtype = null;\n\n/**\n * Remember the pos of the selected node.\n * @type {number}\n * @private\n */\nlet _selectedOffset = -1;\n\n/**\n * The maximum marks for the sub question\n *\n * @param _marks\n * @type {Integer}\n * @private\n */\nlet _marks = 1;\n\n/**\n * The modal dialogue to be displayed when designing the cloze question types.\n * @type {Modal|null}\n */\nlet _modal = null;\n\n/**\n * If its a normal selection of text, use it for the first answer field.\n * @type {string|null}\n */\nlet _firstAnswer = null;\n\n/**\n * Inject the editor instance and add markers to the cloze question texts.\n * @param {tinymce.Editor} ed\n */\nconst onInit = function(ed) {\n _editor = ed; // The current editor instance.\n // Add the marker spans.\n _addMarkers();\n // And get the language strings.\n getStr();\n};\n\n/**\n * Create the modal.\n * @return {Promise}\n * @private\n */\nconst _createModal = async function() {\n // Create the modal dialogue. Depending on whether we have a selected node or not, the content is different.\n const cfg = {\n title: STR.title,\n templateContext: {\n elementid: _editor.id\n },\n removeOnClose: true,\n large: true,\n };\n if (typeof Modal.create === 'function') {\n _modal = await Modal.create(cfg);\n } else {\n _modal = await ModalFactory.create(cfg);\n }\n};\n\n/**\n * Display modal dialogue to edit a cloze question. Either a form is displayed to edit subquestion or a list\n * of possible questions is show.\n *\n * @method displayDialogue\n * @private\n */\nconst displayDialogue = async function() {\n await _createModal();\n\n // Resolve whether cursor is in a subquestion.\n var subquestion = resolveSubquestion();\n if (subquestion) {\n _firstAnswer = null;\n // Subquestion found, remember which node of the marker nodes is selected.\n _selectedOffset = indexOfNode(_editor.dom.select('.' + markerClass), subquestion);\n _parseSubquestion(subquestion.innerHTML);\n _setDialogueContent(_qtype);\n } else {\n // No subquestion found, no offset to remember.\n _firstAnswer = _editor.selection.getContent();\n _selectedOffset = -1;\n _setDialogueContent();\n }\n};\n\n/**\n * Search for cloze questions based on a regular expression. All the matching snippets at least contain the cloze\n * question definition. Although Moodle does not support encapsulated other functions within curly brackets, we\n * still try to find the correct closing bracket. The so extracted cloze question is surrounded by a marker span\n * element, that contains attributes so that the content inside the span cannot be modified by the editor (in the\n * textarea). Also, this makes it a lot easier to select the question, edit it in the dialogue and replace the result\n * in the existing text area.\n */\nconst _addMarkers = function() {\n\n let content = _editor.getContent();\n let newContent = '';\n\n // Check if there is already a marker span. In this case we do not have to do anything.\n if (content.indexOf(markerClass) !== -1) {\n return;\n }\n\n let m;\n do {\n m = content.match(reQtype);\n if (!m) { // No match of a cloze question, then we are done.\n newContent += content;\n break;\n }\n // Copy the current match to the new string preceded with the .\n const pos = content.indexOf(m[0]);\n newContent += content.substring(0, pos) + markerSpan + content.substring(pos, pos + m[0].length);\n content = content.substring(pos + m[0].length);\n\n // Count the { in the string, should be just one (the very first one at position 0).\n let level = (m[0].match(/\\{/g) || []).length;\n if (level === 1) {\n // If that's the case, we close the span and the cloze question text is the innerHTML of that marker span.\n newContent += '';\n continue; // Look for the next matching cloze question.\n }\n // If there are more { than } in the string, then we did not find the corresponding } that belongs to the cloze string.\n while (level > 1) {\n const a = content.indexOf('{');\n const b = content.indexOf('}');\n if (a > -1 && b > -1 && a < b) { // The { is before another } so remember to find as many } until we back at level 1.\n level++;\n newContent = content.substring(0, a);\n content = content.substring(a + 1);\n } else if (b > -1) { // We found a closing } to a previously {.\n newContent = content.substring(0, b);\n content = content.substring(b + 1);\n level--;\n } else {\n level = 1; // Should not happen, just to stop the endless loop.\n }\n }\n newContent += '
';\n } while (m);\n _editor.setContent(newContent);\n};\n\n/**\n * Look for the marker span elements around a cloze question and remove that span. Also, the marker for a new\n * node to be inserted would be removed here as well.\n */\nconst _removeMarkers = function() {\n for (const span of _editor.dom.select('span.' + markerClass)) {\n _editor.dom.setOuterHTML(span, span.classList.contains('new') ? '' : span.innerHTML);\n }\n};\n\n/**\n * When the source code view dialogue is show, we must remove the spans around the cloze question strings\n * from the editor content and add them again when the dialogue is closed.\n * Since this event is also triggered when the editor data is saved, we use this function to remove the\n * highlighting content at that time.\n * @param {object} content\n */\nconst onBeforeGetContent = function(content) {\n if (!isNull(content.source_view) && content.source_view === true) {\n // If the user clicks on 'Cancel' or the close button on the html\n // source code dialog view, make sure we re-add the visual styling.\n var onClose = function() {\n _editor.off('close', onClose);\n _addMarkers();\n };\n _editor.on('CloseWindow', () => {\n onClose();\n });\n // Remove markers only if modal is not called, otherwise we will lose our new question marker.\n if (!_modal) {\n _removeMarkers();\n }\n }\n};\n\n/**\n * Fires when the form containing the editor is submitted.\n */\nconst onSubmit = function() {\n _removeMarkers();\n};\n\n/**\n * Set the dialogue content for the tool, attaching any required events. Either the modal dialogue displays\n * a list of the question types for the form for a particular question to edit. The set content is also\n * called when the form has changed (up or down move, deletion and adding a response). We must be aware of that\n * an event to the dialogue buttons must be attached once only. Therefore, when the form content is modified, only\n * the form events for the answers are set again, the general events are nor (nomodalevents is true then).\n *\n * @method _setDialogueContent\n * @param {String} qtype The question type to be used\n * @param {boolean} nomodalevents Optional do not attach events.\n * @private\n */\nconst _setDialogueContent = function(qtype, nomodalevents) {\n const footer = Mustache.render(TEMPLATE.FOOTER, {\n cancel: STR.btn_cancel,\n submit: !qtype ? STR.btn_select : STR.btn_insert,\n });\n let contentText;\n if (!qtype) {\n contentText = Mustache.render(TEMPLATE.TYPE, {\n CSS: CSS,\n STR: STR,\n qtype: _qtype,\n types: getQuestionTypes()\n });\n } else {\n contentText = Mustache.render(TEMPLATE.FORM, {\n CSS: CSS,\n STR: STR,\n answerdata: _answerdata,\n elementid: getUuid(),\n qtype: _qtype,\n name: getQuestionTypes().filter(q => _qtype === q.type)[0].name,\n marks: _marks,\n numerical: (_qtype === 'NUMERICAL' || _qtype === 'NM')\n });\n }\n _modal.setBody(contentText);\n _modal.setFooter(footer);\n _modal.show();\n const $root = _modal.getRoot();\n _form = $root.get(0).querySelector('form');\n _toggleDeleteIcon();\n\n if (!nomodalevents) {\n _modal.registerEventListeners();\n _modal.registerCloseOnSave();\n _modal.registerCloseOnCancel();\n $root.on(ModalEvents.cancel, _cancel);\n\n if (!qtype) { // For the question list we need the choice handler only, and we are done.\n $root.on(ModalEvents.save, _choiceHandler);\n return;\n } // Handler to add the question string to the editor content.\n $root.on(ModalEvents.save, _setSubquestion);\n }\n // The form needs events for the icons to move up/down, add or delete a response.\n const getTarget = e => {\n let p = e.target;\n while (!isNull(p) && p.nodeType === 1 && p.tagName !== 'A') {\n p = p.parentNode;\n }\n if (isNull(p.classList)) {\n return null;\n }\n return p;\n };\n\n _form.addEventListener('click', e => {\n const p = getTarget(e);\n if (isNull(p)) {\n return;\n }\n if (p.classList.contains(CSS.DELETE)) {\n e.preventDefault();\n _deleteAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.ADD)) {\n e.preventDefault();\n _addAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.LOWER)) {\n e.preventDefault();\n _lowerAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.RAISE)) {\n e.preventDefault();\n _raiseAnswer(p);\n }\n });\n _form.addEventListener('keyup', e => {\n const p = getTarget(e);\n if (isNull(p)) {\n return;\n }\n if (p.classList.contains(CSS.ANSWER) || p.classList.contains(CSS.FEEDBACK)) {\n e.preventDefault();\n _addAnswer(p);\n }\n });\n _form.querySelectorAll('.' + CSS.FRACTION).forEach((sel) => {\n sel.addEventListener('change', e => {\n const id = e.target.getAttribute('id');\n if (e.target.value === selectCustomPercent) {\n document.getElementById(id + '_custom').parentNode.classList.remove('hidden');\n } else {\n document.getElementById(id + '_custom').parentNode.classList.add('hidden');\n }\n });\n });\n};\n\n/**\n * If there is one answer field, hide the delete icon. Otherwise show them\n * all to allow deletion of any answer.\n *\n * @private\n */\nconst _toggleDeleteIcon = function() {\n const deleteIcons =_form.querySelectorAll('.' + CSS.DELETE);\n if (deleteIcons.length === 1) {\n deleteIcons[0].classList.add('hidden');\n return;\n }\n for (let i = 0; i < deleteIcons.length; i++) {\n deleteIcons[i].classList.remove('hidden');\n }\n};\n\n/**\n * Handle question choice.\n *\n * @method _choiceHandler\n * @private\n * @param {Event} e Event from button click in chooser\n */\nconst _choiceHandler = function(e) {\n e.preventDefault();\n let qtype = _form.querySelector('input[name=qtype]:checked');\n if (qtype) {\n _qtype = qtype.value;\n }\n // For numerical and short answer questions we offer one response field only. All other\n // question types have three empty response fields.\n const max = (_qtype.indexOf('SHORTANSWER') !== -1 || _qtype === 'NUMERICAL') ? 1 : 3;\n const blankAnswer = {\n id: getUuid(),\n answer: '',\n feedback: '',\n fraction: 100,\n fractionOptions: getFractionOptions(''),\n tolerance: 0,\n isCustomGrade: false,\n };\n _answerdata = [];\n for (let x = 0; x < max; x++) {\n _answerdata.push({...blankAnswer, id: getUuid()});\n }\n // The first response field gets the default grade correct.\n _answerdata[0].fractionOptions = getFractionOptions('=');\n // In case the user seleced some text, this is used as the first answer.\n if (_firstAnswer) {\n _answerdata[0].answer = _firstAnswer;\n }\n _modal.destroy();\n // Our choice is stored in _qtype. We need to create the modal dialogue with the form now.\n _createModal().then(() => {\n _setDialogueContent(_qtype);\n _form.querySelector('.' + CSS.ANSWER).focus();\n return ''; // Make the linter happy.\n }).catch(() => {\n return '';\n });\n};\n\n/**\n * Parse question and set properties found.\n *\n * @method _parseSubquestion\n * @private\n * @param {String} question The question string\n */\nconst _parseSubquestion = function(question) {\n _answerdata = []; // Flush answers to have an empty dialogue if something goes wrong parsing the question string.\n const parts = reQtype.exec(question);\n reQtype.lastIndex = 0; // Reset lastIndex so that the next match starts from the beginning of the question string.\n if (!parts) {\n return;\n }\n _marks = parts[1];\n _qtype = parts[2];\n // Convert the short notation to the long form e.g. SA to SHORTANSWER.\n if (_qtype.length < 5) {\n getQuestionTypes().forEach(l => {\n for (const a of l.abbr) {\n if (a === _qtype) {\n _qtype = l.type;\n return;\n }\n }\n });\n }\n const answers = parts[7].match(/(\\\\.|[^~])*/g);\n if (!answers) {\n return;\n }\n answers.forEach(function(answer) {\n const options = /^(%(-?[.0-9]+)%|(=?))((\\\\.|[^#])*)#?(.*)/.exec(answer);\n if (options && options[4]) {\n let frac = '';\n if (options[3]) {\n frac = options[3] === '=' ? '=' : 100;\n } else if (options[2]) {\n frac = options[2];\n }\n if (_qtype === 'NUMERICAL' || _qtype === 'NM') {\n const tolerance = /^([^:]*):?(.*)/.exec(options[4])[2] || 0;\n _answerdata.push({\n id: getUuid(),\n answer: strdecode(options[4].replace(/:.*/, '')),\n feedback: strdecode(options[6]),\n tolerance: tolerance,\n fraction: frac,\n fractionOptions: getFractionOptions(frac),\n isCustomGrade: isCustomGrade(frac),\n });\n return;\n }\n _answerdata.push({\n answer: strdecode(options[4]),\n id: getUuid(),\n feedback: strdecode(options[6]),\n fraction: frac,\n fractionOptions: getFractionOptions(frac),\n isCustomGrade: isCustomGrade(frac),\n });\n }\n });\n};\n\n/**\n * Insert a new set of answer blanks below the button.\n *\n * @method _addAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _addAnswer = function(a) {\n let index = indexOfNode(_form.querySelectorAll('.' + CSS.ADD), a);\n if (index === -1) {\n index = 0;\n }\n let fraction = '';\n let answer = '';\n let feedback = '';\n let tolerance = 0;\n if (a.closest('li')) {\n fraction = a.closest('li').querySelector('.' + CSS.FRACTION).value;\n if (fraction === selectCustomPercent) {\n fraction = a.closest('li').querySelector('.' + CSS.FRAC_CUSTOM).value;\n }\n answer = a.closest('li').querySelector('.' + CSS.ANSWER).value;\n feedback = a.closest('li').querySelector('.' + CSS.FEEDBACK).value;\n if (a.closest('li').querySelector('.' + CSS.TOLERANCE)) {\n tolerance = a.closest('li').querySelector('.' + CSS.TOLERANCE).value;\n }\n }\n _processFormData();\n _answerdata.splice(index, 0, {\n id: getUuid(),\n answer: answer,\n feedback: feedback,\n fraction: fraction,\n fractionOptions: getFractionOptions(fraction),\n tolerance: tolerance,\n isCustomGrade: isCustomGrade(fraction)\n });\n _setDialogueContent(_qtype, true);\n _toggleDeleteIcon();\n _form.querySelectorAll('.' + CSS.ANSWER).item(index).focus();\n};\n\n/**\n * Delete set of answer next to the button.\n *\n * @method _deleteAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _deleteAnswer = function(a) {\n let index = indexOfNode(_form.querySelectorAll('.' + CSS.DELETE), a);\n if (index === -1) {\n index = indexOfNode(_form.querySelectorAll('li'), a.closest('li'));\n }\n _processFormData();\n _answerdata.splice(index, 1);\n _setDialogueContent(_qtype, true);\n const answers = _form.querySelectorAll('.' + CSS.ANSWER);\n index = Math.min(index, answers.length - 1);\n answers.item(index).focus();\n _toggleDeleteIcon();\n};\n\n/**\n * Lower answer option\n *\n * @method _lowerAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _lowerAnswer = function(a) {\n const li = a.closest('li');\n li.before(li.nextSibling);\n li.querySelector('.' + CSS.ANSWER).focus();\n};\n\n/**\n * Raise answer option\n *\n * @method _raiseAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _raiseAnswer = function(a) {\n const li = a.closest('li');\n li.after(li.previousSibling);\n li.querySelector('.' + CSS.ANSWER).focus();\n};\n\n/**\n * Reset and hide form.\n *\n * @method _cancel\n * @param {Event} e Event from button click\n * @private\n */\nconst _cancel = function(e) {\n e.preventDefault();\n // In case there is a marker where the new question should be inserted in the text it needs to be removed.\n for (const span of _editor.dom.select('.' + markerClass + '.new')) {\n span.remove();\n }\n _modal.destroy();\n _editor.focus();\n _modal = null;\n};\n\n/**\n * Insert question string into editor content and reset and hide form. If the form contains an error\n * nothing happens.\n *\n * @method _setSubquestion\n * @param {Event} e Event from button click\n * @private\n */\nconst _setSubquestion = function(e) {\n e.preventDefault();\n // Check if there are any errors and if so, fill the error container with the\n // messages and return without going any further and closing the dialogue.\n const errMsg = _form.querySelector('.msg-error');\n const formErrors = _processFormData(true);\n if (formErrors.length > 0) {\n errMsg.innerHTML = '
  • ' + formErrors.join('
  • ') + '
';\n errMsg.classList.remove('hidden');\n return;\n } else {\n errMsg.classList.add('hidden');\n }\n // Build the parser function from the data, that is going to be placed into the editor content.\n let question = '{' + _marks + ':' + _qtype + ':';\n\n // Filter all empty responses\n for (let i = 0; i < _answerdata.length; i++) {\n if (_answerdata[i].raw === '') {\n continue;\n }\n question += _answerdata[i].fraction && !isNaN(_answerdata[i].fraction)\n ? '%' + _answerdata[i].fraction + '%' : _answerdata[i].fraction;\n question += strencode(_answerdata[i].answer);\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n question += ':' + _answerdata[i].tolerance;\n }\n if (_answerdata[i].feedback) {\n question += '#' + strencode(_answerdata[i].feedback);\n }\n if (i < _answerdata.length - 1) {\n question += '~';\n }\n }\n if (question.slice(-1) === '~') {\n question = question.substring(0, question.length - 1);\n }\n question += '}';\n\n _modal.destroy();\n _modal = null;\n _editor.focus();\n if (_selectedOffset > -1) { // We have to replace one of the marker spans (the innerHTML contains the question string).\n _editor.dom.select('.' + markerClass)[_selectedOffset].innerHTML = question;\n } else {\n // Just add the question text with markup.\n _editor.insertContent(markerSpan + question + '');\n }\n};\n\n/**\n * Read the form data, process it and store the result in the internal _answerdata array.\n * Also, if validation is enabled, the fields are checked for invalid values e.g.\n * - answer field is empty (if a correct answer is contained, empty fields are eliminated).\n * - custom_grade field whenin use and does not contain a number.\n * - no field is marked as a correct answer.\n * - tolerance field must be in percentage of min -100 and max 100.\n * Any field with an error is maked and the first field containing an error gets the focus.\n *\n * @method _processFormData\n * @param {boolean} validate\n * @return {Array}\n * @private\n */\nconst _processFormData = function(validate) {\n _answerdata = [];\n let globalErrors = [];\n const answers = _form.querySelectorAll('.' + CSS.ANSWER);\n const feedbacks = _form.querySelectorAll('.' + CSS.FEEDBACK);\n const fractions = _form.querySelectorAll('.' + CSS.FRACTION);\n const customGrades = _form.querySelectorAll('.' + CSS.FRAC_CUSTOM);\n const tolerances = _form.querySelectorAll('.' + CSS.TOLERANCE);\n // Remove any error classes.\n for (let i = 0; i < answers.length; i++) {\n answers.item(i).classList.remove('error');\n customGrades.item(i).classList.remove('error');\n const currentAnswer = {\n raw: answers.item(i).value.trim(),\n answer: answers.item(i).value.trim(),\n id: getUuid(),\n feedback: feedbacks.item(i).value,\n fraction: fractions.item(i).value === selectCustomPercent ? customGrades.item(i).value : fractions.item(i).value,\n fractionOptions: getFractionOptions(fractions.item(i).value),\n tolerance: tolerances.length > 0 ? tolerances.item(i).value : 0,\n isCustomGrade: fractions.item(i).value === selectCustomPercent\n };\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n tolerances.item(i).classList.remove('error');\n // In numeric questions convert answer and tolerance to numeric values (this filters non numeric values).\n currentAnswer.answer = Number(currentAnswer.answer);\n currentAnswer.tolerance = Number(currentAnswer.tolerance);\n }\n _answerdata.push(currentAnswer);\n }\n _marks = _form.querySelector('.' + CSS.MARKS).value;\n\n if (validate) {\n const {hasCorrectAnswer, errors} = _validateAnswers();\n for (let i = 0; i < _answerdata.length; i++) {\n for (const err of _answerdata[i].hasErrors) {\n if (hasCorrectAnswer && (err === 'empty_answer' || err === 'correct_but_empty')) {\n break;\n }\n if (err === 'answer_not_numeric' || err === 'empty_answer' || err === 'correct_but_empty') {\n answers.item(i).classList.add('error');\n } else if (err === 'tolerance_not_numeric') {\n tolerances.item(i).classList.add('error');\n } else if (err === 'error_custom_rate') {\n customGrades.item(i).classList.add('error');\n }\n }\n }\n globalErrors = _translateGlobalErrors(hasCorrectAnswer, errors);\n // If we have errors, we focus the first field that contains an error.\n if (globalErrors.length > 0) {\n _form.querySelector('input.error').focus();\n }\n }\n return globalErrors;\n};\n\n/**\n * Validates the answer array. Checks for each question if the data from the form is\n * incomplete or has other errors. These are flagged accordingly in the array element.\n * The retruned object contains the properties:\n * - hasCorrectAnswer {boolean} is true if there is at least one correct answer.\n * - errors {Array} list of strings that contain an error code that is globaly used for error messages.\n *\n * @return {Array}\n * @private\n */\nconst _validateAnswers = function() {\n let errors = [];\n let hasCorrect = false;\n for (let i = 0; i < _answerdata.length; i++) {\n _answerdata[i].hasErrors = [];\n // Check if we have an empty answer string.\n if (_answerdata[i].raw === '') {\n _answerdata[i].hasErrors.push('empty_answer');\n }\n // When there are numeric questions, check that the answer and tolerance is a valid number.\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n if (isNaN(_answerdata[i].answer) && _answerdata[i].raw !== '') {\n _answerdata[i].hasErrors.push('answer_not_numeric');\n }\n if (isNaN(_answerdata[i].tolerance)) {\n _answerdata[i].hasErrors.push('tolerance_not_numeric');\n }\n }\n // Check the custom grade, that must be a percentage number between -100 and 100.\n if (_answerdata[i].isCustomGrade &&\n (isNaN(_answerdata[i].fraction) || _answerdata[i].fraction < -100 || _answerdata[i].fraction > 100\n || _answerdata[i].fraction.trim() === '')\n ) {\n _answerdata[i].hasErrors.push('error_custom_rate');\n }\n // We found a correct answer, when grade is marked as 100 or \"=\" and the answer is not empty.\n if (_answerdata[i].fraction === '100' || _answerdata[i].fraction === '=') {\n if (_answerdata[i].raw !== '') {\n _answerdata[i].isCorrect = true;\n hasCorrect = true;\n } else {\n _answerdata[i].hasErrors.push('correct_but_empty');\n }\n }\n errors = errors.concat(_answerdata[i].hasErrors);\n }\n\n return {\n hasCorrectAnswer: hasCorrect,\n errors: _combineGlobalErrors(hasCorrect, errors),\n };\n};\n\n/**\n * Translate the errors into a readable string for a list that is used on top of the\n * input fields, to indicate what part of the data is incorrect.\n *\n * @param {Boolean} hasCorrectAnswer\n * @param {Array} errors\n * @return {Array}\n * @private\n */\nconst _translateGlobalErrors = function(hasCorrectAnswer, errors) {\n const errTranslated = [];\n // Translate the error strings into a string that can be displayed in the form.\n const trMsg = {\n emptyanswer: STR.err_empty_answer,\n answernotnumeric: STR.err_not_numeric,\n tolerancenotnumeric: STR.err_not_numeric,\n errorcustomrate: STR.err_custom_rate,\n nonecorrect: STR.err_none_correct,\n };\n for (const err of errors) {\n // If there's at least one correct answer, we filter out all empty answers and therefore do not\n // show the error message.\n if (hasCorrectAnswer && err === 'empty_answer' || err === 'correct_but_empty') {\n continue;\n }\n // Remove underscore (we do this only because of the js linter).\n const key = err.replace(/_/g, '');\n errTranslated.push(trMsg[key]);\n }\n return errTranslated;\n};\n\n/**\n * Combine the error list from the answers to a global list.\n *\n * @param {Boolean} hasCorrectAnswer\n * @param {Array} errors\n * @return {Array}\n * @private\n */\nconst _combineGlobalErrors = function(hasCorrectAnswer, errors) {\n // Unique errors for the global error list.\n const errUnique = errors.filter((value, index, array) => array.indexOf(value) === index);\n // If we have a correct answer, do not show the empty answer error, because empty responses are filtered.\n if (hasCorrectAnswer) {\n const i = errUnique.indexOf('empty_answer');\n if (i > -1) {\n errUnique.splice(i, 1);\n }\n } else if (!errUnique.includes('correct_but_empty')) {\n errUnique.push('none_correct');\n }\n return errUnique;\n};\n\n/**\n * Check whether cursor is in a subquestion and return subquestion text if\n * true.\n *\n * @method resolveSubquestion\n * @return {Mixed} The selected node of with the subquestion if found, false otherwise.\n */\nconst resolveSubquestion = function() {\n let span = _editor.selection.getStart();\n if (!isNull(span.classList) && span.classList.contains(markerClass)) {\n return span;\n }\n _editor.dom.getParents(span, elm => {\n // Are we in a span that encapsulates the cloze question?\n if (!isNull(elm.classList) && elm.classList.contains(markerClass)) {\n return elm;\n }\n return false;\n });\n return false;\n};\n\nexport {\n displayDialogue,\n resolveSubquestion,\n onInit,\n onBeforeGetContent,\n onSubmit,\n};\n"],"names":["isNull","a","strdecode","t","String","replace","strencode","indexOfNode","list","node","i","length","getUuid","crypto","randomUUID","Math","floor","random","toString","getFractionOptions","s","attrSel","isSel","html","STR","incorrect","correct","FRACTIONS","forEach","item","value","indexOf","custom_grade","isCustomGrade","found","markerClass","markerSpan","reQtype","CSS","ANSWER","ANSWERS","ADD","CANCEL","DELETE","FEEDBACK","FRACTION","FRAC_CUSTOM","LEFT","LOWER","RIGHT","MARKS","DUPLICATE","RAISE","SUBMIT","SUMMARY","TOLERANCE","TYPE","TEMPLATE","FORM","M","util","image_url","FOOTER","getQuestionTypes","multichoice","summary_multichoice","selectinline","singleyes","horizontal","vertical","shuffle","multiresponse","multi_vertical","singleno","multi_horizontal","numerical","summary_numerical","shortanswer","summary_shortanswer","caseno","caseyes","_editor","_form","_answerdata","_qtype","_selectedOffset","_marks","_modal","_firstAnswer","ed","_addMarkers","async","key","component","then","args","Array","from","arguments","map","l","catch","getStr","_createModal","cfg","title","templateContext","elementid","id","removeOnClose","large","Modal","create","ModalFactory","subquestion","resolveSubquestion","dom","select","_parseSubquestion","innerHTML","_setDialogueContent","selection","getContent","m","content","newContent","match","pos","substring","level","b","setContent","_removeMarkers","span","setOuterHTML","classList","contains","source_view","onClose","off","on","qtype","nomodalevents","footer","Mustache","render","cancel","btn_cancel","submit","btn_insert","btn_select","contentText","answerdata","name","filter","q","type","marks","types","setBody","setFooter","show","$root","getRoot","get","querySelector","_toggleDeleteIcon","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","ModalEvents","_cancel","save","_choiceHandler","_setSubquestion","getTarget","e","p","target","nodeType","tagName","parentNode","addEventListener","preventDefault","_deleteAnswer","_addAnswer","_lowerAnswer","_raiseAnswer","querySelectorAll","sel","getAttribute","document","getElementById","remove","add","deleteIcons","max","blankAnswer","answer","feedback","fraction","fractionOptions","tolerance","x","push","destroy","focus","question","parts","exec","lastIndex","abbr","answers","options","frac","index","closest","_processFormData","splice","min","li","before","nextSibling","after","previousSibling","errMsg","formErrors","join","raw","isNaN","slice","insertContent","validate","globalErrors","feedbacks","fractions","customGrades","tolerances","currentAnswer","trim","Number","hasCorrectAnswer","errors","_validateAnswers","err","hasErrors","_translateGlobalErrors","hasCorrect","isCorrect","concat","_combineGlobalErrors","errTranslated","trMsg","emptyanswer","err_empty_answer","answernotnumeric","err_not_numeric","tolerancenotnumeric","errorcustomrate","err_custom_rate","nonecorrect","err_none_correct","errUnique","array","includes","getStart","getParents","elm"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/ui.js b/amd/src/ui.js index 1cba238..ff17618 100644 --- a/amd/src/ui.js +++ b/amd/src/ui.js @@ -647,6 +647,7 @@ const _setDialogueContent = function(qtype, nomodalevents) { _modal.show(); const $root = _modal.getRoot(); _form = $root.get(0).querySelector('form'); + _toggleDeleteIcon(); if (!nomodalevents) { _modal.registerEventListeners(); @@ -719,6 +720,23 @@ const _setDialogueContent = function(qtype, nomodalevents) { }); }; +/** + * If there is one answer field, hide the delete icon. Otherwise show them + * all to allow deletion of any answer. + * + * @private + */ +const _toggleDeleteIcon = function() { + const deleteIcons =_form.querySelectorAll('.' + CSS.DELETE); + if (deleteIcons.length === 1) { + deleteIcons[0].classList.add('hidden'); + return; + } + for (let i = 0; i < deleteIcons.length; i++) { + deleteIcons[i].classList.remove('hidden'); + } +}; + /** * Handle question choice. * @@ -868,6 +886,7 @@ const _addAnswer = function(a) { isCustomGrade: isCustomGrade(fraction) }); _setDialogueContent(_qtype, true); + _toggleDeleteIcon(); _form.querySelectorAll('.' + CSS.ANSWER).item(index).focus(); }; @@ -889,6 +908,7 @@ const _deleteAnswer = function(a) { const answers = _form.querySelectorAll('.' + CSS.ANSWER); index = Math.min(index, answers.length - 1); answers.item(index).focus(); + _toggleDeleteIcon(); }; /**