diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a2ddcc0..b90f342 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,16 +31,16 @@ jobs:
matrix:
include:
- php: '8.2'
- moodle-branch: 'master'
+ moodle-branch: 'main'
database: 'pgsql'
- php: '8.1'
- moodle-branch: 'MOODLE_402_STABLE'
+ moodle-branch: 'MOODLE_403_STABLE'
database: 'mariadb'
- php: '8.0'
- moodle-branch: 'MOODLE_401_STABLE'
+ moodle-branch: 'MOODLE_402_STABLE'
database: 'pgsql'
- php: '7.4'
- moodle-branch: 'MOODLE_400_STABLE'
+ moodle-branch: 'MOODLE_401_STABLE'
database: 'mariadb'
steps:
diff --git a/amd/build/commands.min.js b/amd/build/commands.min.js
new file mode 100644
index 0000000..e98ee99
--- /dev/null
+++ b/amd/build/commands.min.js
@@ -0,0 +1,3 @@
+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)=>{const handleDialogManager=async()=>{const dialog=new _dialogue_manager.DialogManager(editor);await dialog.displayDialogue()};editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addButton(_common.buttonName,{icon:_common.icon,tooltip:buttonText,onAction:async()=>{await handleDialogManager()}}),editor.ui.registry.addMenuItem(_common.buttonName,{icon:_common.icon,text:buttonText,onAction:async()=>{await handleDialogManager()}}),editor.ui.registry.addToggleButton(_common.buttonName,{icon:_common.icon,tooltip:buttonText,onAction:async()=>{await handleDialogManager()},onSetup:api=>{editor.on("NodeChange",(()=>{const dialog=new _dialogue_manager.DialogManager(editor);api.setActive(!!dialog.getEmbedCodeFromTextSelection(editor))}))}})}}));
+
+//# 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
new file mode 100644
index 0000000..c258afa
--- /dev/null
+++ b/amd/build/commands.min.js.map
@@ -0,0 +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 editor.ui.registry.addButton(buttonName, {\n icon: icon,\n tooltip: buttonText,\n onAction: async() => {\n await handleDialogManager();\n }\n });\n\n editor.ui.registry.addMenuItem(buttonName, {\n icon: icon,\n text: buttonText,\n onAction: async() => {\n await handleDialogManager();\n }\n });\n\n // Register the Menu Button as a toggle.\n editor.ui.registry.addToggleButton(buttonName, {\n icon: icon,\n tooltip: buttonText,\n onAction: async() => {\n await handleDialogManager();\n },\n onSetup: (api) => {\n editor.on('NodeChange', () => {\n const dialog = new DialogManager(editor);\n // Set the button to be active if the current selection matches the embed question code format.\n api.setActive(!!dialog.getEmbedCodeFromTextSelection(editor));\n });\n },\n });\n};\n"],"names":["async","buttonText","buttonImage","Promise","all","component","registerManagerCommand","editor","handleDialogManager","dialog","DialogManager","displayDialogue","ui","registry","addIcon","icon","html","addButton","buttonName","tooltip","onAction","addMenuItem","text","addToggleButton","onSetup","api","on","setActive","getEmbedCodeFromTextSelection"],"mappings":"6QAwCwBA,gBAEhBC,WACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,aAAcC,oBACxB,yBAAe,OAAQA,4BAGpBL,MAAAA,SACHM,uBAAuBC,OAAQN,WAAYC,qBAY7CI,uBAAyBN,MAAMO,OAAQN,WAAYC,qBAC/CM,oBAAsBR,gBAClBS,OAAS,IAAIC,gCAAcH,cAC3BE,OAAOE,mBAGjBJ,OAAOK,GAAGC,SAASC,QAAQC,aAAMb,YAAYc,MAC7CT,OAAOK,GAAGC,SAASI,UAAUC,mBAAY,CACrCH,KAAMA,aACNI,QAASlB,WACTmB,SAAUpB,gBACAQ,yBAIdD,OAAOK,GAAGC,SAASQ,YAAYH,mBAAY,CACvCH,KAAMA,aACNO,KAAMrB,WACNmB,SAAUpB,gBACAQ,yBAKdD,OAAOK,GAAGC,SAASU,gBAAgBL,mBAAY,CAC3CH,KAAMA,aACNI,QAASlB,WACTmB,SAAUpB,gBACAQ,uBAEVgB,QAAUC,MACNlB,OAAOmB,GAAG,cAAc,WACdjB,OAAS,IAAIC,gCAAcH,QAEjCkB,IAAIE,YAAYlB,OAAOmB,8BAA8BrB"}
\ No newline at end of file
diff --git a/amd/build/common.min.js b/amd/build/common.min.js
new file mode 100644
index 0000000..81ff963
--- /dev/null
+++ b/amd/build/common.min.js
@@ -0,0 +1,11 @@
+define("tiny_embedquestion/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;
+/**
+ * Common values helper for the embed question plugin.
+ *
+ * @module tiny_embedquestion/common
+ * @copyright 2024 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+const component="tiny_embedquestion";var _default={pluginName:"".concat(component,"/plugin"),component:"".concat(component),buttonName:"".concat(component),icon:"".concat(component)};return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=common.min.js.map
\ No newline at end of file
diff --git a/amd/build/common.min.js.map b/amd/build/common.min.js.map
new file mode 100644
index 0000000..2af1848
--- /dev/null
+++ b/amd/build/common.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"common.min.js","sources":["../src/common.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 * Common values helper for the embed question plugin.\n *\n * @module tiny_embedquestion/common\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst component = 'tiny_embedquestion';\n\nexport default {\n pluginName: `${component}/plugin`,\n component: `${component}`,\n buttonName: `${component}`,\n icon: `${component}`,\n};\n"],"names":["component","pluginName","buttonName","icon"],"mappings":";;;;;;;;MAuBMA,UAAY,kCAEH,CACXC,qBAAeD,qBACfA,oBAAcA,WACdE,qBAAeF,WACfG,eAASH"}
\ No newline at end of file
diff --git a/amd/build/configuration.min.js b/amd/build/configuration.min.js
new file mode 100644
index 0000000..c75846e
--- /dev/null
+++ b/amd/build/configuration.min.js
@@ -0,0 +1,3 @@
+define("tiny_embedquestion/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{return{toolbar:(toolbar=instanceConfig.toolbar,toolbar=(0,_utils.addToolbarButton)(toolbar,"formatting",_common.buttonName)),menu:(0,_utils.addMenubarItem)(instanceConfig.menu,"insert",_common.buttonName)};var toolbar}}));
+
+//# sourceMappingURL=configuration.min.js.map
\ No newline at end of file
diff --git a/amd/build/configuration.min.js.map b/amd/build/configuration.min.js.map
new file mode 100644
index 0000000..7a30a1e
--- /dev/null
+++ b/amd/build/configuration.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"configuration.min.js","sources":["../src/configuration.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 * Tiny embed question configuration for Moodle.\n *\n * @module tiny_embedquestion/configuration\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {buttonName} from './common';\nimport {addMenubarItem, addToolbarButton} from 'editor_tiny/utils';\n\n/**\n * This function control where the button is display in the editor.\n *\n * @param {Object} toolbar\n * @returns {addToolbarButton}\n */\nconst configureToolbar = (toolbar) => {\n toolbar = addToolbarButton(toolbar, 'formatting', buttonName);\n return toolbar;\n};\n\n/**\n * This function control where the button is display in the menu bar of the editor.\n *\n * @param {Object} instanceConfig\n * @returns {Object}\n */\nexport const configure = (instanceConfig) => {\n return {\n toolbar: configureToolbar(instanceConfig.toolbar),\n menu: addMenubarItem(instanceConfig.menu, 'insert', buttonName),\n };\n};\n"],"names":["instanceConfig","toolbar","buttonName","menu"],"mappings":"6NA2C0BA,uBACf,CACHC,SAbkBA,QAaQD,eAAeC,QAZ7CA,SAAU,2BAAiBA,QAAS,aAAcC,qBAa9CC,MAAM,yBAAeH,eAAeG,KAAM,SAAUD,qBAdlCD,IAAAA"}
\ No newline at end of file
diff --git a/amd/build/dialogue_manager.min.js b/amd/build/dialogue_manager.min.js
new file mode 100644
index 0000000..0a35424
--- /dev/null
+++ b/amd/build/dialogue_manager.min.js
@@ -0,0 +1,10 @@
+define("tiny_embedquestion/dialogue_manager",["exports","core/templates","core/str","core/modal","core/modal_factory","core/pending","core/loadingicon","./options","core/ajax","core/notification","core/fragment"],(function(_exports,_templates,_str,_modal,_modal_factory,_pending,_loadingicon,_options,_ajax,_notification,_fragment){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}
+/**
+ * Manages the embed question dialog.
+ *
+ * @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.\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":";;;;;;;sYAiC6B,MAczBA,YAAYC,uCAXH,0CAGM,8CAiBGC,eACc,IAAjBC,eAAMC,YACRC,mBAAqBF,eAAMC,OAAO,CACnCE,OAAO,EACPC,OAAO,mBAAU,aAAc,sBAC/BC,KAAM,8CACNC,MAAM,EACNC,eAAe,UAIdL,mBAAqBM,uBAAaP,OAAO,CAC1CG,OAAO,mBAAU,aAAc,sBAC/BC,KAAM,8CACNF,OAAO,EACPI,eAAe,SAEdL,aAAaI,cAGhBG,kBAAoB,IAAIC,iBAAQ,sCAChCL,KAAOM,KAAKT,aAAaU,UAAU,yDAErCP,KAAMI,uBAGNI,aAAeF,KAAKG,8BAA8BH,KAAKb,QACvDe,eACAA,aAAeA,aAAaE,iBAE1BC,cAAgBL,uBACbM,aAAa,qBAAsB,oBAAoB,iCAAqBN,KAAKb,QACtF,CAACoB,WAAW,iCAAqBP,KAAKb,QAASiB,UAAWF,eAAeM,MAAK,SAASC,KAAMC,8BAEnFC,oBAAoBjB,KAAMe,KAAMC,IAC1ChB,KAAKkB,cAAc,gCAAgCC,iBAAiB,QAASR,cAAcS,cAC3FhB,kBAAkBiB,UACXV,cAAcd,gBACtByB,MAAMC,sBAAaC,mDASVC,IACZA,EAAEC,uBACIC,kBAAoBC,SAASC,eAAe,wBAAwBC,MACpEC,iBAAmBH,SAASC,eAAe,uBAAuBC,MAClEnB,cAAgBL,KAGjByB,mBAQDJ,kBAAkBK,SAAWL,kBAAkBK,OAAS,GAAKL,kBAAkBK,OAAS,MAI5FrB,cAAcsB,iBAAiBN,kBAAmBI,kBAAkBjB,MAAK,SAASJ,kBAC9EC,cAAcuB,gBAAgBxB,WACvBC,iBACRW,MAAMC,sBAAaC,wDAUP,CAACG,kBAAmBI,6RAC5B,cAAU,CAAC,CACdI,WAAY,sCACZC,KAAM,CACFC,SAAUT,SAASV,cAAc,wBAAwBY,MACzDQ,iBAAkBV,SAASC,eAAe,uBAAuBC,MACjES,iBAAkBR,iBAClBS,kBAAmBb,kBACnBc,yCAAWb,SAASC,eAAe,8EAAiBC,QAAS,GAC7DY,wCAASd,SAASC,eAAe,8EAAeC,QAAS,GACzDa,wCAASf,SAASC,eAAe,8EAAeC,QAAS,GACzDc,4CAAahB,SAASC,eAAe,kFAAmBC,QAAS,GACjEe,sCAAOjB,SAASC,eAAe,4EAAaC,QAAS,GACrDgB,uCAAQlB,SAASC,eAAe,6EAAcC,QAAS,GACvDiB,yCAAUnB,SAASC,eAAe,+EAAgBC,QAAS,GAC3DkB,gDAAiBpB,SAASC,eAAe,sFAAuBC,QAAS,GACzEmB,4CAAarB,SAASC,eAAe,kFAAmBC,QAAS,GACjEoB,yCAAStB,SAASC,eAAe,gFAAeC,QAAS,GACzDqB,gDAAgBvB,SAASC,eAAe,uFAAsBC,QAAS,OAE3E,8CAQWpB,kBACTF,aAAeF,KAAKG,8BAA8BH,KAAKb,WACzDe,aAAc,OAER4C,OAAS9C,KAAKb,OAAO4D,UAAUC,UAC/BC,KAAOH,OAAOI,YACpBJ,OAAOI,YAAcD,KAAKE,MAAM,EAAGjD,aAAakD,OAC5ChD,UAAY6C,KAAKE,MAAMjD,aAAamD,eAEnClE,OAAOmE,cAAclD,gBAEzBb,aAAagE,mEAWWpE,eAGvB4D,UAAY5D,OAAO4D,UAAUS,SAC/BC,aAAetE,OAAO4D,UAAUC,cAEhCC,KAEAS,eADAC,aAAc,MAGbZ,iBACM,OAEGA,UAAUa,WAAab,UAAUc,WAAW,GAAK,aAEpD,KAEXZ,KAAOQ,aAAaP,YACpBQ,eAAiBT,KAAKa,MAbR,+BAeTJ,iBAAmBA,eAAehC,cAC5B,MAKN,IAAIqC,EAAI,EAAGA,EAAIL,eAAehC,SAAUqC,EAAG,KACxCC,WAAa,QACuC,IAAjDf,KAAKgB,QAAQP,eAAeK,GAAIC,aAAoB,OAGjDZ,MAAQH,KAAKgB,QAAQP,eAAeK,GAAIC,YAC1CX,IAAMD,MAAQM,eAAeK,GAAGrC,OAChCwC,aAAgBnB,UAAUoB,cAAgBf,OAASL,UAAUoB,aAAed,IAC5Ee,WAAcrB,UAAUsB,aAAehB,KAAON,UAAUsB,YAAcjB,MACtEkB,oBAAuBvB,UAAUoB,cAAgBd,KAAON,UAAUoB,aAAef,MACjFmB,kBAAqBxB,UAAUsB,aAAejB,OAASL,UAAUsB,YAAchB,OAC9Ea,cAAgBE,YAAgBE,qBAAuBC,kBAAoB,CAE5EZ,YAAc,CAEVP,MAAOA,MACPC,IAAKA,IACLjD,UAAWsD,eAAeK,UASlCC,WAAaX,YAGdM,oBAhMFxE,OAASA"}
\ No newline at end of file
diff --git a/amd/build/options.min.js b/amd/build/options.min.js
new file mode 100644
index 0000000..9f6d3fd
--- /dev/null
+++ b/amd/build/options.min.js
@@ -0,0 +1,11 @@
+define("tiny_embedquestion/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.getRelevantContextId=void 0;
+/**
+ * Option helper for the Moodle tiny_embedquestion plugin.
+ *
+ * @module tiny_embedquestion/options
+ * @copyright 2024 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+const relevantContextId=(0,_options.getPluginOptionName)(_common.pluginName,"relevantContextId");_exports.register=editor=>{(0,editor.options.register)(relevantContextId,{processor:"number"})};_exports.getRelevantContextId=editor=>editor.options.get(relevantContextId)}));
+
+//# sourceMappingURL=options.min.js.map
\ No newline at end of file
diff --git a/amd/build/options.min.js.map b/amd/build/options.min.js.map
new file mode 100644
index 0000000..f532b19
--- /dev/null
+++ b/amd/build/options.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"options.min.js","sources":["../src/options.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 * Option helper for the Moodle tiny_embedquestion plugin.\n *\n * @module tiny_embedquestion/options\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\nconst relevantContextId = getPluginOptionName(pluginName, 'relevantContextId');\n\n/**\n * Register the options for the Tiny Equation plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(relevantContextId, {\n processor: 'number',\n });\n\n};\n\n/**\n * Get the context id for the Tiny embed question plugin.\n *\n * @param {TinyMCE} editor\n * @returns {Number} - context id\n */\nexport const getRelevantContextId = (editor) => editor.options.get(relevantContextId);\n"],"names":["relevantContextId","pluginName","editor","registerOption","options","register","processor","get"],"mappings":";;;;;;;;MAwBMA,mBAAoB,gCAAoBC,mBAAY,uCAOjCC,UAGrBC,EAFuBD,OAAOE,QAAQC,UAEvBL,kBAAmB,CAC9BM,UAAW,0CAWkBJ,QAAWA,OAAOE,QAAQG,IAAIP"}
\ No newline at end of file
diff --git a/amd/build/plugin.min.js b/amd/build/plugin.min.js
new file mode 100644
index 0000000..555e272
--- /dev/null
+++ b/amd/build/plugin.min.js
@@ -0,0 +1,10 @@
+define("tiny_embedquestion/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration","./options"],(function(_exports,_loader,_utils,_common,_commands,Configuration,Options){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)}function _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
+/**
+ * Embed question plugin for TinyMCE.
+ *
+ * @module tiny_embedquestion
+ * @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.default=void 0,Configuration=_interopRequireWildcard(Configuration),Options=_interopRequireWildcard(Options);var _default=new Promise((async resolve=>{const[tinyMCE,pluginMetadata,setupCommands]=await Promise.all([(0,_loader.getTinyMCE)(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName),(0,_commands.getSetup)()]);tinyMCE.PluginManager.add(_common.pluginName,(editor=>(Options.register(editor),setupCommands(editor),pluginMetadata))),resolve([_common.pluginName,Configuration])}));return _exports.default=_default,_exports.default}));
+
+//# sourceMappingURL=plugin.min.js.map
\ No newline at end of file
diff --git a/amd/build/plugin.min.js.map b/amd/build/plugin.min.js.map
new file mode 100644
index 0000000..4202849
--- /dev/null
+++ b/amd/build/plugin.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"plugin.min.js","sources":["../src/plugin.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 * Embed question plugin for TinyMCE.\n *\n * @module tiny_embedquestion\n * @copyright 2024 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\nimport {component, pluginName} from './common';\nimport {getSetup as getCommandSetup} from './commands';\nimport * as Configuration from './configuration';\nimport * as Options from \"./options\";\n\n// Setup the tiny_embedquestion Plugin.\n// eslint-disable-next-line no-async-promise-executor\nexport default new Promise(async(resolve) => {\n // Note: The PluginManager.add function does not support asynchronous configuration.\n // Perform any asynchronous configuration here, and then call the PluginManager.add function.\n const [\n tinyMCE,\n pluginMetadata,\n setupCommands,\n ] = await Promise.all([\n getTinyMCE(),\n getPluginMetadata(component, pluginName),\n getCommandSetup(),\n ]);\n\n // Reminder: Any asynchronous code must be run before this point.\n tinyMCE.PluginManager.add(pluginName, (editor) => {\n\n // Register any options that your plugin has\n Options.register(editor);\n // Setup any commands such as buttons, menu items, and so on.\n setupCommands(editor);\n\n // Return the pluginMetadata object. This is used by TinyMCE to display a help link for your plugin.\n return pluginMetadata;\n });\n\n resolve([pluginName, Configuration]);\n});\n"],"names":["Promise","async","tinyMCE","pluginMetadata","setupCommands","all","component","pluginName","PluginManager","add","editor","Options","register","resolve","Configuration"],"mappings":";;;;;;;gMAgCe,IAAIA,SAAQC,MAAAA,gBAInBC,QACAC,eACAC,qBACMJ,QAAQK,IAAI,EAClB,yBACA,4BAAkBC,kBAAWC,qBAC7B,0BAIJL,QAAQM,cAAcC,IAAIF,oBAAaG,SAGnCC,QAAQC,SAASF,QAEjBN,cAAcM,QAGPP,kBAGXU,QAAQ,CAACN,mBAAYO"}
\ No newline at end of file
diff --git a/amd/src/commands.js b/amd/src/commands.js
new file mode 100644
index 0000000..ee9de85
--- /dev/null
+++ b/amd/src/commands.js
@@ -0,0 +1,101 @@
+// This file is part of Moodle - https://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * 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
+ */
+
+import {getButtonImage} from 'editor_tiny/utils';
+import {get_string as getString} from 'core/str';
+import {
+ component,
+ buttonName,
+ icon
+} from './common';
+import {DialogManager} from "./dialogue_manager";
+
+/**
+ * Get the setup function for the buttons.
+ *
+ * This is performed in an async function which ultimately returns the registration function as the
+ * Tiny.AddOnManager.Add() function does not support async functions.
+ *
+ * @returns {function} The registration function to call within the Plugin.add function.
+ */
+export const getSetup = async() => {
+ const [
+ buttonText,
+ buttonImage,
+ ] = await Promise.all([
+ getString('pluginname', component),
+ getButtonImage('icon', component),
+ ]);
+
+ return async(editor) => {
+ registerManagerCommand(editor, buttonText, buttonImage);
+ };
+};
+
+/**
+ * Registers a custom command for embed question in the editor.
+ *
+ * @async
+ * @param {Object} editor - The editor instance.
+ * @param {string} buttonText - The text to display as a tooltip for the button.
+ * @param {Object} buttonImage - The image to be displayed on the button.
+ */
+const registerManagerCommand = async(editor, buttonText, buttonImage) => {
+ const handleDialogManager = async() => {
+ const dialog = new DialogManager(editor);
+ await dialog.displayDialogue();
+ };
+
+ editor.ui.registry.addIcon(icon, buttonImage.html);
+ editor.ui.registry.addButton(buttonName, {
+ icon: icon,
+ tooltip: buttonText,
+ onAction: async() => {
+ await handleDialogManager();
+ }
+ });
+
+ editor.ui.registry.addMenuItem(buttonName, {
+ icon: icon,
+ text: buttonText,
+ onAction: async() => {
+ await handleDialogManager();
+ }
+ });
+
+ // Register the Menu Button as a toggle.
+ editor.ui.registry.addToggleButton(buttonName, {
+ icon: icon,
+ tooltip: buttonText,
+ onAction: async() => {
+ await handleDialogManager();
+ },
+ onSetup: (api) => {
+ editor.on('NodeChange', () => {
+ const dialog = new DialogManager(editor);
+ // Set the button to be active if the current selection matches the embed question code format.
+ api.setActive(!!dialog.getEmbedCodeFromTextSelection(editor));
+ });
+ },
+ });
+};
diff --git a/amd/src/common.js b/amd/src/common.js
index 228b622..d98b884 100644
--- a/amd/src/common.js
+++ b/amd/src/common.js
@@ -14,16 +14,18 @@
// along with Moodle. If not, see .
/**
- * Common values helper for the Moodle tiny_embedquestion plugin.
+ * Common values helper for the embed question plugin.
*
- * @module plugintype_pluginname/common
- * @copyright 2023 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module tiny_embedquestion/common
+ * @copyright 2024 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const component = 'tiny_embedquestion';
export default {
- component,
pluginName: `${component}/plugin`,
+ component: `${component}`,
+ buttonName: `${component}`,
+ icon: `${component}`,
};
diff --git a/amd/src/configuration.js b/amd/src/configuration.js
new file mode 100644
index 0000000..4f8e8b1
--- /dev/null
+++ b/amd/src/configuration.js
@@ -0,0 +1,49 @@
+// This file is part of Moodle - https://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Tiny embed question configuration for Moodle.
+ *
+ * @module tiny_embedquestion/configuration
+ * @copyright 2024 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {buttonName} from './common';
+import {addMenubarItem, addToolbarButton} from 'editor_tiny/utils';
+
+/**
+ * This function control where the button is display in the editor.
+ *
+ * @param {Object} toolbar
+ * @returns {addToolbarButton}
+ */
+const configureToolbar = (toolbar) => {
+ toolbar = addToolbarButton(toolbar, 'formatting', buttonName);
+ return toolbar;
+};
+
+/**
+ * This function control where the button is display in the menu bar of the editor.
+ *
+ * @param {Object} instanceConfig
+ * @returns {Object}
+ */
+export const configure = (instanceConfig) => {
+ return {
+ toolbar: configureToolbar(instanceConfig.toolbar),
+ menu: addMenubarItem(instanceConfig.menu, 'insert', buttonName),
+ };
+};
diff --git a/amd/src/dialogue_manager.js b/amd/src/dialogue_manager.js
new file mode 100644
index 0000000..dd4888f
--- /dev/null
+++ b/amd/src/dialogue_manager.js
@@ -0,0 +1,243 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+import Templates from 'core/templates';
+import {get_string as getString} from 'core/str';
+import Modal from 'core/modal';
+import ModalFactory from 'core/modal_factory';
+import Pending from 'core/pending';
+import {addIconToContainerRemoveOnCompletion} from 'core/loadingicon';
+import {getRelevantContextId} from './options';
+import {call as fetchMany} from 'core/ajax';
+import Notification from 'core/notification';
+import Fragment from 'core/fragment';
+
+/**
+ * Manages the embed question dialog.
+ *
+ * @module tiny_embedquestion/dialogue_manager
+ * @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 */
+ editor = null;
+
+ /** @property {Object} current display dialog */
+ currentModal = null;
+
+ /**
+ * Dialog constructor.
+ *
+ * @constructor
+ * @param {Object} editor current editor instance.
+ */
+ constructor(editor) {
+ this.editor = editor;
+ }
+
+ /**
+ * Displays a modal dialogue for managing embed question.
+ *
+ * @async
+ */
+ displayDialogue = async() => {
+ if (typeof Modal.create !== "undefined") {
+ this.currentModal = await Modal.create({
+ large: true,
+ title: getString('pluginname', 'tiny_embedquestion'),
+ body: '',
+ show: true,
+ removeOnClose: true
+ });
+ } else {
+ // TODO Need to be remove after we no longer support 4.2 and below.
+ this.currentModal = await ModalFactory.create({
+ title: getString('pluginname', 'tiny_embedquestion'),
+ body: '',
+ large: true,
+ removeOnClose: true
+ });
+ this.currentModal.show();
+ }
+
+ const pendingModalReady = new Pending('tiny_embedquestion/displayDialogue');
+ const body = this.currentModal.getBody()[0];
+ addIconToContainerRemoveOnCompletion(
+ body, pendingModalReady
+ );
+
+ let existingCode = this.getEmbedCodeFromTextSelection(this.editor);
+ if (existingCode) {
+ existingCode = existingCode.embedCode;
+ }
+ const dialogManager = this;
+ Fragment.loadFragment('tiny_embedquestion', 'questionselector', getRelevantContextId(this.editor),
+ {contextId: getRelevantContextId(this.editor), embedCode: existingCode}).then(function(html, js) {
+
+ Templates.replaceNodeContents(body, html, js);
+ body.querySelector('#embedqform #id_submitbutton').addEventListener('click', dialogManager.getEmbedCode);
+ pendingModalReady.resolve();
+ return dialogManager.currentModal;
+ }).catch(Notification.exception);
+ };
+
+ /**
+ * Handler for when the form button is clicked.
+ * Make an AJAX request to the server to get the embed code.
+ *
+ * @param {Event} e - the click event.
+ */
+ getEmbedCode = (e) => {
+ e.preventDefault();
+ const iframeDescription = document.getElementById('id_iframedescription').value;
+ const questionIdnumber = document.getElementById('id_questionidnumber').value;
+ const dialogManager = this;
+ // Required value of questionidnumber.
+ // Note that the form also validates this, and deals with displaying a message to the user.
+ if (!questionIdnumber) {
+ return;
+ }
+
+ // Validate iframedescription.
+ // If it is present, then it must have at least 3 characters and a maximum of 100 characters.
+ // (It can be left blank to get the default description.)
+ // Note that the form also validates this, and deals with displaying a message to the user.
+ if (iframeDescription.length && (iframeDescription.length < 3 || iframeDescription.length > 100)) {
+ return;
+ }
+
+ dialogManager.getEmbedCodeCall(iframeDescription, questionIdnumber).then(function(embedCode) {
+ dialogManager.insertEmbedCode(embedCode);
+ return dialogManager;
+ }).catch(Notification.exception);
+ };
+
+ /**
+ * 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.
+ * @returns {Promise}
+ */
+ getEmbedCodeCall = (iframeDescription, questionIdnumber) => {
+ return fetchMany([{
+ 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: document.getElementById('id_behaviour')?.value || '',
+ maxmark: document.getElementById('id_maxmark')?.value || '',
+ variant: document.getElementById('id_variant')?.value || '',
+ correctness: document.getElementById('id_correctness')?.value || '',
+ marks: document.getElementById('id_marks')?.value || '',
+ markdp: document.getElementById('id_markdp')?.value || '',
+ feedback: document.getElementById('id_feedback')?.value || '',
+ generalfeedback: document.getElementById('id_generalfeedback')?.value || '',
+ rightanswer: document.getElementById('id_rightanswer')?.value || '',
+ history: document.getElementById('id_history')?.value || '',
+ forcedlanguage: document.getElementById('id_forcedlanguage')?.value || ''
+ }
+ }])[0];
+ };
+
+ /**
+ * Handles when we get the embed code from the AJAX request.
+ *
+ * @param {String} embedCode - the embed code to insert.
+ */
+ insertEmbedCode = (embedCode) => {
+ const existingCode = this.getEmbedCodeFromTextSelection(this.editor);
+ if (existingCode) {
+ // Replace the existing code.
+ const parent = this.editor.selection.getNode();
+ const text = parent.textContent;
+ parent.textContent = text.slice(0, existingCode.start) +
+ embedCode + text.slice(existingCode.end);
+ } else {
+ this.editor.insertContent(embedCode);
+ }
+ this.currentModal.destroy();
+ };
+
+ /**
+ * Get the embed code of the current selected text,
+ *
+ * @param {TinyMCE} editor
+ * @returns {boolean|Object}
+ * return false if we can't find the match pattern.
+ * return Object {start: start position of the text, end: end position of the text, embedCode: embed code of the string}
+ */
+ getEmbedCodeFromTextSelection = (editor) => {
+
+ // Find the embed code in the surrounding text.
+ const selection = editor.selection.getSel(),
+ selectedNode = editor.selection.getNode(),
+ pattern = /\{Q\{(?:(?!\}Q\}).)*\}Q\}/g;
+ let text,
+ returnValue = false,
+ patternMatches;
+
+ if (!selection) {
+ return false;
+ }
+ const range = selection.rangeCount ? selection.getRangeAt(0) : null;
+ if (!range) {
+ return false;
+ }
+ text = selectedNode.textContent;
+ patternMatches = text.match(pattern);
+
+ if (!patternMatches || !patternMatches.length) {
+ return false;
+ }
+ // This pattern matches at least once. See if this pattern matches our current position.
+ // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent
+ // searches which is the required behaviour of this function.
+ for (let i = 0; i < patternMatches.length; ++i) {
+ let startIndex = 0;
+ while (text.indexOf(patternMatches[i], startIndex) !== -1) {
+ // Determine whether the cursor is in the current occurrence of this string.
+ // Note: We do not support a selection exceeding the bounds of an equation.
+ const start = text.indexOf(patternMatches[i], startIndex),
+ end = start + patternMatches[i].length,
+ startMatches = (selection.anchorOffset >= start && selection.anchorOffset < end),
+ endMatches = (selection.focusOffset <= end && selection.focusOffset > start),
+ reserveStartMatches = (selection.anchorOffset <= end && selection.anchorOffset > start),
+ reserveEndMatches = (selection.focusOffset >= start && selection.focusOffset < end);
+ if ((startMatches && endMatches) || (reserveStartMatches && reserveEndMatches)) {
+ // Save all data for later.
+ returnValue = {
+ // Outer match data.
+ start: start,
+ end: end,
+ embedCode: patternMatches[i]
+ };
+
+ // This breaks out the loop
+ break;
+ }
+
+ // Update the startIndex to match the end of the current match so that we can continue hunting
+ // for further matches.
+ startIndex = end;
+ }
+ }
+ return returnValue;
+ };
+};
diff --git a/amd/src/options.js b/amd/src/options.js
new file mode 100644
index 0000000..183da2a
--- /dev/null
+++ b/amd/src/options.js
@@ -0,0 +1,47 @@
+// This file is part of Moodle - https://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * Option helper for the Moodle tiny_embedquestion plugin.
+ *
+ * @module tiny_embedquestion/options
+ * @copyright 2024 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import {getPluginOptionName} from 'editor_tiny/options';
+import {pluginName} from './common';
+const relevantContextId = getPluginOptionName(pluginName, 'relevantContextId');
+
+/**
+ * Register the options for the Tiny Equation plugin.
+ *
+ * @param {TinyMCE} editor
+ */
+export const register = (editor) => {
+ const registerOption = editor.options.register;
+
+ registerOption(relevantContextId, {
+ processor: 'number',
+ });
+
+};
+
+/**
+ * Get the context id for the Tiny embed question plugin.
+ *
+ * @param {TinyMCE} editor
+ * @returns {Number} - context id
+ */
+export const getRelevantContextId = (editor) => editor.options.get(relevantContextId);
diff --git a/amd/src/plugin.js b/amd/src/plugin.js
index 20eed13..938ae24 100644
--- a/amd/src/plugin.js
+++ b/amd/src/plugin.js
@@ -14,35 +14,46 @@
// along with Moodle. If not, see .
/**
- * Tiny tiny_embedquestion for Moodle.
+ * Embed question plugin for TinyMCE.
*
- * @module plugintype_pluginname/plugin
- * @copyright 2023 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @module tiny_embedquestion
+ * @copyright 2024 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getTinyMCE} from 'editor_tiny/loader';
import {getPluginMetadata} from 'editor_tiny/utils';
-
import {component, pluginName} from './common';
+import {getSetup as getCommandSetup} from './commands';
+import * as Configuration from './configuration';
+import * as Options from "./options";
// Setup the tiny_embedquestion Plugin.
+// eslint-disable-next-line no-async-promise-executor
export default new Promise(async(resolve) => {
// Note: The PluginManager.add function does not support asynchronous configuration.
// Perform any asynchronous configuration here, and then call the PluginManager.add function.
const [
tinyMCE,
pluginMetadata,
+ setupCommands,
] = await Promise.all([
getTinyMCE(),
getPluginMetadata(component, pluginName),
+ getCommandSetup(),
]);
// Reminder: Any asynchronous code must be run before this point.
tinyMCE.PluginManager.add(pluginName, (editor) => {
+
+ // Register any options that your plugin has
+ Options.register(editor);
+ // Setup any commands such as buttons, menu items, and so on.
+ setupCommands(editor);
+
// Return the pluginMetadata object. This is used by TinyMCE to display a help link for your plugin.
return pluginMetadata;
});
- resolve(pluginName);
+ resolve([pluginName, Configuration]);
});
diff --git a/changes.md b/changes.md
new file mode 100644
index 0000000..4f61683
--- /dev/null
+++ b/changes.md
@@ -0,0 +1,3 @@
+## Changes in 1.0
+
+* Initial release.
diff --git a/classes/plugininfo.php b/classes/plugininfo.php
index 99ed05f..ad9f916 100644
--- a/classes/plugininfo.php
+++ b/classes/plugininfo.php
@@ -18,13 +18,44 @@
use context;
use editor_tiny\plugin;
+use editor_tiny\plugin_with_buttons;
+use context_course;
+use editor_tiny\plugin_with_configuration;
/**
- * Tiny Tiny plugin for Moodle.
+ * Tiny Embed question plugin for Moodle.
*
- * @package tiny_embedquestion
- * @copyright 2023 The Open University
- * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package tiny_embedquestion
+ * @copyright 2024 The Open University
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class plugininfo extends plugin {
+class plugininfo extends plugin implements plugin_with_buttons, plugin_with_configuration {
+
+ public static function is_enabled(context $context, array $options, array $fpoptions,
+ ?\editor_tiny\editor $editor = null): bool {
+ // Users must have permission to embed content.
+ // Get the course context, this is the only context we use.
+ $context = context_course::instance(\filter_embedquestion\utils::get_relevant_courseid($context));
+ return has_any_capability(['moodle/question:useall', 'moodle/question:usemine'], $context);
+ }
+
+ public static function get_available_buttons(): array {
+ return [
+ 'tiny_embedquestion',
+ ];
+ }
+
+ public static function get_plugin_configuration_for_context(
+ context $context,
+ array $options,
+ array $fpoptions,
+ ?\editor_tiny\editor $editor = null
+ ): array {
+ // Get the course context, this is the only context we use.
+ $context = \context_course::instance(
+ \filter_embedquestion\utils::get_relevant_courseid($context));
+ return [
+ 'relevantContextId' => $context->id,
+ ];
+ }
}
diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php
index 526b013..0ee10b6 100644
--- a/classes/privacy/provider.php
+++ b/classes/privacy/provider.php
@@ -1,5 +1,5 @@
.
+// along with Moodle. If not, see .
namespace tiny_embedquestion\privacy;
-use core_privacy\local\metadata\null_provider;
-
/**
- * Privacy Subsystem implementation for tiny_embedquestion.
+ * Privacy Subsystem implementation for the Tiny embed question for TinyMCE.
*
- * @package tiny_embedquestion
- * @copyright 2023 The Open University
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package tiny_embedquestion
+ * @copyright 2024 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class provider implements null_provider {
- public static function get_reason() : string {
+class provider implements \core_privacy\local\metadata\null_provider {
+
+ public static function get_reason(): string {
return 'privacy:metadata';
}
}
diff --git a/lang/en/tiny_embedquestion.php b/lang/en/tiny_embedquestion.php
index 6aabcec..0e9b0b6 100644
--- a/lang/en/tiny_embedquestion.php
+++ b/lang/en/tiny_embedquestion.php
@@ -17,15 +17,12 @@
/**
* Plugin strings are defined here.
*
- * @package tiny_embedquestion
- * @category string
- * @copyright 2023 The Open University
- * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package tiny_embedquestion
+ * @copyright 2024 The Open University
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-defined('MOODLE_INTERNAL') || die();
-
$string['embedqcode'] = 'Embed question code';
$string['loading'] = 'Loading...';
$string['pluginname'] = 'Embed question';
-$string['privacy:metadata'] = 'The TinyMCE embed question plugin does not store any personal data.';
+$string['privacy:metadata'] = 'The Tiny Embed question plugin does not store any personal data.';
diff --git a/lib.php b/lib.php
new file mode 100644
index 0000000..ce3c301
--- /dev/null
+++ b/lib.php
@@ -0,0 +1,58 @@
+.
+
+/**
+ * Tiny text editor library file.
+ *
+ * @package tiny_embedquestion
+ * @copyright 2024 The Open University
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+use filter_embedquestion\form\embed_options_form;
+
+/**
+ * Server side controller used by core Fragment javascript to return a moodle form html.
+ * This is used for the question selection form displayed in the embedquestion atto dialogue.
+ * Reference https://docs.moodle.org/dev/Fragment.
+ * Based on similar function in mod/assign/lib.php.
+ *
+ * @param array $args Must contain contextid
+ * @return string
+ */
+function tiny_embedquestion_output_fragment_questionselector(array $args): string {
+ 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]);
+
+ $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) {
+ $toform['questionidnumber'] = $embedid->questionidnumber;
+ $toform['categoryidnumber'] = $embedid->categoryidnumber;
+ // Decode iframedescription data to form.
+ if (isset($toform['iframedescription'])) {
+ $toform['iframedescription'] = base64_decode($toform['iframedescription']);
+ }
+ $mform->set_data($toform);
+ }
+ }
+
+ return $mform->render();
+}
diff --git a/pix/icon.svg b/pix/icon.svg
new file mode 100644
index 0000000..7ad28ce
--- /dev/null
+++ b/pix/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/behat/embedquestion.feature b/tests/behat/embedquestion.feature
new file mode 100644
index 0000000..de08e78
--- /dev/null
+++ b/tests/behat/embedquestion.feature
@@ -0,0 +1,39 @@
+@ou @ou_vle @editor @tiny @editor_tiny @tiny_embedquestion @filter_embedquestion
+Feature: Embed question in the Tiny editor
+ In order to encourage students interacting with ativity and learning from it
+ As a teacher
+ I need to insert interactive questions in my content
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher | Terry | Teacher | teacher@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher | C1 | editingteacher |
+ And the following "question categories" exist:
+ | contextlevel | reference | name | idnumber |
+ | Course | C1 | Test questions | embed |
+ And the following "questions" exist:
+ | questioncategory | qtype | name | idnumber |
+ | Test questions | truefalse | First question | test1 |
+ And the "embedquestion" filter is "on"
+
+ @javascript
+ Scenario: Test using 'Embed question' button
+ 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"
+ 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 "Embed question" "button"
+ 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 "Embed question" "dialogue"
+ And I switch to the "Description" TinyMCE editor iframe
+ Then I should see "{Q{embed/test1|"
+ And I should see "}Q}Test page description"
diff --git a/version.php b/version.php
index 408f627..0c12436 100644
--- a/version.php
+++ b/version.php
@@ -17,19 +17,18 @@
/**
* Plugin version and other meta-data are defined here.
*
- * @package tiny_embedquestion
- * @copyright 2023 The Open University
- * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package tiny_embedquestion
+ * @copyright 2024 The Open University
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'tiny_embedquestion';
-$plugin->release = '0.1.0';
-$plugin->version = 2023091800;
-$plugin->requires = 2022112800;
-$plugin->maturity = MATURITY_ALPHA;
-
-$plugin->dependencies = ['filter_embedquestion' => 2022032900];
-
-$plugin->outestssufficient = true;
+$plugin->release = '1.0';
+$plugin->version = 2024011100;
+$plugin->requires = 2020061500;
+$plugin->maturity = MATURITY_STABLE;
+$plugin->dependencies = [
+ 'filter_embedquestion' => 2022032900,
+];