diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0ff5de9..89377a3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,11 +31,11 @@ jobs:
matrix:
include:
- php: '8.3'
- moodle-branch: 'MOODLE_405_STABLE'
- database: 'mariadb'
- - php: '8.2'
- moodle-branch: 'MOODLE_404_STABLE'
+ moodle-branch: 'main'
database: 'pgsql'
+ - php: '8.2'
+ moodle-branch: 'MOODLE_500_STABLE'
+ database: 'mariadb'
steps:
- name: Check out repository code
diff --git a/amd/build/commands.min.js b/amd/build/commands.min.js
index 99749fb..dc4e8c1 100644
--- a/amd/build/commands.min.js
+++ b/amd/build/commands.min.js
@@ -1,3 +1,10 @@
-define("tiny_embedquestion/commands",["exports","editor_tiny/utils","core/str","./common","./dialogue_manager"],(function(_exports,_utils,_str,_common,_dialogue_manager){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[buttonText,buttonImage]=await Promise.all([(0,_str.get_string)("pluginname",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return async editor=>{registerManagerCommand(editor,buttonText,buttonImage)}};const registerManagerCommand=async(editor,buttonText,buttonImage)=>{editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addMenuItem(_common.buttonName,{icon:_common.icon,text:buttonText,onAction:async()=>{await(async()=>{const dialog=new _dialogue_manager.DialogManager(editor);await dialog.displayDialogue()})()}})}}));
+define("tiny_embedquestion/commands",["exports","editor_tiny/utils","core/str","./common","./dialogue_manager","filter_embedquestion/modal_embedquestion_question_bank","core/notification"],(function(_exports,_utils,_str,_common,_dialogue_manager,_modal_embedquestion_question_bank,Notification){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0,Notification=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
+/**
+ * Commands helper for the Moodle tiny_embedquestion plugin.
+ *
+ * @module tiny_embedquestion/commands
+ * @copyright 2024 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */(Notification);const SELECTORS_SWITCH_TO_OTHER_BANK='button[data-action="switch-question-bank"]';let isEventsRegistered=!1;_exports.getSetup=async()=>{const[buttonText,buttonImage]=await Promise.all([(0,_str.get_string)("pluginname",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return async editor=>{registerManagerCommand(editor,buttonText,buttonImage)}};const registerManagerCommand=async(editor,buttonText,buttonImage)=>{let currentDialog=null;const showQuestionBankModal=async e=>{e.preventDefault();const contextId=document.querySelector('input[name="contextid"]').value,courseId=document.querySelector('input[name="courseid"]').value,bankCmId=document.getElementById("id_qbankcmid").value;_modal_embedquestion_question_bank.ModalEmbedQuestionQuestionBank.create({contextId:contextId,courseId:courseId,bankCmId:bankCmId,title:"",addOnPage:"",large:!0,editor:editor}).catch(Notification.exception),currentDialog&¤tDialog.currentModal.destroy()};isEventsRegistered||(isEventsRegistered=!0,document.addEventListener("filter_embedquestion:qbank_selected",(function(e){currentDialog=new _dialogue_manager.DialogManager(e.detail.editor),currentDialog.displayDialogue(e.detail.bankCmid,!0).catch(Notification.exception).finally((()=>{currentDialog.currentModal.getModal().on("click",SELECTORS_SWITCH_TO_OTHER_BANK,showQuestionBankModal)}))}))),editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addMenuItem(_common.buttonName,{icon:_common.icon,text:buttonText,onAction:async()=>{await(async()=>{currentDialog=new _dialogue_manager.DialogManager(editor),await currentDialog.displayDialogue("",!1),currentDialog.currentModal.getModal().on("click",SELECTORS_SWITCH_TO_OTHER_BANK,showQuestionBankModal)})()}})}}));
//# sourceMappingURL=commands.min.js.map
\ No newline at end of file
diff --git a/amd/build/commands.min.js.map b/amd/build/commands.min.js.map
index 057b0a3..46847bb 100644
--- a/amd/build/commands.min.js.map
+++ b/amd/build/commands.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"commands.min.js","sources":["../src/commands.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 * Commands helper for the Moodle tiny_embedquestion plugin.\n *\n * @module tiny_embedquestion/commands\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getButtonImage} from 'editor_tiny/utils';\nimport {get_string as getString} from 'core/str';\nimport {\n component,\n buttonName,\n icon\n} from './common';\nimport {DialogManager} from \"./dialogue_manager\";\n\n/**\n * Get the setup function for the buttons.\n *\n * This is performed in an async function which ultimately returns the registration function as the\n * Tiny.AddOnManager.Add() function does not support async functions.\n *\n * @returns {function} The registration function to call within the Plugin.add function.\n */\nexport const getSetup = async() => {\n const [\n buttonText,\n buttonImage,\n ] = await Promise.all([\n getString('pluginname', component),\n getButtonImage('icon', component),\n ]);\n\n return async(editor) => {\n registerManagerCommand(editor, buttonText, buttonImage);\n };\n};\n\n/**\n * Registers a custom command for embed question in the editor.\n *\n * @async\n * @param {Object} editor - The editor instance.\n * @param {string} buttonText - The text to display as a tooltip for the button.\n * @param {Object} buttonImage - The image to be displayed on the button.\n */\nconst registerManagerCommand = async(editor, buttonText, buttonImage) => {\n const handleDialogManager = async() => {\n const dialog = new DialogManager(editor);\n await dialog.displayDialogue();\n };\n\n editor.ui.registry.addIcon(icon, buttonImage.html);\n\n editor.ui.registry.addMenuItem(buttonName, {\n icon: icon,\n text: buttonText,\n onAction: async() => {\n await handleDialogManager();\n }\n });\n};\n"],"names":["async","buttonText","buttonImage","Promise","all","component","registerManagerCommand","editor","ui","registry","addIcon","icon","html","addMenuItem","buttonName","text","onAction","dialog","DialogManager","displayDialogue","handleDialogManager"],"mappings":"6QAwCwBA,gBAEhBC,WACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,aAAcC,oBACxB,yBAAe,OAAQA,4BAGpBL,MAAAA,SACHM,uBAAuBC,OAAQN,WAAYC,qBAY7CI,uBAAyBN,MAAMO,OAAQN,WAAYC,eAMrDK,OAAOC,GAAGC,SAASC,QAAQC,aAAMT,YAAYU,MAE7CL,OAAOC,GAAGC,SAASI,YAAYC,mBAAY,CACvCH,KAAMA,aACNI,KAAMd,WACNe,SAAUhB,eAVcA,iBAClBiB,OAAS,IAAIC,gCAAcX,cAC3BU,OAAOE,mBASHC"}
\ No newline at end of file
+{"version":3,"file":"commands.min.js","sources":["../src/commands.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 * Commands helper for the Moodle tiny_embedquestion plugin.\n *\n * @module tiny_embedquestion/commands\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getButtonImage} from 'editor_tiny/utils';\nimport {get_string as getString} from 'core/str';\nimport {\n component,\n buttonName,\n icon\n} from './common';\nimport {DialogManager} from \"./dialogue_manager\";\nimport {ModalEmbedQuestionQuestionBank} from 'filter_embedquestion/modal_embedquestion_question_bank';\nimport * as Notification from 'core/notification';\nconst SELECTORS = {\n SWITCH_TO_OTHER_BANK: 'button[data-action=\"switch-question-bank\"]',\n};\n\nlet isEventsRegistered = false;\n\n/**\n * Get the setup function for the buttons.\n *\n * This is performed in an async function which ultimately returns the registration function as the\n * Tiny.AddOnManager.Add() function does not support async functions.\n *\n * @returns {function} The registration function to call within the Plugin.add function.\n */\nexport const getSetup = async() => {\n const [\n buttonText,\n buttonImage,\n ] = await Promise.all([\n getString('pluginname', component),\n getButtonImage('icon', component),\n ]);\n\n return async(editor) => {\n registerManagerCommand(editor, buttonText, buttonImage);\n };\n};\n\n/**\n * Registers a custom command for embed question in the editor.\n *\n * @async\n * @param {Object} editor - The editor instance.\n * @param {string} buttonText - The text to display as a tooltip for the button.\n * @param {Object} buttonImage - The image to be displayed on the button.\n */\nconst registerManagerCommand = async(editor, buttonText, buttonImage) => {\n let currentDialog = null;\n const handleDialogManager = async() => {\n currentDialog = new DialogManager(editor);\n await currentDialog.displayDialogue('', false);\n currentDialog.currentModal.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, showQuestionBankModal);\n };\n /*\n * Show the question bank modal when the user clicks on the switch to other bank button.\n * This will destroy the current modal and create a new one for the question bank.\n * @param {Event} e - The event triggered by the click.\n * @returns {Promise}\n */\n const showQuestionBankModal = async(e) => {\n e.preventDefault();\n const contextId = document.querySelector('input[name=\"contextid\"]').value;\n const courseId = document.querySelector('input[name=\"courseid\"]').value;\n const bankCmId = document.getElementById('id_qbankcmid').value;\n // Create a new instance of the modal to switch to the question bank.\n ModalEmbedQuestionQuestionBank.create({\n contextId,\n courseId,\n bankCmId,\n title: '',\n addOnPage: '',\n large: true,\n editor,\n }).catch(Notification.exception);\n if (currentDialog) {\n currentDialog.currentModal.destroy();\n }\n };\n\n\n // Just make sure we only register the events once.\n if (!isEventsRegistered) {\n isEventsRegistered = true;\n document.addEventListener('filter_embedquestion:qbank_selected', function(e) {\n currentDialog = new DialogManager(e.detail.editor);\n currentDialog.displayDialogue(e.detail.bankCmid, true).catch(Notification.exception).finally(() => {\n currentDialog.currentModal.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, showQuestionBankModal);\n });\n });\n }\n\n editor.ui.registry.addIcon(icon, buttonImage.html);\n\n editor.ui.registry.addMenuItem(buttonName, {\n icon: icon,\n text: buttonText,\n onAction: async() => {\n await handleDialogManager();\n }\n });\n};\n"],"names":["SELECTORS","isEventsRegistered","async","buttonText","buttonImage","Promise","all","component","registerManagerCommand","editor","currentDialog","showQuestionBankModal","e","preventDefault","contextId","document","querySelector","value","courseId","bankCmId","getElementById","create","title","addOnPage","large","catch","Notification","exception","currentModal","destroy","addEventListener","DialogManager","detail","displayDialogue","bankCmid","finally","getModal","on","ui","registry","addIcon","icon","html","addMenuItem","buttonName","text","onAction","handleDialogManager"],"mappings":";;;;;;;0BAiCMA,+BACoB,iDAGtBC,oBAAqB,oBAUDC,gBAEhBC,WACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,aAAcC,oBACxB,yBAAe,OAAQA,4BAGpBL,MAAAA,SACHM,uBAAuBC,OAAQN,WAAYC,qBAY7CI,uBAAyBN,MAAMO,OAAQN,WAAYC,mBACjDM,cAAgB,WAYdC,sBAAwBT,MAAAA,IAC1BU,EAAEC,uBACIC,UAAYC,SAASC,cAAc,2BAA2BC,MAC9DC,SAAWH,SAASC,cAAc,0BAA0BC,MAC5DE,SAAWJ,SAASK,eAAe,gBAAgBH,wEAE1BI,OAAO,CAClCP,UAAAA,UACAI,SAAAA,SACAC,SAAAA,SACAG,MAAO,GACPC,UAAW,GACXC,OAAO,EACPf,OAAAA,SACDgB,MAAMC,aAAaC,WAClBjB,eACAA,cAAckB,aAAaC,WAM9B5B,qBACDA,oBAAqB,EACrBc,SAASe,iBAAiB,uCAAuC,SAASlB,GACtEF,cAAgB,IAAIqB,gCAAcnB,EAAEoB,OAAOvB,QAC3CC,cAAcuB,gBAAgBrB,EAAEoB,OAAOE,UAAU,GAAMT,MAAMC,aAAaC,WAAWQ,SAAQ,KACzFzB,cAAckB,aAAaQ,WAAWC,GAAG,QAASrC,+BAAgCW,8BAK9FF,OAAO6B,GAAGC,SAASC,QAAQC,aAAMrC,YAAYsC,MAE7CjC,OAAO6B,GAAGC,SAASI,YAAYC,mBAAY,CACvCH,KAAMA,aACNI,KAAM1C,WACN2C,SAAU5C,eAhDcA,WACxBQ,cAAgB,IAAIqB,gCAActB,cAC5BC,cAAcuB,gBAAgB,IAAI,GACxCvB,cAAckB,aAAaQ,WAAWC,GAAG,QAASrC,+BAAgCW,wBA8CxEoC"}
\ No newline at end of file
diff --git a/amd/build/dialogue_manager.min.js b/amd/build/dialogue_manager.min.js
index 0a35424..7d84d21 100644
--- a/amd/build/dialogue_manager.min.js
+++ b/amd/build/dialogue_manager.min.js
@@ -5,6 +5,6 @@ define("tiny_embedquestion/dialogue_manager",["exports","core/templates","core/s
* @module tiny_embedquestion/dialogue_manager
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.DialogManager=void 0,_templates=_interopRequireDefault(_templates),_modal=_interopRequireDefault(_modal),_modal_factory=_interopRequireDefault(_modal_factory),_pending=_interopRequireDefault(_pending),_notification=_interopRequireDefault(_notification),_fragment=_interopRequireDefault(_fragment);_exports.DialogManager=class{constructor(_editor){_defineProperty(this,"editor",null),_defineProperty(this,"currentModal",null),_defineProperty(this,"displayDialogue",(async()=>{void 0!==_modal.default.create?this.currentModal=await _modal.default.create({large:!0,title:(0,_str.get_string)("pluginname","tiny_embedquestion"),body:'
',show:!0,removeOnClose:!0}):(this.currentModal=await _modal_factory.default.create({title:(0,_str.get_string)("pluginname","tiny_embedquestion"),body:'',large:!0,removeOnClose:!0}),this.currentModal.show());const pendingModalReady=new _pending.default("tiny_embedquestion/displayDialogue"),body=this.currentModal.getBody()[0];(0,_loadingicon.addIconToContainerRemoveOnCompletion)(body,pendingModalReady);let existingCode=this.getEmbedCodeFromTextSelection(this.editor);existingCode&&(existingCode=existingCode.embedCode);const dialogManager=this;_fragment.default.loadFragment("tiny_embedquestion","questionselector",(0,_options.getRelevantContextId)(this.editor),{contextId:(0,_options.getRelevantContextId)(this.editor),embedCode:existingCode}).then((function(html,js){return _templates.default.replaceNodeContents(body,html,js),body.querySelector("#embedqform #id_submitbutton").addEventListener("click",dialogManager.getEmbedCode),pendingModalReady.resolve(),dialogManager.currentModal})).catch(_notification.default.exception)})),_defineProperty(this,"getEmbedCode",(e=>{e.preventDefault();const iframeDescription=document.getElementById("id_iframedescription").value,questionIdnumber=document.getElementById("id_questionidnumber").value,dialogManager=this;questionIdnumber&&(iframeDescription.length&&(iframeDescription.length<3||iframeDescription.length>100)||dialogManager.getEmbedCodeCall(iframeDescription,questionIdnumber).then((function(embedCode){return dialogManager.insertEmbedCode(embedCode),dialogManager})).catch(_notification.default.exception))})),_defineProperty(this,"getEmbedCodeCall",((iframeDescription,questionIdnumber)=>{var _document$getElementB,_document$getElementB2,_document$getElementB3,_document$getElementB4,_document$getElementB5,_document$getElementB6,_document$getElementB7,_document$getElementB8,_document$getElementB9,_document$getElementB10,_document$getElementB11;return(0,_ajax.call)([{methodname:"filter_embedquestion_get_embed_code",args:{courseid:document.querySelector("input[name=courseid]").value,categoryidnumber:document.getElementById("id_categoryidnumber").value,questionidnumber:questionIdnumber,iframedescription:iframeDescription,behaviour:(null===(_document$getElementB=document.getElementById("id_behaviour"))||void 0===_document$getElementB?void 0:_document$getElementB.value)||"",maxmark:(null===(_document$getElementB2=document.getElementById("id_maxmark"))||void 0===_document$getElementB2?void 0:_document$getElementB2.value)||"",variant:(null===(_document$getElementB3=document.getElementById("id_variant"))||void 0===_document$getElementB3?void 0:_document$getElementB3.value)||"",correctness:(null===(_document$getElementB4=document.getElementById("id_correctness"))||void 0===_document$getElementB4?void 0:_document$getElementB4.value)||"",marks:(null===(_document$getElementB5=document.getElementById("id_marks"))||void 0===_document$getElementB5?void 0:_document$getElementB5.value)||"",markdp:(null===(_document$getElementB6=document.getElementById("id_markdp"))||void 0===_document$getElementB6?void 0:_document$getElementB6.value)||"",feedback:(null===(_document$getElementB7=document.getElementById("id_feedback"))||void 0===_document$getElementB7?void 0:_document$getElementB7.value)||"",generalfeedback:(null===(_document$getElementB8=document.getElementById("id_generalfeedback"))||void 0===_document$getElementB8?void 0:_document$getElementB8.value)||"",rightanswer:(null===(_document$getElementB9=document.getElementById("id_rightanswer"))||void 0===_document$getElementB9?void 0:_document$getElementB9.value)||"",history:(null===(_document$getElementB10=document.getElementById("id_history"))||void 0===_document$getElementB10?void 0:_document$getElementB10.value)||"",forcedlanguage:(null===(_document$getElementB11=document.getElementById("id_forcedlanguage"))||void 0===_document$getElementB11?void 0:_document$getElementB11.value)||""}}])[0]})),_defineProperty(this,"insertEmbedCode",(embedCode=>{const existingCode=this.getEmbedCodeFromTextSelection(this.editor);if(existingCode){const parent=this.editor.selection.getNode(),text=parent.textContent;parent.textContent=text.slice(0,existingCode.start)+embedCode+text.slice(existingCode.end)}else this.editor.insertContent(embedCode);this.currentModal.destroy()})),_defineProperty(this,"getEmbedCodeFromTextSelection",(editor=>{const selection=editor.selection.getSel(),selectedNode=editor.selection.getNode();let text,patternMatches,returnValue=!1;if(!selection)return!1;if(!(selection.rangeCount?selection.getRangeAt(0):null))return!1;if(text=selectedNode.textContent,patternMatches=text.match(/\{Q\{(?:(?!\}Q\}).)*\}Q\}/g),!patternMatches||!patternMatches.length)return!1;for(let i=0;i=start&&selection.anchorOffsetstart,reserveStartMatches=selection.anchorOffset<=end&&selection.anchorOffset>start,reserveEndMatches=selection.focusOffset>=start&&selection.focusOffset{void 0!==_modal.default.create?this.currentModal=await _modal.default.create({large:!0,title:(0,_str.get_string)("pluginname","tiny_embedquestion"),body:'',show:!0,removeOnClose:!0}):(this.currentModal=await _modal_factory.default.create({title:(0,_str.get_string)("pluginname","tiny_embedquestion"),body:'',large:!0,removeOnClose:!0}),this.currentModal.show());const pendingModalReady=new _pending.default("tiny_embedquestion/displayDialogue"),body=this.currentModal.getBody()[0];(0,_loadingicon.addIconToContainerRemoveOnCompletion)(body,pendingModalReady);let existingCode=this.getEmbedCodeFromTextSelection(this.editor);existingCode=existingCode&&!isSwitchBank?existingCode.embedCode:"";const dialogManager=this;_fragment.default.loadFragment("tiny_embedquestion","questionselector",(0,_options.getRelevantContextId)(this.editor),{contextId:(0,_options.getRelevantContextId)(this.editor),embedCode:existingCode,qbankCmid:qbankCmid}).then((function(html,js){return _templates.default.replaceNodeContents(body,html,js),body.querySelector("#embedqform #id_submitbutton").addEventListener("click",dialogManager.getEmbedCode),pendingModalReady.resolve(),dialogManager.currentModal})).catch(_notification.default.exception)})),_defineProperty(this,"getEmbedCode",(e=>{e.preventDefault();const iframeDescription=document.getElementById("id_iframedescription").value,questionIdnumber=document.getElementById("id_questionidnumber").value,cmId=document.getElementById("id_qbankcmid").value,dialogManager=this;cmId&&questionIdnumber&&(iframeDescription.length&&(iframeDescription.length<3||iframeDescription.length>100)||dialogManager.getEmbedCodeCall(iframeDescription,questionIdnumber,cmId).then((function(embedCode){return dialogManager.insertEmbedCode(embedCode),dialogManager})).catch(_notification.default.exception))})),_defineProperty(this,"getEmbedCodeCall",((iframeDescription,questionIdnumber,cmId)=>{var _document$getElementB,_document$getElementB2,_document$getElementB3,_document$getElementB4,_document$getElementB5,_document$getElementB6,_document$getElementB7,_document$getElementB8,_document$getElementB9,_document$getElementB10,_document$getElementB11;return(0,_ajax.call)([{methodname:"filter_embedquestion_get_embed_code",args:{cmid:cmId,categoryidnumber:document.getElementById("id_categoryidnumber").value,questionidnumber:questionIdnumber,iframedescription:iframeDescription,behaviour:(null===(_document$getElementB=document.getElementById("id_behaviour"))||void 0===_document$getElementB?void 0:_document$getElementB.value)||"",maxmark:(null===(_document$getElementB2=document.getElementById("id_maxmark"))||void 0===_document$getElementB2?void 0:_document$getElementB2.value)||"",variant:(null===(_document$getElementB3=document.getElementById("id_variant"))||void 0===_document$getElementB3?void 0:_document$getElementB3.value)||"",correctness:(null===(_document$getElementB4=document.getElementById("id_correctness"))||void 0===_document$getElementB4?void 0:_document$getElementB4.value)||"",marks:(null===(_document$getElementB5=document.getElementById("id_marks"))||void 0===_document$getElementB5?void 0:_document$getElementB5.value)||"",markdp:(null===(_document$getElementB6=document.getElementById("id_markdp"))||void 0===_document$getElementB6?void 0:_document$getElementB6.value)||"",feedback:(null===(_document$getElementB7=document.getElementById("id_feedback"))||void 0===_document$getElementB7?void 0:_document$getElementB7.value)||"",generalfeedback:(null===(_document$getElementB8=document.getElementById("id_generalfeedback"))||void 0===_document$getElementB8?void 0:_document$getElementB8.value)||"",rightanswer:(null===(_document$getElementB9=document.getElementById("id_rightanswer"))||void 0===_document$getElementB9?void 0:_document$getElementB9.value)||"",history:(null===(_document$getElementB10=document.getElementById("id_history"))||void 0===_document$getElementB10?void 0:_document$getElementB10.value)||"",forcedlanguage:(null===(_document$getElementB11=document.getElementById("id_forcedlanguage"))||void 0===_document$getElementB11?void 0:_document$getElementB11.value)||""}}])[0]})),_defineProperty(this,"insertEmbedCode",(embedCode=>{const existingCode=this.getEmbedCodeFromTextSelection(this.editor);if(existingCode){const parent=this.editor.selection.getNode(),text=parent.textContent;parent.textContent=text.slice(0,existingCode.start)+embedCode+text.slice(existingCode.end)}else this.editor.insertContent(embedCode);this.currentModal.destroy()})),_defineProperty(this,"getEmbedCodeFromTextSelection",(editor=>{const selection=editor.selection.getSel(),selectedNode=editor.selection.getNode();let text,patternMatches,returnValue=!1;if(!selection)return!1;if(!(selection.rangeCount?selection.getRangeAt(0):null))return!1;if(text=selectedNode.textContent,patternMatches=text.match(/\{Q\{(?:(?!\}Q\}).)*\}Q\}/g),!patternMatches||!patternMatches.length)return!1;for(let i=0;i=start&&selection.anchorOffsetstart,reserveStartMatches=selection.anchorOffset<=end&&selection.anchorOffset>start,reserveEndMatches=selection.focusOffset>=start&&selection.focusOffset.\n\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\nimport Modal from 'core/modal';\nimport ModalFactory from 'core/modal_factory';\nimport Pending from 'core/pending';\nimport {addIconToContainerRemoveOnCompletion} from 'core/loadingicon';\nimport {getRelevantContextId} from './options';\nimport {call as fetchMany} from 'core/ajax';\nimport Notification from 'core/notification';\nimport Fragment from 'core/fragment';\n\n/**\n * Manages the embed question dialog.\n *\n * @module tiny_embedquestion/dialogue_manager\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport const DialogManager = class {\n\n /** @property {Object} current Tiny MCE editor instance */\n editor = null;\n\n /** @property {Object} current display dialog */\n currentModal = null;\n\n /**\n * Dialog constructor.\n *\n * @constructor\n * @param {Object} editor current editor instance.\n */\n constructor(editor) {\n this.editor = editor;\n }\n\n /**\n * Displays a modal dialogue for managing embed question.\n *\n * @async\n */\n displayDialogue = async() => {\n if (typeof Modal.create !== \"undefined\") {\n this.currentModal = await Modal.create({\n large: true,\n title: getString('pluginname', 'tiny_embedquestion'),\n body: '',\n show: true,\n removeOnClose: true\n });\n } else {\n // TODO Need to be remove after we no longer support 4.2 and below.\n this.currentModal = await ModalFactory.create({\n title: getString('pluginname', 'tiny_embedquestion'),\n body: '',\n large: true,\n removeOnClose: true\n });\n this.currentModal.show();\n }\n\n const pendingModalReady = new Pending('tiny_embedquestion/displayDialogue');\n const body = this.currentModal.getBody()[0];\n addIconToContainerRemoveOnCompletion(\n body, pendingModalReady\n );\n\n let existingCode = this.getEmbedCodeFromTextSelection(this.editor);\n if (existingCode) {\n existingCode = existingCode.embedCode;\n }\n const dialogManager = this;\n Fragment.loadFragment('tiny_embedquestion', 'questionselector', getRelevantContextId(this.editor),\n {contextId: getRelevantContextId(this.editor), embedCode: existingCode}).then(function(html, js) {\n\n Templates.replaceNodeContents(body, html, js);\n body.querySelector('#embedqform #id_submitbutton').addEventListener('click', dialogManager.getEmbedCode);\n pendingModalReady.resolve();\n return dialogManager.currentModal;\n }).catch(Notification.exception);\n };\n\n /**\n * Handler for when the form button is clicked.\n * Make an AJAX request to the server to get the embed code.\n *\n * @param {Event} e - the click event.\n */\n getEmbedCode = (e) => {\n e.preventDefault();\n const iframeDescription = document.getElementById('id_iframedescription').value;\n const questionIdnumber = document.getElementById('id_questionidnumber').value;\n const dialogManager = this;\n // Required value of questionidnumber.\n // Note that the form also validates this, and deals with displaying a message to the user.\n if (!questionIdnumber) {\n return;\n }\n\n // Validate iframedescription.\n // If it is present, then it must have at least 3 characters and a maximum of 100 characters.\n // (It can be left blank to get the default description.)\n // Note that the form also validates this, and deals with displaying a message to the user.\n if (iframeDescription.length && (iframeDescription.length < 3 || iframeDescription.length > 100)) {\n return;\n }\n\n dialogManager.getEmbedCodeCall(iframeDescription, questionIdnumber).then(function(embedCode) {\n dialogManager.insertEmbedCode(embedCode);\n return dialogManager;\n }).catch(Notification.exception);\n };\n\n /**\n * Ajax call to get the embed code from back end.\n *\n * @param {String} iframeDescription - Description for the the embed code\n * @param {Number} questionIdnumber - question id number.\n * @returns {Promise}\n */\n getEmbedCodeCall = (iframeDescription, questionIdnumber) => {\n return fetchMany([{\n methodname: 'filter_embedquestion_get_embed_code',\n args: {\n courseid: document.querySelector('input[name=courseid]').value,\n categoryidnumber: document.getElementById('id_categoryidnumber').value,\n questionidnumber: questionIdnumber,\n iframedescription: iframeDescription,\n behaviour: document.getElementById('id_behaviour')?.value || '',\n maxmark: document.getElementById('id_maxmark')?.value || '',\n variant: document.getElementById('id_variant')?.value || '',\n correctness: document.getElementById('id_correctness')?.value || '',\n marks: document.getElementById('id_marks')?.value || '',\n markdp: document.getElementById('id_markdp')?.value || '',\n feedback: document.getElementById('id_feedback')?.value || '',\n generalfeedback: document.getElementById('id_generalfeedback')?.value || '',\n rightanswer: document.getElementById('id_rightanswer')?.value || '',\n history: document.getElementById('id_history')?.value || '',\n forcedlanguage: document.getElementById('id_forcedlanguage')?.value || ''\n }\n }])[0];\n };\n\n /**\n * Handles when we get the embed code from the AJAX request.\n *\n * @param {String} embedCode - the embed code to insert.\n */\n insertEmbedCode = (embedCode) => {\n const existingCode = this.getEmbedCodeFromTextSelection(this.editor);\n if (existingCode) {\n // Replace the existing code.\n const parent = this.editor.selection.getNode();\n const text = parent.textContent;\n parent.textContent = text.slice(0, existingCode.start) +\n embedCode + text.slice(existingCode.end);\n } else {\n this.editor.insertContent(embedCode);\n }\n this.currentModal.destroy();\n };\n\n /**\n * Get the embed code of the current selected text,\n *\n * @param {TinyMCE} editor\n * @returns {boolean|Object}\n * return false if we can't find the match pattern.\n * return Object {start: start position of the text, end: end position of the text, embedCode: embed code of the string}\n */\n getEmbedCodeFromTextSelection = (editor) => {\n\n // Find the embed code in the surrounding text.\n const selection = editor.selection.getSel(),\n selectedNode = editor.selection.getNode(),\n pattern = /\\{Q\\{(?:(?!\\}Q\\}).)*\\}Q\\}/g;\n let text,\n returnValue = false,\n patternMatches;\n\n if (!selection) {\n return false;\n }\n const range = selection.rangeCount ? selection.getRangeAt(0) : null;\n if (!range) {\n return false;\n }\n text = selectedNode.textContent;\n patternMatches = text.match(pattern);\n\n if (!patternMatches || !patternMatches.length) {\n return false;\n }\n // This pattern matches at least once. See if this pattern matches our current position.\n // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent\n // searches which is the required behaviour of this function.\n for (let i = 0; i < patternMatches.length; ++i) {\n let startIndex = 0;\n while (text.indexOf(patternMatches[i], startIndex) !== -1) {\n // Determine whether the cursor is in the current occurrence of this string.\n // Note: We do not support a selection exceeding the bounds of an equation.\n const start = text.indexOf(patternMatches[i], startIndex),\n end = start + patternMatches[i].length,\n startMatches = (selection.anchorOffset >= start && selection.anchorOffset < end),\n endMatches = (selection.focusOffset <= end && selection.focusOffset > start),\n reserveStartMatches = (selection.anchorOffset <= end && selection.anchorOffset > start),\n reserveEndMatches = (selection.focusOffset >= start && selection.focusOffset < end);\n if ((startMatches && endMatches) || (reserveStartMatches && reserveEndMatches)) {\n // Save all data for later.\n returnValue = {\n // Outer match data.\n start: start,\n end: end,\n embedCode: patternMatches[i]\n };\n\n // This breaks out the loop\n break;\n }\n\n // Update the startIndex to match the end of the current match so that we can continue hunting\n // for further matches.\n startIndex = end;\n }\n }\n return returnValue;\n };\n};\n"],"names":["constructor","editor","async","Modal","create","currentModal","large","title","body","show","removeOnClose","ModalFactory","pendingModalReady","Pending","this","getBody","existingCode","getEmbedCodeFromTextSelection","embedCode","dialogManager","loadFragment","contextId","then","html","js","replaceNodeContents","querySelector","addEventListener","getEmbedCode","resolve","catch","Notification","exception","e","preventDefault","iframeDescription","document","getElementById","value","questionIdnumber","length","getEmbedCodeCall","insertEmbedCode","methodname","args","courseid","categoryidnumber","questionidnumber","iframedescription","behaviour","maxmark","variant","correctness","marks","markdp","feedback","generalfeedback","rightanswer","history","forcedlanguage","parent","selection","getNode","text","textContent","slice","start","end","insertContent","destroy","getSel","selectedNode","patternMatches","returnValue","rangeCount","getRangeAt","match","i","startIndex","indexOf","startMatches","anchorOffset","endMatches","focusOffset","reserveStartMatches","reserveEndMatches"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"dialogue_manager.min.js","sources":["../src/dialogue_manager.js"],"sourcesContent":["// This file is part of Moodle - http://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\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\nimport Modal from 'core/modal';\nimport ModalFactory from 'core/modal_factory';\nimport Pending from 'core/pending';\nimport {addIconToContainerRemoveOnCompletion} from 'core/loadingicon';\nimport {getRelevantContextId} from './options';\nimport {call as fetchMany} from 'core/ajax';\nimport Notification from 'core/notification';\nimport Fragment from 'core/fragment';\n\n/**\n * Manages the embed question dialog.\n *\n * @module tiny_embedquestion/dialogue_manager\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport const DialogManager = class {\n\n /** @property {Object} current Tiny MCE editor instance */\n editor = null;\n\n /** @property {Object} current display dialog */\n currentModal = null;\n\n /**\n * Dialog constructor.\n *\n * @constructor\n * @param {Object} editor current editor instance.\n */\n constructor(editor) {\n this.editor = editor;\n }\n\n /**\n * Displays a modal dialogue for managing embed question.\n *\n * @param {String} qbankCmid - The course module id of the question bank.\n * @param {boolean} isSwitchBank - Whether we are switching to another question bank.\n * @async\n */\n displayDialogue = async(qbankCmid, isSwitchBank) => {\n if (typeof Modal.create !== \"undefined\") {\n this.currentModal = await Modal.create({\n large: true,\n title: getString('pluginname', 'tiny_embedquestion'),\n body: '',\n show: true,\n removeOnClose: true\n });\n } else {\n // TODO Need to be remove after we no longer support 4.2 and below.\n this.currentModal = await ModalFactory.create({\n title: getString('pluginname', 'tiny_embedquestion'),\n body: '',\n large: true,\n removeOnClose: true\n });\n this.currentModal.show();\n }\n\n const pendingModalReady = new Pending('tiny_embedquestion/displayDialogue');\n const body = this.currentModal.getBody()[0];\n addIconToContainerRemoveOnCompletion(\n body, pendingModalReady\n );\n\n let existingCode = this.getEmbedCodeFromTextSelection(this.editor);\n if (existingCode && !isSwitchBank) {\n existingCode = existingCode.embedCode;\n } else {\n existingCode = '';\n }\n const dialogManager = this;\n Fragment.loadFragment('tiny_embedquestion', 'questionselector', getRelevantContextId(this.editor),\n {contextId: getRelevantContextId(this.editor), embedCode: existingCode, qbankCmid: qbankCmid}).then(function(html, js) {\n\n Templates.replaceNodeContents(body, html, js);\n body.querySelector('#embedqform #id_submitbutton').addEventListener('click', dialogManager.getEmbedCode);\n pendingModalReady.resolve();\n return dialogManager.currentModal;\n }).catch(Notification.exception);\n };\n\n /**\n * Handler for when the form button is clicked.\n * Make an AJAX request to the server to get the embed code.\n *\n * @param {Event} e - the click event.\n */\n getEmbedCode = (e) => {\n e.preventDefault();\n const iframeDescription = document.getElementById('id_iframedescription').value;\n const questionIdnumber = document.getElementById('id_questionidnumber').value;\n const cmId = document.getElementById('id_qbankcmid').value;\n const dialogManager = this;\n // Required value of cmid.\n if (!cmId) {\n return;\n }\n // Required value of questionidnumber.\n // Note that the form also validates this, and deals with displaying a message to the user.\n if (!questionIdnumber) {\n return;\n }\n\n // Validate iframedescription.\n // If it is present, then it must have at least 3 characters and a maximum of 100 characters.\n // (It can be left blank to get the default description.)\n // Note that the form also validates this, and deals with displaying a message to the user.\n if (iframeDescription.length && (iframeDescription.length < 3 || iframeDescription.length > 100)) {\n return;\n }\n\n dialogManager.getEmbedCodeCall(iframeDescription, questionIdnumber, cmId).then(function(embedCode) {\n dialogManager.insertEmbedCode(embedCode);\n return dialogManager;\n }).catch(Notification.exception);\n };\n\n /**\n * Ajax call to get the embed code from back end.\n *\n * @param {String} iframeDescription - Description for the the embed code\n * @param {String} questionIdnumber - question id number.\n * @param {String} cmId - course module id.\n * @returns {Promise}\n */\n getEmbedCodeCall = (iframeDescription, questionIdnumber, cmId) => {\n return fetchMany([{\n methodname: 'filter_embedquestion_get_embed_code',\n args: {\n cmid: cmId,\n categoryidnumber: document.getElementById('id_categoryidnumber').value,\n questionidnumber: questionIdnumber,\n iframedescription: iframeDescription,\n behaviour: document.getElementById('id_behaviour')?.value || '',\n maxmark: document.getElementById('id_maxmark')?.value || '',\n variant: document.getElementById('id_variant')?.value || '',\n correctness: document.getElementById('id_correctness')?.value || '',\n marks: document.getElementById('id_marks')?.value || '',\n markdp: document.getElementById('id_markdp')?.value || '',\n feedback: document.getElementById('id_feedback')?.value || '',\n generalfeedback: document.getElementById('id_generalfeedback')?.value || '',\n rightanswer: document.getElementById('id_rightanswer')?.value || '',\n history: document.getElementById('id_history')?.value || '',\n forcedlanguage: document.getElementById('id_forcedlanguage')?.value || '',\n }\n }])[0];\n };\n\n /**\n * Handles when we get the embed code from the AJAX request.\n *\n * @param {String} embedCode - the embed code to insert.\n */\n insertEmbedCode = (embedCode) => {\n const existingCode = this.getEmbedCodeFromTextSelection(this.editor);\n if (existingCode) {\n // Replace the existing code.\n const parent = this.editor.selection.getNode();\n const text = parent.textContent;\n parent.textContent = text.slice(0, existingCode.start) +\n embedCode + text.slice(existingCode.end);\n } else {\n this.editor.insertContent(embedCode);\n }\n this.currentModal.destroy();\n };\n\n /**\n * Get the embed code of the current selected text,\n *\n * @param {TinyMCE} editor\n * @returns {boolean|Object}\n * return false if we can't find the match pattern.\n * return Object {start: start position of the text, end: end position of the text, embedCode: embed code of the string}\n */\n getEmbedCodeFromTextSelection = (editor) => {\n\n // Find the embed code in the surrounding text.\n const selection = editor.selection.getSel(),\n selectedNode = editor.selection.getNode(),\n pattern = /\\{Q\\{(?:(?!\\}Q\\}).)*\\}Q\\}/g;\n let text,\n returnValue = false,\n patternMatches;\n\n if (!selection) {\n return false;\n }\n const range = selection.rangeCount ? selection.getRangeAt(0) : null;\n if (!range) {\n return false;\n }\n text = selectedNode.textContent;\n patternMatches = text.match(pattern);\n\n if (!patternMatches || !patternMatches.length) {\n return false;\n }\n // This pattern matches at least once. See if this pattern matches our current position.\n // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent\n // searches which is the required behaviour of this function.\n for (let i = 0; i < patternMatches.length; ++i) {\n let startIndex = 0;\n while (text.indexOf(patternMatches[i], startIndex) !== -1) {\n // Determine whether the cursor is in the current occurrence of this string.\n // Note: We do not support a selection exceeding the bounds of an equation.\n const start = text.indexOf(patternMatches[i], startIndex),\n end = start + patternMatches[i].length,\n startMatches = (selection.anchorOffset >= start && selection.anchorOffset < end),\n endMatches = (selection.focusOffset <= end && selection.focusOffset > start),\n reserveStartMatches = (selection.anchorOffset <= end && selection.anchorOffset > start),\n reserveEndMatches = (selection.focusOffset >= start && selection.focusOffset < end);\n if ((startMatches && endMatches) || (reserveStartMatches && reserveEndMatches)) {\n // Save all data for later.\n returnValue = {\n // Outer match data.\n start: start,\n end: end,\n embedCode: patternMatches[i]\n };\n\n // This breaks out the loop\n break;\n }\n\n // Update the startIndex to match the end of the current match so that we can continue hunting\n // for further matches.\n startIndex = end;\n }\n }\n return returnValue;\n };\n};\n"],"names":["constructor","editor","async","qbankCmid","isSwitchBank","Modal","create","currentModal","large","title","body","show","removeOnClose","ModalFactory","pendingModalReady","Pending","this","getBody","existingCode","getEmbedCodeFromTextSelection","embedCode","dialogManager","loadFragment","contextId","then","html","js","replaceNodeContents","querySelector","addEventListener","getEmbedCode","resolve","catch","Notification","exception","e","preventDefault","iframeDescription","document","getElementById","value","questionIdnumber","cmId","length","getEmbedCodeCall","insertEmbedCode","methodname","args","cmid","categoryidnumber","questionidnumber","iframedescription","behaviour","maxmark","variant","correctness","marks","markdp","feedback","generalfeedback","rightanswer","history","forcedlanguage","parent","selection","getNode","text","textContent","slice","start","end","insertContent","destroy","getSel","selectedNode","patternMatches","returnValue","rangeCount","getRangeAt","match","i","startIndex","indexOf","startMatches","anchorOffset","endMatches","focusOffset","reserveStartMatches","reserveEndMatches"],"mappings":"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"}
\ No newline at end of file
diff --git a/amd/src/commands.js b/amd/src/commands.js
index 282a4a9..4f7ff5a 100644
--- a/amd/src/commands.js
+++ b/amd/src/commands.js
@@ -29,6 +29,13 @@ import {
icon
} from './common';
import {DialogManager} from "./dialogue_manager";
+import {ModalEmbedQuestionQuestionBank} from 'filter_embedquestion/modal_embedquestion_question_bank';
+import * as Notification from 'core/notification';
+const SELECTORS = {
+ SWITCH_TO_OTHER_BANK: 'button[data-action="switch-question-bank"]',
+};
+
+let isEventsRegistered = false;
/**
* Get the setup function for the buttons.
@@ -61,11 +68,50 @@ export const getSetup = async() => {
* @param {Object} buttonImage - The image to be displayed on the button.
*/
const registerManagerCommand = async(editor, buttonText, buttonImage) => {
+ let currentDialog = null;
const handleDialogManager = async() => {
- const dialog = new DialogManager(editor);
- await dialog.displayDialogue();
+ currentDialog = new DialogManager(editor);
+ await currentDialog.displayDialogue('', false);
+ currentDialog.currentModal.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, showQuestionBankModal);
+ };
+ /*
+ * Show the question bank modal when the user clicks on the switch to other bank button.
+ * This will destroy the current modal and create a new one for the question bank.
+ * @param {Event} e - The event triggered by the click.
+ * @returns {Promise}
+ */
+ const showQuestionBankModal = async(e) => {
+ e.preventDefault();
+ const contextId = document.querySelector('input[name="contextid"]').value;
+ const courseId = document.querySelector('input[name="courseid"]').value;
+ const bankCmId = document.getElementById('id_qbankcmid').value;
+ // Create a new instance of the modal to switch to the question bank.
+ ModalEmbedQuestionQuestionBank.create({
+ contextId,
+ courseId,
+ bankCmId,
+ title: '',
+ addOnPage: '',
+ large: true,
+ editor,
+ }).catch(Notification.exception);
+ if (currentDialog) {
+ currentDialog.currentModal.destroy();
+ }
};
+
+ // Just make sure we only register the events once.
+ if (!isEventsRegistered) {
+ isEventsRegistered = true;
+ document.addEventListener('filter_embedquestion:qbank_selected', function(e) {
+ currentDialog = new DialogManager(e.detail.editor);
+ currentDialog.displayDialogue(e.detail.bankCmid, true).catch(Notification.exception).finally(() => {
+ currentDialog.currentModal.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, showQuestionBankModal);
+ });
+ });
+ }
+
editor.ui.registry.addIcon(icon, buttonImage.html);
editor.ui.registry.addMenuItem(buttonName, {
diff --git a/amd/src/dialogue_manager.js b/amd/src/dialogue_manager.js
index dd4888f..eb48ae5 100644
--- a/amd/src/dialogue_manager.js
+++ b/amd/src/dialogue_manager.js
@@ -31,6 +31,7 @@ import Fragment from 'core/fragment';
* @copyright 2024 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
export const DialogManager = class {
/** @property {Object} current Tiny MCE editor instance */
@@ -52,9 +53,11 @@ export const DialogManager = class {
/**
* Displays a modal dialogue for managing embed question.
*
+ * @param {String} qbankCmid - The course module id of the question bank.
+ * @param {boolean} isSwitchBank - Whether we are switching to another question bank.
* @async
*/
- displayDialogue = async() => {
+ displayDialogue = async(qbankCmid, isSwitchBank) => {
if (typeof Modal.create !== "undefined") {
this.currentModal = await Modal.create({
large: true,
@@ -81,12 +84,14 @@ export const DialogManager = class {
);
let existingCode = this.getEmbedCodeFromTextSelection(this.editor);
- if (existingCode) {
+ if (existingCode && !isSwitchBank) {
existingCode = existingCode.embedCode;
+ } else {
+ existingCode = '';
}
const dialogManager = this;
Fragment.loadFragment('tiny_embedquestion', 'questionselector', getRelevantContextId(this.editor),
- {contextId: getRelevantContextId(this.editor), embedCode: existingCode}).then(function(html, js) {
+ {contextId: getRelevantContextId(this.editor), embedCode: existingCode, qbankCmid: qbankCmid}).then(function(html, js) {
Templates.replaceNodeContents(body, html, js);
body.querySelector('#embedqform #id_submitbutton').addEventListener('click', dialogManager.getEmbedCode);
@@ -105,7 +110,12 @@ export const DialogManager = class {
e.preventDefault();
const iframeDescription = document.getElementById('id_iframedescription').value;
const questionIdnumber = document.getElementById('id_questionidnumber').value;
+ const cmId = document.getElementById('id_qbankcmid').value;
const dialogManager = this;
+ // Required value of cmid.
+ if (!cmId) {
+ return;
+ }
// Required value of questionidnumber.
// Note that the form also validates this, and deals with displaying a message to the user.
if (!questionIdnumber) {
@@ -120,7 +130,7 @@ export const DialogManager = class {
return;
}
- dialogManager.getEmbedCodeCall(iframeDescription, questionIdnumber).then(function(embedCode) {
+ dialogManager.getEmbedCodeCall(iframeDescription, questionIdnumber, cmId).then(function(embedCode) {
dialogManager.insertEmbedCode(embedCode);
return dialogManager;
}).catch(Notification.exception);
@@ -130,14 +140,15 @@ export const DialogManager = class {
* Ajax call to get the embed code from back end.
*
* @param {String} iframeDescription - Description for the the embed code
- * @param {Number} questionIdnumber - question id number.
+ * @param {String} questionIdnumber - question id number.
+ * @param {String} cmId - course module id.
* @returns {Promise}
*/
- getEmbedCodeCall = (iframeDescription, questionIdnumber) => {
+ getEmbedCodeCall = (iframeDescription, questionIdnumber, cmId) => {
return fetchMany([{
methodname: 'filter_embedquestion_get_embed_code',
args: {
- courseid: document.querySelector('input[name=courseid]').value,
+ cmid: cmId,
categoryidnumber: document.getElementById('id_categoryidnumber').value,
questionidnumber: questionIdnumber,
iframedescription: iframeDescription,
@@ -151,7 +162,7 @@ export const DialogManager = class {
generalfeedback: document.getElementById('id_generalfeedback')?.value || '',
rightanswer: document.getElementById('id_rightanswer')?.value || '',
history: document.getElementById('id_history')?.value || '',
- forcedlanguage: document.getElementById('id_forcedlanguage')?.value || ''
+ forcedlanguage: document.getElementById('id_forcedlanguage')?.value || '',
}
}])[0];
};
diff --git a/changes.md b/changes.md
index 81ad764..c6b1956 100644
--- a/changes.md
+++ b/changes.md
@@ -1,3 +1,6 @@
+## Changes in 1.2
+* Added support for Moodle 5.0+
+
## Changes in 1.1
* Added support for Moodle 4.5+
diff --git a/lib.php b/lib.php
index ce3c301..83457cc 100644
--- a/lib.php
+++ b/lib.php
@@ -23,6 +23,7 @@
*/
use filter_embedquestion\form\embed_options_form;
+use filter_embedquestion\utils;
/**
* Server side controller used by core Fragment javascript to return a moodle form html.
@@ -37,13 +38,22 @@ function tiny_embedquestion_output_fragment_questionselector(array $args): strin
global $CFG;
require_once($CFG->dirroot . '/filter/embedquestion/filter.php');
$context = context::instance_by_id($args['contextId']);
- $mform = new embed_options_form(null, ['context' => $context]);
+ $qbankcmid = $args['qbankCmid'];
+
+ $mform = new embed_options_form(null, ['context' => $context, 'qbankcmid' => $qbankcmid,
+ 'embedcode' => $args['embedCode']]);
$currentvalue = $args['embedCode'];
if ($currentvalue && preg_match(filter_embedquestion::get_filter_regexp(), $currentvalue, $matches)) {
-
[$embedid, $toform] = filter_embedquestion::parse_embed_code($matches[1]);
if ($embedid !== null) {
+ $courseid = $context->instanceid;
+ if ($embedid->courseshortname) {
+ $courseid = utils::get_courseid_by_course_shortname($embedid->courseshortname);
+ }
+ $cmid = utils::get_qbank_by_idnumber($courseid, $embedid->questionbankidnumber);
+ $toform['courseshortname'] = $embedid->courseshortname;
+ $toform['qbankcmid'] = $cmid;
$toform['questionidnumber'] = $embedid->questionidnumber;
$toform['categoryidnumber'] = $embedid->categoryidnumber;
// Decode iframedescription data to form.
@@ -53,6 +63,5 @@ function tiny_embedquestion_output_fragment_questionselector(array $args): strin
$mform->set_data($toform);
}
}
-
return $mform->render();
}
diff --git a/tests/behat/embedquestion.feature b/tests/behat/embedquestion.feature
index f29efcd..acc8b86 100644
--- a/tests/behat/embedquestion.feature
+++ b/tests/behat/embedquestion.feature
@@ -11,15 +11,23 @@ Feature: Embed question in the Tiny editor
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
+ | Course 2 | C2 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher | C1 | editingteacher |
+ | teacher | C2 | editingteacher |
+ And the following "activities" exist:
+ | activity | name | intro | course | idnumber |
+ | qbank | Qbank 1 | Question bank 1 | C1 | qbank1 |
+ | qbank | Qbank 2 | Question bank 2 | C2 | qbank2 |
And the following "question categories" exist:
- | contextlevel | reference | name | idnumber |
- | Course | C1 | Test questions | embed |
+ | contextlevel | reference | name | idnumber |
+ | Activity module | qbank1 | Test questions | embed |
+ | Activity module | qbank2 | Test questions 2 | embed2 |
And the following "questions" exist:
| questioncategory | qtype | name | idnumber |
| Test questions | truefalse | First question | test1 |
+ | Test questions 2 | truefalse | First question | test2 |
And the "embedquestion" filter is "on"
@javascript
@@ -27,15 +35,18 @@ Feature: Embed question in the Tiny editor
Given I am on the "Course 1" course page logged in as teacher
And I turn editing mode on
When I add a page activity to course "Course 1" section "1"
+ # Just make the Tiny dropdown have enough space to show the "Embed question" menu item.
+ And I change window size to "large"
And I set the field "Name" to "Test page 01"
And I set the field "Description" to "Test page description"
And I set the field "content" to "Test page content"
- When I click on the "Insert > Embedded question" menu item for the "Description" TinyMCE editor
+ And I click on the "Insert > Embedded question" menu item for the "Description" TinyMCE editor
+ And I set the field "Question bank" to "Qbank 1 [qbank1]"
And I set the field "Question category" to "Test questions [embed] (1)"
And I set the field "id_questionidnumber" to "First question [test1]"
And I click on "Embed question" "button" in the "Embedded question" "dialogue"
And I switch to the "Description" TinyMCE editor iframe
- Then I should see "{Q{embed/test1|"
+ Then I should see "{Q{C1/qbank1/embed/test1|"
And I should see "}Q}Test page description"
And I switch to the main frame
# Check that reopening the form sets the fields to the current question.
@@ -43,3 +54,32 @@ Feature: Embed question in the Tiny editor
And I click on the "Insert > Embedded question" menu item for the "Description" TinyMCE editor
And the field "Question category" matches value "Test questions [embed] (1)"
And the field "id_questionidnumber" matches value "First question [test1]"
+
+ @javascript
+ Scenario: Test using 'Switch bank' button
+ Given I am on the "Course 1" course page logged in as teacher
+ And I turn editing mode on
+ And I add a page activity to course "Course 1" section "1"
+ # Just make the Tiny dropdown have enough space to show the "Embed question" menu item.
+ And I change window size to "large"
+ And I set the field "Name" to "Test page 01"
+ And I set the field "Description" to "Test page description"
+ And I set the field "content" to "Test page content"
+ And I click on the "Insert > Embedded question" menu item for the "Description" TinyMCE editor
+ When I click on "Switch bank" "button" in the "Embedded question" "dialogue"
+ And I should see "Qbank 1"
+ And I open the autocomplete suggestions list in the "Select question bank" "dialogue"
+ And "Qbank 2" "autocomplete_suggestions" should exist
+ And I click on "C2 - Qbank 2" item in the autocomplete list
+ And I set the field "Question category" to "Test questions 2 [embed2] (1)"
+ And I set the field "id_questionidnumber" to "First question [test2]"
+ And I click on "Embed question" "button" in the "Embedded question" "dialogue"
+ And I switch to the "Description" TinyMCE editor iframe
+ Then I should see "{Q{C2/qbank2/embed2/test2|"
+ And I should see "}Q}Test page description"
+ And I switch to the main frame
+ # Check that reopening the form sets the fields to the current question.
+ And I select the "Q" "text" in the "Description" TinyMCE editor
+ And I click on the "Insert > Embedded question" menu item for the "Description" TinyMCE editor
+ And the field "Question category" matches value "Test questions 2 [embed2] (1)"
+ And the field "id_questionidnumber" matches value "First question [test2]"
diff --git a/version.php b/version.php
index 33d6fe4..8199cba 100644
--- a/version.php
+++ b/version.php
@@ -24,14 +24,14 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2025082700;
+$plugin->version = 2025091600;
$plugin->requires = 2024042200; // Requires Moodle 4.4.
$plugin->component = 'tiny_embedquestion';
-$plugin->release = '1.1';
+$plugin->release = '1.2';
$plugin->maturity = MATURITY_STABLE;
$plugin->dependencies = [
- 'filter_embedquestion' => 2025050100,
+ 'filter_embedquestion' => 2025091600,
];
$plugin->outestssufficient = true;