From 37c3dffc0b716312fb446c21a04ca9016186cefd Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Fri, 11 Oct 2024 22:02:00 +0200 Subject: [PATCH 1/5] Support for qtype_multianswerrgx when installed. --- .github/workflows/moodle-plugin-ci.yml | 18 ++ amd/build/commands.min.js | 2 +- amd/build/commands.min.js.map | 2 +- amd/build/options.min.js | 11 + amd/build/options.min.js.map | 1 + amd/build/plugin.min.js | 4 +- amd/build/plugin.min.js.map | 2 +- amd/build/ui.min.js | 4 +- amd/build/ui.min.js.map | 2 +- amd/src/commands.js | 4 +- amd/src/options.js | 41 ++++ amd/src/plugin.js | 4 + amd/src/ui.js | 325 ++++++++++++++----------- classes/plugininfo.php | 42 +++- 14 files changed, 314 insertions(+), 148 deletions(-) create mode 100644 amd/build/options.min.js create mode 100644 amd/build/options.min.js.map create mode 100644 amd/src/options.js diff --git a/.github/workflows/moodle-plugin-ci.yml b/.github/workflows/moodle-plugin-ci.yml index ed94414..4a04c9d 100644 --- a/.github/workflows/moodle-plugin-ci.yml +++ b/.github/workflows/moodle-plugin-ci.yml @@ -30,6 +30,24 @@ jobs: fail-fast: false matrix: include: + - php: '8.1' + moodle-branch: 'MOODLE_405_STABLE' + database: pgsql + - php: '8.2' + moodle-branch: 'MOODLE_405_STABLE' + database: pgsql + - php: '8.3' + moodle-branch: 'MOODLE_405_STABLE' + database: pgsql + - php: '8.1' + moodle-branch: 'MOODLE_405_STABLE' + database: mariadb + - php: '8.2' + moodle-branch: 'MOODLE_405_STABLE' + database: mariadb + - php: '8.3' + moodle-branch: 'MOODLE_405_STABLE' + database: mariadb - php: '8.1' moodle-branch: 'MOODLE_404_STABLE' database: pgsql diff --git a/amd/build/commands.min.js b/amd/build/commands.min.js index cc56a8c..37e3c29 100644 --- a/amd/build/commands.min.js +++ b/amd/build/commands.min.js @@ -1,3 +1,3 @@ -define("tiny_cloze/commands",["exports","editor_tiny/utils","core/str","./common","./ui"],(function(_exports,_utils,_str,_common,_ui){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[clozeButtonText,buttonImage]=await Promise.all([(0,_str.get_string)("pluginname",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{document.querySelector("body#page-question-type-multianswer form, body#page-question-type-multianswerwiris form")&&-1!==editor.id.indexOf("questiontext")&&(editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addToggleButton(_common.clozeeditButtonName,{icon:_common.icon,tooltip:clozeButtonText,onAction:()=>(0,_ui.displayDialogue)(),onSetup:api=>{editor.on("click",(()=>{api.setActive(!1!==(0,_ui.resolveSubquestion)())}))}}),editor.ui.registry.addMenuItem(_common.clozeeditButtonName,{icon:_common.icon,text:clozeButtonText,onAction:()=>(0,_ui.displayDialogue)()}),editor.on("init",(()=>(0,_ui.onInit)(editor))),editor.on("BeforeGetContent",(format=>(0,_ui.onBeforeGetContent)(format))),editor.on("submit",(()=>(0,_ui.onSubmit)())),editor.on("dblclick",(e=>(0,_ui.displayDialogueForEdit)(e.target))))}}})); +define("tiny_cloze/commands",["exports","editor_tiny/utils","core/str","./common","./ui"],(function(_exports,_utils,_str,_common,_ui){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[clozeButtonText,buttonImage]=await Promise.all([(0,_str.get_string)("pluginname",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{document.querySelector("body#page-question-type-multianswer form, body#page-question-type-multianswerwiris form,body#page-question-type-multianswerrgx form")&&-1!==editor.id.indexOf("questiontext")&&(editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addToggleButton(_common.clozeeditButtonName,{icon:_common.icon,tooltip:clozeButtonText,onAction:()=>(0,_ui.displayDialogue)(),onSetup:api=>{editor.on("click",(()=>{api.setActive(!1!==(0,_ui.resolveSubquestion)())}))}}),editor.ui.registry.addMenuItem(_common.clozeeditButtonName,{icon:_common.icon,text:clozeButtonText,onAction:()=>(0,_ui.displayDialogue)()}),editor.on("init",(()=>(0,_ui.onInit)(editor))),editor.on("BeforeGetContent",(format=>(0,_ui.onBeforeGetContent)(format))),editor.on("submit",(()=>(0,_ui.onSubmit)())),editor.on("dblclick",(e=>(0,_ui.displayDialogueForEdit)(e.target))))}}})); //# 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 254e365..a06b0ba 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_cloze plugin.\n *\n * @module tiny_cloze/commands\n * @copyright 2023 MoodleDACH\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 clozeeditButtonName,\n icon,\n} from './common';\nimport {displayDialogue, displayDialogueForEdit, resolveSubquestion, onInit, onBeforeGetContent, onSubmit} from './ui';\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 clozeButtonText,\n buttonImage,\n ] = await Promise.all([\n getString('pluginname', component),\n getButtonImage('icon', component),\n ]);\n\n return (editor) => {\n // Check whether we are editing a question.\n const body = document.querySelector('body#page-question-type-multianswer form, ' +\n 'body#page-question-type-multianswerwiris form');\n // And if the editor is used on the question text.\n if (!body || editor.id.indexOf('questiontext') === -1) {\n return;\n }\n // Only if both conditions are valid, then continue setting up the plugin.\n\n // Register the Moodle SVG as an icon suitable for use as a TinyMCE toolbar button.\n editor.ui.registry.addIcon(icon, buttonImage.html);\n\n // Register the clozeedit Toolbar Button.\n editor.ui.registry.addToggleButton(clozeeditButtonName, {\n icon,\n tooltip: clozeButtonText,\n onAction: () => displayDialogue(),\n onSetup: (api) => {\n editor.on('click', () => {\n api.setActive(resolveSubquestion() !== false);\n });\n }\n });\n\n // Register the menu item.\n editor.ui.registry.addMenuItem(clozeeditButtonName, {\n icon,\n text: clozeButtonText,\n onAction: () => displayDialogue(),\n });\n\n editor.on('init', () => onInit(editor));\n editor.on('BeforeGetContent', format => onBeforeGetContent(format));\n editor.on('submit', () => onSubmit());\n editor.on('dblclick', (e) => displayDialogueForEdit(e.target));\n };\n};\n"],"names":["async","clozeButtonText","buttonImage","Promise","all","component","editor","document","querySelector","id","indexOf","ui","registry","addIcon","icon","html","addToggleButton","clozeeditButtonName","tooltip","onAction","onSetup","api","on","setActive","addMenuItem","text","format","e","target"],"mappings":"yOAwCwBA,gBAEhBC,gBACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,aAAcC,oBACxB,yBAAe,OAAQA,4BAGnBC,SAESC,SAASC,cAAc,6FAGgB,IAAvCF,OAAOG,GAAGC,QAAQ,kBAM/BJ,OAAOK,GAAGC,SAASC,QAAQC,aAAMZ,YAAYa,MAG7CT,OAAOK,GAAGC,SAASI,gBAAgBC,4BAAqB,CACpDH,KAAAA,aACAI,QAASjB,gBACTkB,SAAU,KAAM,yBAChBC,QAAUC,MACNf,OAAOgB,GAAG,SAAS,KACdD,IAAIE,WAAmC,KAAzB,mCAM3BjB,OAAOK,GAAGC,SAASY,YAAYP,4BAAqB,CAChDH,KAAAA,aACAW,KAAMxB,gBACNkB,SAAU,KAAM,2BAGpBb,OAAOgB,GAAG,QAAQ,KAAM,cAAOhB,UAC/BA,OAAOgB,GAAG,oBAAoBI,SAAU,0BAAmBA,UAC3DpB,OAAOgB,GAAG,UAAU,KAAM,oBAC1BhB,OAAOgB,GAAG,YAAaK,IAAM,8BAAuBA,EAAEC"} \ 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_cloze plugin.\n *\n * @module tiny_cloze/commands\n * @copyright 2023 MoodleDACH\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 clozeeditButtonName,\n icon,\n} from './common';\nimport {displayDialogue, displayDialogueForEdit, resolveSubquestion, onInit, onBeforeGetContent, onSubmit} from './ui';\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 clozeButtonText,\n buttonImage,\n ] = await Promise.all([\n getString('pluginname', component),\n getButtonImage('icon', component),\n ]);\n\n return (editor) => {\n // Check whether we are editing a question.\n const body = document.querySelector('body#page-question-type-multianswer form, ' +\n 'body#page-question-type-multianswerwiris form,' +\n 'body#page-question-type-multianswerrgx form'\n );\n // And if the editor is used on the question text.\n if (!body || editor.id.indexOf('questiontext') === -1) {\n return;\n }\n // Only if both conditions are valid, then continue setting up the plugin.\n\n // Register the Moodle SVG as an icon suitable for use as a TinyMCE toolbar button.\n editor.ui.registry.addIcon(icon, buttonImage.html);\n\n // Register the clozeedit Toolbar Button.\n editor.ui.registry.addToggleButton(clozeeditButtonName, {\n icon,\n tooltip: clozeButtonText,\n onAction: () => displayDialogue(),\n onSetup: (api) => {\n editor.on('click', () => {\n api.setActive(resolveSubquestion() !== false);\n });\n }\n });\n\n // Register the menu item.\n editor.ui.registry.addMenuItem(clozeeditButtonName, {\n icon,\n text: clozeButtonText,\n onAction: () => displayDialogue(),\n });\n\n editor.on('init', () => onInit(editor));\n editor.on('BeforeGetContent', format => onBeforeGetContent(format));\n editor.on('submit', () => onSubmit());\n editor.on('dblclick', (e) => displayDialogueForEdit(e.target));\n };\n};\n"],"names":["async","clozeButtonText","buttonImage","Promise","all","component","editor","document","querySelector","id","indexOf","ui","registry","addIcon","icon","html","addToggleButton","clozeeditButtonName","tooltip","onAction","onSetup","api","on","setActive","addMenuItem","text","format","e","target"],"mappings":"yOAwCwBA,gBAEhBC,gBACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,aAAcC,oBACxB,yBAAe,OAAQA,4BAGnBC,SAESC,SAASC,cAAc,yIAKgB,IAAvCF,OAAOG,GAAGC,QAAQ,kBAM/BJ,OAAOK,GAAGC,SAASC,QAAQC,aAAMZ,YAAYa,MAG7CT,OAAOK,GAAGC,SAASI,gBAAgBC,4BAAqB,CACpDH,KAAAA,aACAI,QAASjB,gBACTkB,SAAU,KAAM,yBAChBC,QAAUC,MACNf,OAAOgB,GAAG,SAAS,KACdD,IAAIE,WAAmC,KAAzB,mCAM3BjB,OAAOK,GAAGC,SAASY,YAAYP,4BAAqB,CAChDH,KAAAA,aACAW,KAAMxB,gBACNkB,SAAU,KAAM,2BAGpBb,OAAOgB,GAAG,QAAQ,KAAM,cAAOhB,UAC/BA,OAAOgB,GAAG,oBAAoBI,SAAU,0BAAmBA,UAC3DpB,OAAOgB,GAAG,UAAU,KAAM,oBAC1BhB,OAAOgB,GAAG,YAAaK,IAAM,8BAAuBA,EAAEC"} \ 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..f7743df --- /dev/null +++ b/amd/build/options.min.js @@ -0,0 +1,11 @@ +define("tiny_cloze/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.hasQtypeMultianswerrgx=void 0; +/** + * Options helper for tiny_cloze plugin. + * + * @module tiny_cloze + * @copyright 2024 Stephan Robotta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const multianswerrgx=(0,_options.getPluginOptionName)(_common.pluginName,"multianswerrgx");_exports.register=editor=>{editor.options.register(multianswerrgx,{processor:"boolean",default:!1})};_exports.hasQtypeMultianswerrgx=editor=>editor.options.get(multianswerrgx)})); + +//# 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..fe396e5 --- /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 - 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\n/**\n * Options helper for tiny_cloze plugin.\n *\n * @module tiny_cloze\n * @copyright 2024 Stephan Robotta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst multianswerrgx = getPluginOptionName(pluginName, 'multianswerrgx');\n\n/**\n * Register the options for the Tiny Cloze question plugin.\n *\n * @param {tinymce.Editor} editor\n */\nexport const register = (editor) => {\n editor.options.register(multianswerrgx, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\nexport const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx);"],"names":["multianswerrgx","pluginName","editor","options","register","processor","get"],"mappings":";;;;;;;;MA0BMA,gBAAiB,gCAAoBC,mBAAY,oCAO9BC,SACrBA,OAAOC,QAAQC,SAASJ,eAAgB,CACpCK,UAAW,mBACA,qCAIoBH,QAAWA,OAAOC,QAAQG,IAAIN"} \ No newline at end of file diff --git a/amd/build/plugin.min.js b/amd/build/plugin.min.js index 8cc17d7..eefffd3 100644 --- a/amd/build/plugin.min.js +++ b/amd/build/plugin.min.js @@ -1,10 +1,10 @@ -define("tiny_cloze/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration"],(function(_exports,_loader,_utils,_common,_commands,Configuration){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.default=void 0,Configuration=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} +define("tiny_cloze/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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Configuration=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} /** * Tiny tiny_cloze for Moodle. * * @module tiny_cloze/plugin * @copyright 2023 MoodleDACH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */(Configuration);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=>(setupCommands(editor),pluginMetadata))),resolve([_common.pluginName,Configuration])}));return _exports.default=_default,_exports.default})); + */(Configuration);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=>((0,_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 index c6b5aa4..3392139 100644 --- a/amd/build/plugin.min.js.map +++ b/amd/build/plugin.min.js.map @@ -1 +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 * Tiny tiny_cloze for Moodle.\n *\n * @module tiny_cloze/plugin\n * @copyright 2023 MoodleDACH\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';\n\nimport {component, pluginName} from './common';\nimport {getSetup as getCommandSetup} from './commands';\nimport * as Configuration from './configuration';\n\n// Setup the tiny_cloze 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 // 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","resolve","Configuration"],"mappings":";;;;;;;kCAgCe,IAAIA,SAAQC,MAAAA,gBAInBC,QACAC,eACAC,qBACMJ,QAAQK,IAAI,EAClB,yBACA,4BAAkBC,kBAAWC,qBAC7B,0BAIJL,QAAQM,cAAcC,IAAIF,oBAAaG,SAEnCN,cAAcM,QAGPP,kBAGXQ,QAAQ,CAACJ,mBAAYK"} \ No newline at end of file +{"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 * Tiny tiny_cloze for Moodle.\n *\n * @module tiny_cloze/plugin\n * @copyright 2023 MoodleDACH\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';\n\nimport {component, pluginName} from './common';\nimport {getSetup as getCommandSetup} from './commands';\nimport * as Configuration from './configuration';\nimport {register as registerOptions} from './options';\n\n// Setup the tiny_cloze 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 // Register options.\n registerOptions(editor);\n\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","resolve","Configuration"],"mappings":";;;;;;;kCAiCe,IAAIA,SAAQC,MAAAA,gBAInBC,QACAC,eACAC,qBACMJ,QAAQK,IAAI,EAClB,yBACA,4BAAkBC,kBAAWC,qBAC7B,0BAIJL,QAAQM,cAAcC,IAAIF,oBAAaG,+BAEnBA,QAGhBN,cAAcM,QAGPP,kBAGXQ,QAAQ,CAACJ,mBAAYK"} \ No newline at end of file diff --git a/amd/build/ui.min.js b/amd/build/ui.min.js index ee3c082..84093bf 100644 --- a/amd/build/ui.min.js +++ b/amd/build/ui.min.js @@ -1,10 +1,10 @@ -define("tiny_cloze/ui",["exports","core/modal_events","core/modal","core/modal_factory","core/mustache","core/str","./common"],(function(_exports,_modal_events,_modal2,_modal_factory,_mustache,_str,_common){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +define("tiny_cloze/ui",["exports","core/modal_events","core/modal","core/modal_factory","core/mustache","core/str","./common","./options"],(function(_exports,_modal_events,_modal2,_modal_factory,_mustache,_str,_common,_options){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Plugin tiny_cloze for TinyMCE v6 in Moodle. * * @module tiny_cloze/ui * @copyright 2023 MoodleDACH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.resolveSubquestion=_exports.onSubmit=_exports.onInit=_exports.onBeforeGetContent=_exports.displayDialogueForEdit=_exports.displayDialogue=void 0,_modal_events=_interopRequireDefault(_modal_events),_modal2=_interopRequireDefault(_modal2),_modal_factory=_interopRequireDefault(_modal_factory),_mustache=_interopRequireDefault(_mustache);const isNull=a=>null==a,strdecode=t=>String(t).replace(/\\(#|\}|~)/g,"$1"),strencode=t=>String(t).replace(/(#|\}|~)/g,"\\$1"),indexOfNode=(list,node)=>{for(let i=0;i{const attrSel=' selected="selected"';let isSel="="===s?attrSel:"",html='");return FRACTIONS.forEach((item=>{isSel=item.value.toString()===s?attrSel:"",html+='")})),isSel=""!==s&&-1===html.indexOf(attrSel)?attrSel:"",html+='"),html},isCustomGrade=s=>{if("="===s||""===s)return!1;let found=!1;return FRACTIONS.forEach((item=>{item.value.toString()===s&&(found=!0)})),!found},markerClass="cloze-question-marker",markerSpan='',reQtype=/\{([0-9]*):(MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?):(.*?)(?

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

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

{{STR.chooseqtypetoadd}}

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

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

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

{{STR.chooseqtypetoadd}}

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

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

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

{{STR.chooseqtypetoadd}}

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

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

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

{{STR.chooseqtypetoadd}}

' +\n '
' +\n '
' +\n '{{#types}}' +\n '
' +\n '' +\n '
' +\n '{{/types}}
' +\n '
',\n FOOTER: '' +\n '',\n};\nconst FRACTIONS = [\n {value: 100},\n {value: 50},\n {value: 0},\n];\n\n// Language strings used in the modal dialogue.\nconst STR = {};\n\n/**\n * The editor instance that is injected via the onInit() function.\n *\n * @type {tinymce.Editor}\n * @private\n */\nlet _editor = null;\n\n/**\n * A reference to the currently open form.\n *\n * @param _form\n * @type {Node}\n * @private\n */\nlet _form = null;\n\n/**\n * An array containing the current answers options\n *\n * @param _answerdata\n * @type {Array}\n * @private\n */\nlet _answerdata = [];\n\n/**\n * The sub question type to be edited\n *\n * @param _qtype\n * @type {string|null}\n * @private\n */\nlet _qtype = null;\n\n/**\n * Remember the pos of the selected node.\n * @type {number}\n * @private\n */\nlet _selectedOffset = -1;\n\n/**\n * The maximum marks for the sub question\n *\n * @param _marks\n * @type {Integer}\n * @private\n */\nlet _marks = 1;\n\n/**\n * The modal dialogue to be displayed when designing the cloze question types.\n * @type {Modal|null}\n */\nlet _modal = null;\n\n/**\n * If its a normal selection of text, use it for the first answer field.\n * @type {string|null}\n */\nlet _firstAnswer = null;\n\n/**\n * Inject the editor instance and add markers to the cloze question texts.\n * @param {tinymce.Editor} ed\n */\nconst onInit = function(ed) {\n _editor = ed; // The current editor instance.\n // Add the marker spans.\n _addMarkers();\n // And get the language strings.\n _getStr(ed);\n};\n\n/**\n * Regex to recognize the question string in the text e.g. {1:NUMERICAL:...} or {:MULTICHOICE:...}\n * @param {tinymce.Editor} editor\n * @return {RegExp}\n * @private\n */\nconst _getRegexQtype = (editor) => {\n // eslint-disable-next-line max-len\n const baseQtypes = 'MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?';\n const extQtypes = hasQtypeMultianswerrgx(editor) ? '|REGEXP(_C)?|RXC?' : '';\n return new RegExp('\\\\{([0-9]*):(' + baseQtypes + extQtypes + '):(.*?)(? {\n let strToFetch = [\n {key: 'answer', component: 'question'},\n {key: 'chooseqtypetoadd', component: 'question'},\n {key: 'defaultmark', component: 'question'},\n {key: 'feedback', component: 'question'},\n {key: 'correct', component: 'question'},\n {key: 'incorrect', component: 'question'},\n {key: 'addmoreanswerblanks', component: 'qtype_calculated'},\n {key: 'delete', component: 'core'},\n {key: 'up', component: 'core'},\n {key: 'down', component: 'core'},\n {key: 'tolerance', component: 'qtype_calculated'},\n {key: 'grade', component: 'grades'},\n {key: 'caseno', component: 'mod_quiz'},\n {key: 'caseyes', component: 'mod_quiz'},\n {key: 'answersingleno', component: 'qtype_multichoice'},\n {key: 'answersingleyes', component: 'qtype_multichoice'},\n {key: 'layoutselectinline', component: 'qtype_multianswer'},\n {key: 'layouthorizontal', component: 'qtype_multianswer'},\n {key: 'layoutvertical', component: 'qtype_multianswer'},\n {key: 'shufflewithin', component: 'mod_quiz'},\n {key: 'layoutmultiple_horizontal', component: 'qtype_multianswer'},\n {key: 'layoutmultiple_vertical', component: 'qtype_multianswer'},\n {key: 'pluginnamesummary', component: 'qtype_multichoice'},\n {key: 'pluginnamesummary', component: 'qtype_shortanswer'},\n {key: 'pluginnamesummary', component: 'qtype_numerical'},\n {key: 'multichoice', component},\n {key: 'multiresponse', component},\n {key: 'numerical', component: 'mod_quiz'},\n {key: 'shortanswer', component: 'mod_quiz'},\n {key: 'cancel', component: 'core'},\n {key: 'select', component},\n {key: 'insert', component},\n {key: 'pluginname', component},\n {key: 'customgrade', component},\n {key: 'err_custom_rate', component},\n {key: 'err_empty_answer', component},\n {key: 'err_none_correct', component},\n {key: 'err_not_numeric', component},\n ];\n let langKeys = [\n 'answer',\n 'chooseqtypetoadd',\n 'defaultmark',\n 'feedback',\n 'correct',\n 'incorrect',\n 'addmoreanswerblanks',\n 'delete',\n 'up',\n 'down',\n 'tolerance',\n 'grade',\n 'caseno',\n 'caseyes',\n 'singleno',\n 'singleyes',\n 'selectinline',\n 'horizontal',\n 'vertical',\n 'shuffle',\n 'multi_horizontal',\n 'multi_vertical',\n 'summary_multichoice',\n 'summary_shortanswer',\n 'summary_numerical',\n 'multichoice',\n 'multiresponse',\n 'numerical',\n 'shortanswer',\n 'btn_cancel',\n 'btn_select',\n 'btn_insert',\n 'title',\n 'custom_grade',\n 'err_custom_rate',\n 'err_empty_answer',\n 'err_none_correct',\n 'err_not_numeric',\n ];\n if (hasQtypeMultianswerrgx(editor)) {\n strToFetch.push({key: 'regexp', component: 'qtype_regexp'});\n strToFetch.push({key: 'pluginnamesummary', component: 'qtype_regexp'});\n langKeys.push('regexp');\n langKeys.push('summary_regexp');\n }\n getStrings(strToFetch).then(function() {\n const args = Array.from(arguments);\n langKeys.map((l, i) => {\n STR[l] = args[0][i];\n return ''; // Make the linter happy.\n });\n return ''; // Make the linter happy.\n }).catch(() => {\n return '';\n });\n};\n\n/**\n * Return the question types that are available for the cloze question.\n * @returns {Array}\n * @private\n */\nconst _getQuestionTypes = function() {\n let qtypes = [\n {\n 'type': 'MULTICHOICE',\n 'abbr': ['MC'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.selectinline, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_H',\n 'abbr': ['MCH'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.horizontal, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_V',\n 'abbr': ['MCV'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.vertical, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_S',\n 'abbr': ['MCS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.selectinline, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_HS',\n 'abbr': ['MCHS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.horizontal, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_VS',\n 'abbr': ['MCVS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.vertical, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTIRESPONSE',\n 'abbr': ['MR'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_vertical, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_H',\n 'abbr': ['MRH'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_horizontal, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_S',\n 'abbr': ['MRS'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_vertical, STR.shuffle, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_HS',\n 'abbr': ['MRHS'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_horizontal, STR.shuffle, STR.singleno],\n },\n {\n 'type': 'NUMERICAL',\n 'abbr': ['NM'],\n 'name': STR.numerical,\n 'summary': STR.summary_numerical,\n },\n {\n 'type': 'SHORTANSWER',\n 'abbr': ['SA', 'MW'],\n 'name': STR.shortanswer,\n 'summary': STR.summary_shortanswer,\n 'options': [STR.caseno],\n },\n {\n 'type': 'SHORTANSWER_C',\n 'abbr': ['SAC', 'MWC'],\n 'name': STR.shortanswer,\n 'summary': STR.summary_shortanswer,\n 'options': [STR.caseyes],\n },\n ];\n if (hasQtypeMultianswerrgx(_editor)) {\n qtypes.splice(11, 0, {\n 'type': 'REGEXP',\n 'abbr': ['RX'],\n 'name': STR.regexp,\n 'summary': STR.summary_regexp,\n 'options': [STR.caseno],\n }, {\n 'type': 'REGEXP_C',\n 'abbr': ['RXC'],\n 'name': STR.regexp,\n 'summary': STR.summary_regexp,\n 'options': [STR.caseyes],\n });\n }\n return qtypes;\n};\n\n/**\n * Create the modal.\n * @return {Promise}\n * @private\n */\nconst _createModal = async function() {\n // Create the modal dialogue. Depending on whether we have a selected node or not, the content is different.\n const cfg = {\n title: STR.title,\n templateContext: {\n elementid: _editor.id\n },\n removeOnClose: true,\n large: true,\n };\n if (typeof Modal.create === 'function') {\n _modal = await Modal.create(cfg);\n } else {\n _modal = await ModalFactory.create(cfg);\n }\n};\n\n/**\n * Display modal dialogue to edit a cloze question. Either a form is displayed to edit subquestion or a list\n * of possible questions is show.\n *\n * @method displayDialogue\n * @public\n */\nconst displayDialogue = async function() {\n await _createModal();\n\n // Resolve whether cursor is in a subquestion.\n const subquestion = resolveSubquestion();\n if (subquestion) {\n _firstAnswer = null;\n // Subquestion found, remember which node of the marker nodes is selected.\n _selectedOffset = indexOfNode(_editor.dom.select('.' + markerClass), subquestion);\n _parseSubquestion(subquestion.innerHTML);\n _setDialogueContent(_qtype);\n } else {\n // No subquestion found, no offset to remember.\n _firstAnswer = _editor.selection.getContent();\n _selectedOffset = -1;\n _setDialogueContent();\n }\n};\n\n/**\n * On double click, check that we are on a question and display the dialogue with the question to edit.\n * @method displayDialogueForEdit\n * @param {Node} target\n * @public\n */\nconst displayDialogueForEdit = async function(target) {\n\n const subquestion = resolveSubquestion(target);\n if (!subquestion) {\n return;\n }\n await _createModal();\n _selectedOffset = indexOfNode(_editor.dom.select('.' + markerClass), subquestion);\n _parseSubquestion(subquestion.innerHTML);\n _setDialogueContent(_qtype);\n};\n\n/**\n * Search for cloze questions based on a regular expression. All the matching snippets at least contain the cloze\n * question definition. Although Moodle does not support encapsulated other functions within curly brackets, we\n * still try to find the correct closing bracket. The so extracted cloze question is surrounded by a marker span\n * element, that contains attributes so that the content inside the span cannot be modified by the editor (in the\n * textarea). Also, this makes it a lot easier to select the question, edit it in the dialogue and replace the result\n * in the existing text area.\n *\n * @method _addMarkers\n * @private\n */\nconst _addMarkers = function() {\n\n let content = _editor.getContent();\n let newContent = '';\n\n // Check if there is already a marker span. In this case we do not have to do anything.\n if (content.indexOf(markerClass) !== -1) {\n return;\n }\n\n let m;\n do {\n m = content.match((_getRegexQtype(_editor)));\n if (!m) { // No match of a cloze question, then we are done.\n newContent += content;\n break;\n }\n // Copy the current match to the new string preceded with the .\n const pos = content.indexOf(m[0]);\n newContent += content.substring(0, pos) + markerSpan + content.substring(pos, pos + m[0].length);\n content = content.substring(pos + m[0].length);\n\n // Count the { in the string, should be just one (the very first one at position 0).\n let level = (m[0].match(/\\{/g) || []).length;\n if (level === 1) {\n // If that's the case, we close the span and the cloze question text is the innerHTML of that marker span.\n newContent += '';\n continue; // Look for the next matching cloze question.\n }\n // If there are more { than } in the string, then we did not find the corresponding } that belongs to the cloze string.\n while (level > 1) {\n const a = content.indexOf('{');\n const b = content.indexOf('}');\n if (a > -1 && b > -1 && a < b) { // The { is before another } so remember to find as many } until we back at level 1.\n level++;\n newContent = content.substring(0, a);\n content = content.substring(a + 1);\n } else if (b > -1) { // We found a closing } to a previously {.\n newContent = content.substring(0, b);\n content = content.substring(b + 1);\n level--;\n } else {\n level = 1; // Should not happen, just to stop the endless loop.\n }\n }\n newContent += '
';\n } while (m);\n _editor.setContent(newContent);\n};\n\n/**\n * Look for the marker span elements around a cloze question and remove that span. Also, the marker for a new\n * node to be inserted would be removed here as well.\n */\nconst _removeMarkers = function() {\n for (const span of _editor.dom.select('span.' + markerClass)) {\n _editor.dom.setOuterHTML(span, span.classList.contains('new') ? '' : span.innerHTML);\n }\n};\n\n/**\n * When the source code view dialogue is show, we must remove the spans around the cloze question strings\n * from the editor content and add them again when the dialogue is closed.\n * Since this event is also triggered when the editor data is saved, we use this function to remove the\n * highlighting content at that time.\n *\n * @method onBeforeGetContent\n * @param {object} content\n * @public\n */\nconst onBeforeGetContent = function(content) {\n if (!isNull(content.source_view) && content.source_view === true) {\n // If the user clicks on 'Cancel' or the close button on the html\n // source code dialog view, make sure we re-add the visual styling.\n var onClose = function() {\n _editor.off('close', onClose);\n _addMarkers();\n };\n _editor.on('CloseWindow', () => {\n onClose();\n });\n // Remove markers only if modal is not called, otherwise we will lose our new question marker.\n if (!_modal) {\n _removeMarkers();\n }\n }\n};\n\n/**\n * Fires when the form containing the editor is submitted.\n *\n * @method onSubmit\n * @public\n */\nconst onSubmit = function() {\n _removeMarkers();\n};\n\n/**\n * Set the dialogue content for the tool, attaching any required events. Either the modal dialogue displays\n * a list of the question types for the form for a particular question to edit. The set content is also\n * called when the form has changed (up or down move, deletion and adding a response). We must be aware of that\n * an event to the dialogue buttons must be attached once only. Therefore, when the form content is modified, only\n * the form events for the answers are set again, the general events are nor (nomodalevents is true then).\n *\n * @method _setDialogueContent\n * @param {String} qtype The question type to be used\n * @param {boolean} nomodalevents Optional do not attach events.\n * @private\n */\nconst _setDialogueContent = function(qtype, nomodalevents) {\n const footer = Mustache.render(TEMPLATE.FOOTER, {\n cancel: STR.btn_cancel,\n submit: !qtype ? STR.btn_select : STR.btn_insert,\n });\n let contentText;\n if (!qtype) {\n contentText = Mustache.render(TEMPLATE.TYPE, {\n CSS: CSS,\n STR: STR,\n qtype: _qtype,\n types: _getQuestionTypes()\n });\n } else {\n contentText = Mustache.render(TEMPLATE.FORM, {\n CSS: CSS,\n STR: STR,\n answerdata: _answerdata,\n elementid: getUuid(),\n qtype: _qtype,\n name: _getQuestionTypes().filter(q => _qtype === q.type)[0].name,\n marks: _marks,\n numerical: (_qtype === 'NUMERICAL' || _qtype === 'NM')\n });\n }\n _modal.setBody(contentText);\n _modal.setFooter(footer);\n _modal.show();\n const $root = _modal.getRoot();\n _form = $root.get(0).querySelector('form');\n _toggleDeleteIcon();\n\n if (!nomodalevents) {\n _modal.registerEventListeners();\n _modal.registerCloseOnSave();\n _modal.registerCloseOnCancel();\n $root.on(ModalEvents.cancel, _cancel);\n\n if (!qtype) { // For the question list we need the choice handler only, and we are done.\n $root.on(ModalEvents.save, _choiceHandler);\n return;\n } // Handler to add the question string to the editor content.\n $root.on(ModalEvents.save, _setSubquestion);\n }\n // The form needs events for the icons to move up/down, add or delete a response.\n const getTarget = e => {\n let p = e.target;\n while (!isNull(p) && p.nodeType === 1 && p.tagName !== 'A') {\n p = p.parentNode;\n }\n if (isNull(p.classList)) {\n return null;\n }\n return p;\n };\n\n _form.addEventListener('click', e => {\n const p = getTarget(e);\n if (isNull(p)) {\n return;\n }\n if (p.classList.contains(CSS.DELETE)) {\n e.preventDefault();\n _deleteAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.ADD)) {\n e.preventDefault();\n _addAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.LOWER)) {\n e.preventDefault();\n _lowerAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.RAISE)) {\n e.preventDefault();\n _raiseAnswer(p);\n }\n });\n _form.addEventListener('keyup', e => {\n const p = getTarget(e);\n if (isNull(p)) {\n return;\n }\n if (p.classList.contains(CSS.ANSWER) || p.classList.contains(CSS.FEEDBACK)) {\n e.preventDefault();\n _addAnswer(p);\n }\n });\n _form.querySelectorAll('.' + CSS.FRACTION).forEach((sel) => {\n sel.addEventListener('change', e => {\n const id = e.target.getAttribute('id');\n if (e.target.value === selectCustomPercent) {\n document.getElementById(id + '_custom').parentNode.classList.remove('hidden');\n } else {\n document.getElementById(id + '_custom').parentNode.classList.add('hidden');\n }\n });\n });\n};\n\n/**\n * If there is one answer field, hide the delete icon. Otherwise show them\n * all to allow deletion of any answer.\n *\n * @private\n */\nconst _toggleDeleteIcon = function() {\n const deleteIcons = _form.querySelectorAll('.' + CSS.DELETE);\n if (deleteIcons.length === 1) {\n deleteIcons[0].classList.add('hidden');\n return;\n }\n for (let i = 0; i < deleteIcons.length; i++) {\n deleteIcons[i].classList.remove('hidden');\n }\n};\n\n/**\n * Handle question choice.\n *\n * @method _choiceHandler\n * @private\n * @param {Event} e Event from button click in chooser\n */\nconst _choiceHandler = function(e) {\n e.preventDefault();\n let qtype = _form.querySelector('input[name=qtype]:checked');\n if (qtype) {\n _qtype = qtype.value;\n }\n // For numerical and short answer questions (and when installed regexp) we offer one response field only.\n // All other question types have three empty response fields.\n const max = (_qtype.indexOf('SHORTANSWER') !== -1 || _qtype === 'NUMERICAL' || _qtype.indexOf('REGEXP') !== -1) ? 1 : 3;\n const blankAnswer = {\n id: getUuid(),\n answer: '',\n feedback: '',\n fraction: 100,\n fractionOptions: getFractionOptions(''),\n tolerance: 0,\n isCustomGrade: false,\n };\n _answerdata = [];\n for (let x = 0; x < max; x++) {\n _answerdata.push({...blankAnswer, id: getUuid()});\n }\n // The first response field gets the default grade correct.\n _answerdata[0].fractionOptions = getFractionOptions('=');\n // In case the user seleced some text, this is used as the first answer.\n if (_firstAnswer) {\n _answerdata[0].answer = _firstAnswer;\n }\n _modal.destroy();\n // Our choice is stored in _qtype. We need to create the modal dialogue with the form now.\n _createModal().then(() => {\n _setDialogueContent(_qtype);\n _form.querySelector('.' + CSS.ANSWER).focus();\n return ''; // Make the linter happy.\n }).catch(() => {\n return '';\n });\n};\n\n/**\n * Parse question and set properties found.\n *\n * @method _parseSubquestion\n * @private\n * @param {String} question The question string\n */\nconst _parseSubquestion = function(question) {\n _answerdata = []; // Flush answers to have an empty dialogue if something goes wrong parsing the question string.\n const regexQtype = _getRegexQtype(_editor);\n const parts = regexQtype.exec(question);\n regexQtype.lastIndex = 0; // Reset lastIndex so that the next match starts from the beginning of the question string.\n if (!parts) {\n return;\n }\n _marks = parts[1];\n _qtype = parts[2];\n // Convert the short notation to the long form e.g. SA to SHORTANSWER.\n if (_qtype.length < 5) {\n _getQuestionTypes().forEach(l => {\n for (const a of l.abbr) {\n if (a === _qtype) {\n _qtype = l.type;\n return;\n }\n }\n });\n }\n // Depending on the regex the position of the answers is different.\n const answers = parts[hasQtypeMultianswerrgx(_editor) ? 8 : 7].match(/(\\\\.|[^~])*/g);\n if (!answers) {\n return;\n }\n answers.forEach(function(answer) {\n const options = /^(%(-?[.0-9]+)%|(=?))((\\\\.|[^#])*)#?(.*)/.exec(answer);\n if (options && options[4]) {\n let frac = '';\n if (options[3]) {\n frac = options[3] === '=' ? '=' : 100;\n } else if (options[2]) {\n frac = options[2];\n }\n if (_qtype === 'NUMERICAL' || _qtype === 'NM') {\n const tolerance = /^([^:]*):?(.*)/.exec(options[4])[2] || 0;\n _answerdata.push({\n id: getUuid(),\n answer: strdecode(options[4].replace(/:.*/, '')),\n feedback: strdecode(options[6]),\n tolerance: tolerance,\n fraction: frac,\n fractionOptions: getFractionOptions(frac),\n isCustomGrade: isCustomGrade(frac),\n });\n return;\n }\n _answerdata.push({\n answer: strdecode(options[4]),\n id: getUuid(),\n feedback: strdecode(options[6]),\n fraction: frac,\n fractionOptions: getFractionOptions(frac),\n isCustomGrade: isCustomGrade(frac),\n });\n }\n });\n};\n\n/**\n * Insert a new set of answer blanks below the button.\n *\n * @method _addAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _addAnswer = function(a) {\n let index = indexOfNode(_form.querySelectorAll('.' + CSS.ADD), a);\n if (index === -1) {\n index = 0;\n }\n let fraction = '';\n let answer = '';\n let feedback = '';\n let tolerance = 0;\n if (a.closest('li')) {\n fraction = a.closest('li').querySelector('.' + CSS.FRACTION).value;\n if (fraction === selectCustomPercent) {\n fraction = a.closest('li').querySelector('.' + CSS.FRAC_CUSTOM).value;\n }\n answer = a.closest('li').querySelector('.' + CSS.ANSWER).value;\n feedback = a.closest('li').querySelector('.' + CSS.FEEDBACK).value;\n if (a.closest('li').querySelector('.' + CSS.TOLERANCE)) {\n tolerance = a.closest('li').querySelector('.' + CSS.TOLERANCE).value;\n }\n }\n _processFormData();\n _answerdata.splice(index, 0, {\n id: getUuid(),\n answer: answer,\n feedback: feedback,\n fraction: fraction,\n fractionOptions: getFractionOptions(fraction),\n tolerance: tolerance,\n isCustomGrade: isCustomGrade(fraction)\n });\n _setDialogueContent(_qtype, true);\n _toggleDeleteIcon();\n _form.querySelectorAll('.' + CSS.ANSWER).item(index).focus();\n};\n\n/**\n * Delete set of answer next to the button.\n *\n * @method _deleteAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _deleteAnswer = function(a) {\n let index = indexOfNode(_form.querySelectorAll('.' + CSS.DELETE), a);\n if (index === -1) {\n index = indexOfNode(_form.querySelectorAll('li'), a.closest('li'));\n }\n _processFormData();\n _answerdata.splice(index, 1);\n _setDialogueContent(_qtype, true);\n const answers = _form.querySelectorAll('.' + CSS.ANSWER);\n index = Math.min(index, answers.length - 1);\n answers.item(index).focus();\n _toggleDeleteIcon();\n};\n\n/**\n * Lower answer option\n *\n * @method _lowerAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _lowerAnswer = function(a) {\n const li = a.closest('li');\n li.before(li.nextSibling);\n li.querySelector('.' + CSS.ANSWER).focus();\n};\n\n/**\n * Raise answer option\n *\n * @method _raiseAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _raiseAnswer = function(a) {\n const li = a.closest('li');\n li.after(li.previousSibling);\n li.querySelector('.' + CSS.ANSWER).focus();\n};\n\n/**\n * Reset and hide form.\n *\n * @method _cancel\n * @param {Event} e Event from button click\n * @private\n */\nconst _cancel = function(e) {\n e.preventDefault();\n // In case there is a marker where the new question should be inserted in the text it needs to be removed.\n for (const span of _editor.dom.select('.' + markerClass + '.new')) {\n span.remove();\n }\n _modal.destroy();\n _editor.focus();\n _modal = null;\n};\n\n/**\n * Insert question string into editor content and reset and hide form. If the form contains an error\n * nothing happens.\n *\n * @method _setSubquestion\n * @param {Event} e Event from button click\n * @private\n */\nconst _setSubquestion = function(e) {\n e.preventDefault();\n // Check if there are any errors and if so, fill the error container with the\n // messages and return without going any further and closing the dialogue.\n const errMsg = _form.querySelector('.msg-error');\n const formErrors = _processFormData(true);\n if (formErrors.length > 0) {\n errMsg.innerHTML = '
  • ' + formErrors.join('
  • ') + '
';\n errMsg.classList.remove('hidden');\n return;\n } else {\n errMsg.classList.add('hidden');\n }\n // Build the parser function from the data, that is going to be placed into the editor content.\n let question = '{' + _marks + ':' + _qtype + ':';\n\n // Filter all empty responses\n for (let i = 0; i < _answerdata.length; i++) {\n if (_answerdata[i].raw === '') {\n continue;\n }\n question += _answerdata[i].fraction && !isNaN(_answerdata[i].fraction)\n ? '%' + _answerdata[i].fraction + '%' : _answerdata[i].fraction;\n question += strencode(_answerdata[i].answer);\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n question += ':' + _answerdata[i].tolerance;\n }\n if (_answerdata[i].feedback) {\n question += '#' + strencode(_answerdata[i].feedback);\n }\n if (i < _answerdata.length - 1) {\n question += '~';\n }\n }\n if (question.slice(-1) === '~') {\n question = question.substring(0, question.length - 1);\n }\n question += '}';\n\n _modal.destroy();\n _modal = null;\n _editor.focus();\n if (_selectedOffset > -1) { // We have to replace one of the marker spans (the innerHTML contains the question string).\n _editor.dom.select('.' + markerClass)[_selectedOffset].innerHTML = question;\n } else {\n // Just add the question text with markup.\n _editor.insertContent(markerSpan + question + '');\n }\n};\n\n/**\n * Read the form data, process it and store the result in the internal _answerdata array.\n * Also, if validation is enabled, the fields are checked for invalid values e.g.\n * - answer field is empty (if a correct answer is contained, empty fields are eliminated).\n * - custom_grade field whenin use and does not contain a number.\n * - no field is marked as a correct answer.\n * - tolerance field must be in percentage of min -100 and max 100.\n * Any field with an error is maked and the first field containing an error gets the focus.\n *\n * @method _processFormData\n * @param {boolean} validate\n * @return {Array}\n * @private\n */\nconst _processFormData = function(validate) {\n _answerdata = [];\n let globalErrors = [];\n const answers = _form.querySelectorAll('.' + CSS.ANSWER);\n const feedbacks = _form.querySelectorAll('.' + CSS.FEEDBACK);\n const fractions = _form.querySelectorAll('.' + CSS.FRACTION);\n const customGrades = _form.querySelectorAll('.' + CSS.FRAC_CUSTOM);\n const tolerances = _form.querySelectorAll('.' + CSS.TOLERANCE);\n // Remove any error classes.\n for (let i = 0; i < answers.length; i++) {\n answers.item(i).classList.remove('error');\n customGrades.item(i).classList.remove('error');\n const currentAnswer = {\n raw: answers.item(i).value.trim(),\n answer: answers.item(i).value.trim(),\n id: getUuid(),\n feedback: feedbacks.item(i).value,\n fraction: fractions.item(i).value === selectCustomPercent ? customGrades.item(i).value : fractions.item(i).value,\n fractionOptions: getFractionOptions(fractions.item(i).value),\n tolerance: tolerances.length > 0 ? tolerances.item(i).value : 0,\n isCustomGrade: fractions.item(i).value === selectCustomPercent\n };\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n tolerances.item(i).classList.remove('error');\n // In numeric questions convert answer and tolerance to numeric values (this filters non numeric values).\n currentAnswer.answer = Number(currentAnswer.answer);\n currentAnswer.tolerance = Number(currentAnswer.tolerance);\n }\n _answerdata.push(currentAnswer);\n }\n _marks = _form.querySelector('.' + CSS.MARKS).value;\n\n if (validate) {\n const {hasCorrectAnswer, errors} = _validateAnswers();\n for (let i = 0; i < _answerdata.length; i++) {\n for (const err of _answerdata[i].hasErrors) {\n if (hasCorrectAnswer && (err === 'empty_answer' || err === 'correct_but_empty')) {\n break;\n }\n if (err === 'answer_not_numeric' || err === 'empty_answer' || err === 'correct_but_empty') {\n answers.item(i).classList.add('error');\n } else if (err === 'tolerance_not_numeric') {\n tolerances.item(i).classList.add('error');\n } else if (err === 'error_custom_rate') {\n customGrades.item(i).classList.add('error');\n }\n }\n }\n globalErrors = _translateGlobalErrors(hasCorrectAnswer, errors);\n // If we have errors, we focus the first field that contains an error.\n if (globalErrors.length > 0) {\n _form.querySelector('input.error').focus();\n }\n }\n return globalErrors;\n};\n\n/**\n * Validates the answer array. Checks for each question if the data from the form is\n * incomplete or has other errors. These are flagged accordingly in the array element.\n * The retruned object contains the properties:\n * - hasCorrectAnswer {boolean} is true if there is at least one correct answer.\n * - errors {Array} list of strings that contain an error code that is globaly used for error messages.\n *\n * @return {Array}\n * @private\n */\nconst _validateAnswers = function() {\n let errors = [];\n let hasCorrect = false;\n for (let i = 0; i < _answerdata.length; i++) {\n _answerdata[i].hasErrors = [];\n // Check if we have an empty answer string.\n if (_answerdata[i].raw === '') {\n _answerdata[i].hasErrors.push('empty_answer');\n }\n // When there are numeric questions, check that the answer and tolerance is a valid number.\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n if (isNaN(_answerdata[i].answer) && _answerdata[i].raw !== '') {\n _answerdata[i].hasErrors.push('answer_not_numeric');\n }\n if (isNaN(_answerdata[i].tolerance)) {\n _answerdata[i].hasErrors.push('tolerance_not_numeric');\n }\n }\n // Check the custom grade, that must be a percentage number between -100 and 100.\n if (_answerdata[i].isCustomGrade &&\n (isNaN(_answerdata[i].fraction) || _answerdata[i].fraction < -100 || _answerdata[i].fraction > 100\n || _answerdata[i].fraction.trim() === '')\n ) {\n _answerdata[i].hasErrors.push('error_custom_rate');\n }\n // We found a correct answer, when grade is marked as 100 or \"=\" and the answer is not empty.\n if (_answerdata[i].fraction === '100' || _answerdata[i].fraction === '=') {\n if (_answerdata[i].raw !== '') {\n _answerdata[i].isCorrect = true;\n hasCorrect = true;\n } else {\n _answerdata[i].hasErrors.push('correct_but_empty');\n }\n }\n errors = errors.concat(_answerdata[i].hasErrors);\n }\n\n return {\n hasCorrectAnswer: hasCorrect,\n errors: _combineGlobalErrors(hasCorrect, errors),\n };\n};\n\n/**\n * Translate the errors into a readable string for a list that is used on top of the\n * input fields, to indicate what part of the data is incorrect.\n *\n * @param {Boolean} hasCorrectAnswer\n * @param {Array} errors\n * @return {Array}\n * @private\n */\nconst _translateGlobalErrors = function(hasCorrectAnswer, errors) {\n const errTranslated = [];\n // Translate the error strings into a string that can be displayed in the form.\n const trMsg = {\n emptyanswer: STR.err_empty_answer,\n answernotnumeric: STR.err_not_numeric,\n tolerancenotnumeric: STR.err_not_numeric,\n errorcustomrate: STR.err_custom_rate,\n nonecorrect: STR.err_none_correct,\n };\n for (const err of errors) {\n // If there's at least one correct answer, we filter out all empty answers and therefore do not\n // show the error message.\n if (hasCorrectAnswer && err === 'empty_answer' || err === 'correct_but_empty') {\n continue;\n }\n // Remove underscore (we do this only because of the js linter).\n const key = err.replace(/_/g, '');\n errTranslated.push(trMsg[key]);\n }\n return errTranslated;\n};\n\n/**\n * Combine the error list from the answers to a global list.\n *\n * @param {Boolean} hasCorrectAnswer\n * @param {Array} errors\n * @return {Array}\n * @private\n */\nconst _combineGlobalErrors = function(hasCorrectAnswer, errors) {\n // Unique errors for the global error list.\n const errUnique = errors.filter((value, index, array) => array.indexOf(value) === index);\n // If we have a correct answer, do not show the empty answer error, because empty responses are filtered.\n if (hasCorrectAnswer) {\n const i = errUnique.indexOf('empty_answer');\n if (i > -1) {\n errUnique.splice(i, 1);\n }\n } else if (!errUnique.includes('correct_but_empty')) {\n errUnique.push('none_correct');\n }\n return errUnique;\n};\n\n/**\n * Check whether cursor is in a subquestion and return subquestion text if\n * true.\n *\n * @method resolveSubquestion\n * @param {Node|null} element The element to check if it is a subquestion.\n * @return {Mixed} The selected node of with the subquestion if found, false otherwise.\n */\nconst resolveSubquestion = function(element) {\n let span = element || _editor.selection.getStart();\n if (!isNull(span.classList) && span.classList.contains(markerClass)) {\n return span;\n }\n _editor.dom.getParents(span, elm => {\n // Are we in a span that encapsulates the cloze question?\n if (!isNull(elm.classList) && elm.classList.contains(markerClass)) {\n return elm;\n }\n return false;\n });\n return false;\n};\n\nexport {\n displayDialogue,\n displayDialogueForEdit,\n resolveSubquestion,\n onInit,\n onBeforeGetContent,\n onSubmit,\n};\n"],"names":["isNull","a","strdecode","t","String","replace","strencode","indexOfNode","list","node","i","length","getUuid","crypto","randomUUID","Math","floor","random","toString","getFractionOptions","s","attrSel","isSel","html","STR","incorrect","correct","FRACTIONS","forEach","item","value","indexOf","custom_grade","isCustomGrade","found","markerClass","markerSpan","CSS","ANSWER","ANSWERS","ADD","CANCEL","DELETE","FEEDBACK","FRACTION","FRAC_CUSTOM","LEFT","LOWER","RIGHT","MARKS","DUPLICATE","RAISE","SUBMIT","SUMMARY","TOLERANCE","TYPE","TEMPLATE","FORM","M","util","image_url","FOOTER","_editor","_form","_answerdata","_qtype","_selectedOffset","_marks","_modal","_firstAnswer","ed","_addMarkers","_getStr","_getRegexQtype","editor","extQtypes","RegExp","async","strToFetch","key","component","langKeys","push","then","args","Array","from","arguments","map","l","catch","_getQuestionTypes","qtypes","multichoice","summary_multichoice","selectinline","singleyes","horizontal","vertical","shuffle","multiresponse","multi_vertical","singleno","multi_horizontal","numerical","summary_numerical","shortanswer","summary_shortanswer","caseno","caseyes","splice","regexp","summary_regexp","_createModal","cfg","title","templateContext","elementid","id","removeOnClose","large","Modal","create","ModalFactory","subquestion","resolveSubquestion","dom","select","_parseSubquestion","innerHTML","_setDialogueContent","selection","getContent","target","m","content","newContent","match","pos","substring","level","b","setContent","_removeMarkers","span","setOuterHTML","classList","contains","source_view","onClose","off","on","qtype","nomodalevents","footer","Mustache","render","cancel","btn_cancel","submit","btn_insert","btn_select","contentText","answerdata","name","filter","q","type","marks","types","setBody","setFooter","show","$root","getRoot","get","querySelector","_toggleDeleteIcon","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","ModalEvents","_cancel","save","_choiceHandler","_setSubquestion","getTarget","e","p","nodeType","tagName","parentNode","addEventListener","preventDefault","_deleteAnswer","_addAnswer","_lowerAnswer","_raiseAnswer","querySelectorAll","sel","getAttribute","document","getElementById","remove","add","deleteIcons","max","blankAnswer","answer","feedback","fraction","fractionOptions","tolerance","x","destroy","focus","question","regexQtype","parts","exec","lastIndex","abbr","answers","options","frac","index","closest","_processFormData","min","li","before","nextSibling","after","previousSibling","errMsg","formErrors","join","raw","isNaN","slice","insertContent","validate","globalErrors","feedbacks","fractions","customGrades","tolerances","currentAnswer","trim","Number","hasCorrectAnswer","errors","_validateAnswers","err","hasErrors","_translateGlobalErrors","hasCorrect","isCorrect","concat","_combineGlobalErrors","errTranslated","trMsg","emptyanswer","err_empty_answer","answernotnumeric","err_not_numeric","tolerancenotnumeric","errorcustomrate","err_custom_rate","nonecorrect","err_none_correct","errUnique","array","includes","element","getStart","getParents","elm"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/commands.js b/amd/src/commands.js index a36dff4..534296e 100644 --- a/amd/src/commands.js +++ b/amd/src/commands.js @@ -50,7 +50,9 @@ export const getSetup = async() => { return (editor) => { // Check whether we are editing a question. const body = document.querySelector('body#page-question-type-multianswer form, ' + - 'body#page-question-type-multianswerwiris form'); + 'body#page-question-type-multianswerwiris form,' + + 'body#page-question-type-multianswerrgx form' + ); // And if the editor is used on the question text. if (!body || editor.id.indexOf('questiontext') === -1) { return; diff --git a/amd/src/options.js b/amd/src/options.js new file mode 100644 index 0000000..65c939a --- /dev/null +++ b/amd/src/options.js @@ -0,0 +1,41 @@ +// 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 . + +/** + * Options helper for tiny_cloze plugin. + * + * @module tiny_cloze + * @copyright 2024 Stephan Robotta + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {getPluginOptionName} from 'editor_tiny/options'; +import {pluginName} from './common'; + +const multianswerrgx = getPluginOptionName(pluginName, 'multianswerrgx'); + +/** + * Register the options for the Tiny Cloze question plugin. + * + * @param {tinymce.Editor} editor + */ +export const register = (editor) => { + editor.options.register(multianswerrgx, { + processor: 'boolean', + "default": false, + }); +}; + +export const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx); \ No newline at end of file diff --git a/amd/src/plugin.js b/amd/src/plugin.js index fe2d573..22df54b 100644 --- a/amd/src/plugin.js +++ b/amd/src/plugin.js @@ -27,6 +27,7 @@ import {getPluginMetadata} from 'editor_tiny/utils'; import {component, pluginName} from './common'; import {getSetup as getCommandSetup} from './commands'; import * as Configuration from './configuration'; +import {register as registerOptions} from './options'; // Setup the tiny_cloze Plugin. // eslint-disable-next-line no-async-promise-executor @@ -45,6 +46,9 @@ export default new Promise(async(resolve) => { // Reminder: Any asynchronous code must be run before this point. tinyMCE.PluginManager.add(pluginName, (editor) => { + // Register options. + registerOptions(editor); + // Setup any commands such as buttons, menu items, and so on. setupCommands(editor); diff --git a/amd/src/ui.js b/amd/src/ui.js index 922319e..86ac690 100644 --- a/amd/src/ui.js +++ b/amd/src/ui.js @@ -27,6 +27,7 @@ import ModalFactory from 'core/modal_factory'; import Mustache from 'core/mustache'; import {get_strings as getStrings} from 'core/str'; import {component} from './common'; +import {hasQtypeMultianswerrgx} from './options'; // Helper functions. const isNull = a => a === null || a === undefined; @@ -77,9 +78,6 @@ const isCustomGrade = s => { // Marker class and the whole span element that is used to encapsulate the cloze question text. const markerClass = 'cloze-question-marker'; const markerSpan = ''; -// Regex to recognize the question string in the text e.g. {1:NUMERICAL:...} or {:MULTICHOICE:...} -// eslint-disable-next-line max-len -const reQtype = /\{([0-9]*):(MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?):(.*?)(?{{cancel}}' + '', }; - const FRACTIONS = [ - {value: 100}, - {value: 50}, - {value: 0}, - ]; +const FRACTIONS = [ + {value: 100}, + {value: 50}, + {value: 0}, +]; // Language strings used in the modal dialogue. const STR = {}; -const getStr = async() => { - getStrings([ + +/** + * The editor instance that is injected via the onInit() function. + * + * @type {tinymce.Editor} + * @private + */ +let _editor = null; + +/** + * A reference to the currently open form. + * + * @param _form + * @type {Node} + * @private + */ +let _form = null; + +/** + * An array containing the current answers options + * + * @param _answerdata + * @type {Array} + * @private + */ +let _answerdata = []; + +/** + * The sub question type to be edited + * + * @param _qtype + * @type {string|null} + * @private + */ +let _qtype = null; + +/** + * Remember the pos of the selected node. + * @type {number} + * @private + */ +let _selectedOffset = -1; + +/** + * The maximum marks for the sub question + * + * @param _marks + * @type {Integer} + * @private + */ +let _marks = 1; + +/** + * The modal dialogue to be displayed when designing the cloze question types. + * @type {Modal|null} + */ +let _modal = null; + +/** + * If its a normal selection of text, use it for the first answer field. + * @type {string|null} + */ +let _firstAnswer = null; + +/** + * Inject the editor instance and add markers to the cloze question texts. + * @param {tinymce.Editor} ed + */ +const onInit = function(ed) { + _editor = ed; // The current editor instance. + // Add the marker spans. + _addMarkers(); + // And get the language strings. + _getStr(ed); +}; + +/** + * Regex to recognize the question string in the text e.g. {1:NUMERICAL:...} or {:MULTICHOICE:...} + * @param {tinymce.Editor} editor + * @return {RegExp} + * @private + */ +const _getRegexQtype = (editor) => { + // eslint-disable-next-line max-len + const baseQtypes = 'MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?'; + const extQtypes = hasQtypeMultianswerrgx(editor) ? '|REGEXP(_C)?|RXC?' : ''; + return new RegExp('\\{([0-9]*):(' + baseQtypes + extQtypes + '):(.*?)(? { + let strToFetch = [ {key: 'answer', component: 'question'}, {key: 'chooseqtypetoadd', component: 'question'}, {key: 'defaultmark', component: 'question'}, @@ -237,48 +329,56 @@ const getStr = async() => { {key: 'err_empty_answer', component}, {key: 'err_none_correct', component}, {key: 'err_not_numeric', component}, - ]).then(function() { + ]; + let langKeys = [ + 'answer', + 'chooseqtypetoadd', + 'defaultmark', + 'feedback', + 'correct', + 'incorrect', + 'addmoreanswerblanks', + 'delete', + 'up', + 'down', + 'tolerance', + 'grade', + 'caseno', + 'caseyes', + 'singleno', + 'singleyes', + 'selectinline', + 'horizontal', + 'vertical', + 'shuffle', + 'multi_horizontal', + 'multi_vertical', + 'summary_multichoice', + 'summary_shortanswer', + 'summary_numerical', + 'multichoice', + 'multiresponse', + 'numerical', + 'shortanswer', + 'btn_cancel', + 'btn_select', + 'btn_insert', + 'title', + 'custom_grade', + 'err_custom_rate', + 'err_empty_answer', + 'err_none_correct', + 'err_not_numeric', + ]; + if (hasQtypeMultianswerrgx(editor)) { + strToFetch.push({key: 'regexp', component: 'qtype_regexp'}); + strToFetch.push({key: 'pluginnamesummary', component: 'qtype_regexp'}); + langKeys.push('regexp'); + langKeys.push('summary_regexp'); + } + getStrings(strToFetch).then(function() { const args = Array.from(arguments); - [ - 'answer', - 'chooseqtypetoadd', - 'defaultmark', - 'feedback', - 'correct', - 'incorrect', - 'addmoreanswerblanks', - 'delete', - 'up', - 'down', - 'tolerance', - 'grade', - 'caseno', - 'caseyes', - 'singleno', - 'singleyes', - 'selectinline', - 'horizontal', - 'vertical', - 'shuffle', - 'multi_horizontal', - 'multi_vertical', - 'summary_multichoice', - 'summary_shortanswer', - 'summary_numerical', - 'multichoice', - 'multiresponse', - 'numerical', - 'shortanswer', - 'btn_cancel', - 'btn_select', - 'btn_insert', - 'title', - 'custom_grade', - 'err_custom_rate', - 'err_empty_answer', - 'err_none_correct', - 'err_not_numeric', - ].map((l, i) => { + langKeys.map((l, i) => { STR[l] = args[0][i]; return ''; // Make the linter happy. }); @@ -287,8 +387,14 @@ const getStr = async() => { return ''; }); }; -const getQuestionTypes = function() { - return [ + +/** + * Return the question types that are available for the cloze question. + * @returns {Array} + * @private + */ +const _getQuestionTypes = function() { + let qtypes = [ { 'type': 'MULTICHOICE', 'abbr': ['MC'], @@ -380,81 +486,22 @@ const getQuestionTypes = function() { 'options': [STR.caseyes], }, ]; -}; - -/** - * The editor instance that is injected via the onInit() function. - * - * @type {tinymce.Editor} - * @private - */ -let _editor = null; - -/** - * A reference to the currently open form. - * - * @param _form - * @type {Node} - * @private - */ -let _form = null; - -/** - * An array containing the current answers options - * - * @param _answerdata - * @type {Array} - * @private - */ -let _answerdata = []; - -/** - * The sub question type to be edited - * - * @param _qtype - * @type {string|null} - * @private - */ -let _qtype = null; - -/** - * Remember the pos of the selected node. - * @type {number} - * @private - */ -let _selectedOffset = -1; - -/** - * The maximum marks for the sub question - * - * @param _marks - * @type {Integer} - * @private - */ -let _marks = 1; - -/** - * The modal dialogue to be displayed when designing the cloze question types. - * @type {Modal|null} - */ -let _modal = null; - -/** - * If its a normal selection of text, use it for the first answer field. - * @type {string|null} - */ -let _firstAnswer = null; - -/** - * Inject the editor instance and add markers to the cloze question texts. - * @param {tinymce.Editor} ed - */ -const onInit = function(ed) { - _editor = ed; // The current editor instance. - // Add the marker spans. - _addMarkers(); - // And get the language strings. - getStr(); + if (hasQtypeMultianswerrgx(_editor)) { + qtypes.splice(11, 0, { + 'type': 'REGEXP', + 'abbr': ['RX'], + 'name': STR.regexp, + 'summary': STR.summary_regexp, + 'options': [STR.caseno], + }, { + 'type': 'REGEXP_C', + 'abbr': ['RXC'], + 'name': STR.regexp, + 'summary': STR.summary_regexp, + 'options': [STR.caseyes], + }); + } + return qtypes; }; /** @@ -546,7 +593,7 @@ const _addMarkers = function() { let m; do { - m = content.match(reQtype); + m = content.match((_getRegexQtype(_editor))); if (!m) { // No match of a cloze question, then we are done. newContent += content; break; @@ -655,7 +702,7 @@ const _setDialogueContent = function(qtype, nomodalevents) { CSS: CSS, STR: STR, qtype: _qtype, - types: getQuestionTypes() + types: _getQuestionTypes() }); } else { contentText = Mustache.render(TEMPLATE.FORM, { @@ -664,7 +711,7 @@ const _setDialogueContent = function(qtype, nomodalevents) { answerdata: _answerdata, elementid: getUuid(), qtype: _qtype, - name: getQuestionTypes().filter(q => _qtype === q.type)[0].name, + name: _getQuestionTypes().filter(q => _qtype === q.type)[0].name, marks: _marks, numerical: (_qtype === 'NUMERICAL' || _qtype === 'NM') }); @@ -777,9 +824,9 @@ const _choiceHandler = function(e) { if (qtype) { _qtype = qtype.value; } - // For numerical and short answer questions we offer one response field only. All other - // question types have three empty response fields. - const max = (_qtype.indexOf('SHORTANSWER') !== -1 || _qtype === 'NUMERICAL') ? 1 : 3; + // For numerical and short answer questions (and when installed regexp) we offer one response field only. + // All other question types have three empty response fields. + const max = (_qtype.indexOf('SHORTANSWER') !== -1 || _qtype === 'NUMERICAL' || _qtype.indexOf('REGEXP') !== -1) ? 1 : 3; const blankAnswer = { id: getUuid(), answer: '', @@ -819,8 +866,9 @@ const _choiceHandler = function(e) { */ const _parseSubquestion = function(question) { _answerdata = []; // Flush answers to have an empty dialogue if something goes wrong parsing the question string. - const parts = reQtype.exec(question); - reQtype.lastIndex = 0; // Reset lastIndex so that the next match starts from the beginning of the question string. + const regexQtype = _getRegexQtype(_editor); + const parts = regexQtype.exec(question); + regexQtype.lastIndex = 0; // Reset lastIndex so that the next match starts from the beginning of the question string. if (!parts) { return; } @@ -828,7 +876,7 @@ const _parseSubquestion = function(question) { _qtype = parts[2]; // Convert the short notation to the long form e.g. SA to SHORTANSWER. if (_qtype.length < 5) { - getQuestionTypes().forEach(l => { + _getQuestionTypes().forEach(l => { for (const a of l.abbr) { if (a === _qtype) { _qtype = l.type; @@ -837,7 +885,8 @@ const _parseSubquestion = function(question) { } }); } - const answers = parts[7].match(/(\\.|[^~])*/g); + // Depending on the regex the position of the answers is different. + const answers = parts[hasQtypeMultianswerrgx(_editor) ? 8 : 7].match(/(\\.|[^~])*/g); if (!answers) { return; } diff --git a/classes/plugininfo.php b/classes/plugininfo.php index 0935ca3..b392400 100644 --- a/classes/plugininfo.php +++ b/classes/plugininfo.php @@ -24,14 +24,22 @@ namespace tiny_cloze; +use context; +use editor_tiny\editor; use editor_tiny\plugin; use editor_tiny\plugin_with_buttons; +use editor_tiny\plugin_with_configuration; use editor_tiny\plugin_with_menuitems; +use question_bank; + +defined('MOODLE_INTERNAL') || die; + +require_once(__DIR__ . '/../../../../../behat/classes/util.php'); /** * The capabilities of the plugin, in this case there is one toolbar button and one menu item. */ -class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menuitems { +class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menuitems, plugin_with_configuration { /** * Get the internal name of the toolbar button. @@ -53,4 +61,36 @@ public static function get_available_menuitems(): array { ]; } + /** + * Returns the configuration values the plugin needs to take into consideration. + * + * @param context $context + * @param array $options + * @param array $fpoptions + * @param editor|null $editor + * @return array + */ + public static function get_plugin_configuration_for_context( + context $context, + array $options, + array $fpoptions, + ?editor $editor = null + ): array { + + // When on the test site, check that the simulation config for an existing regex question type is set. + if (\behat_util::is_test_site()) { + return ['multianswerrgx' => (bool)get_config('tiny_cloze', 'simulate_multianswerrgx')]; + } + + try { + // Check if the multianswerrgx question type is available. + $instance = question_bank::get_qtype('multianswerrgx'); + return ['multianswerrgx' => is_object($instance)]; + } catch (\exception $e) { + // The multianswerrgx question type is not available. + return ['multianswerrgx' => false]; + } + return ['multianswerrgx' => false]; + } + } From 1827c152cd348d04a65a6016f7a03bd436ccbfe0 Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Sat, 12 Oct 2024 17:28:41 +0200 Subject: [PATCH 2/5] Display the REGEX types on the Clozergx question only. --- amd/build/commands.min.js | 2 +- amd/build/commands.min.js.map | 2 +- amd/build/options.min.js | 4 ++-- amd/build/options.min.js.map | 2 +- amd/src/commands.js | 22 +++++++++++++++++----- amd/src/options.js | 3 ++- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/amd/build/commands.min.js b/amd/build/commands.min.js index 37e3c29..714a382 100644 --- a/amd/build/commands.min.js +++ b/amd/build/commands.min.js @@ -1,3 +1,3 @@ -define("tiny_cloze/commands",["exports","editor_tiny/utils","core/str","./common","./ui"],(function(_exports,_utils,_str,_common,_ui){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[clozeButtonText,buttonImage]=await Promise.all([(0,_str.get_string)("pluginname",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{document.querySelector("body#page-question-type-multianswer form, body#page-question-type-multianswerwiris form,body#page-question-type-multianswerrgx form")&&-1!==editor.id.indexOf("questiontext")&&(editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addToggleButton(_common.clozeeditButtonName,{icon:_common.icon,tooltip:clozeButtonText,onAction:()=>(0,_ui.displayDialogue)(),onSetup:api=>{editor.on("click",(()=>{api.setActive(!1!==(0,_ui.resolveSubquestion)())}))}}),editor.ui.registry.addMenuItem(_common.clozeeditButtonName,{icon:_common.icon,text:clozeButtonText,onAction:()=>(0,_ui.displayDialogue)()}),editor.on("init",(()=>(0,_ui.onInit)(editor))),editor.on("BeforeGetContent",(format=>(0,_ui.onBeforeGetContent)(format))),editor.on("submit",(()=>(0,_ui.onSubmit)())),editor.on("dblclick",(e=>(0,_ui.displayDialogueForEdit)(e.target))))}}})); +define("tiny_cloze/commands",["exports","editor_tiny/utils","core/str","./common","./ui","./options"],(function(_exports,_utils,_str,_common,_ui,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[clozeButtonText,buttonImage]=await Promise.all([(0,_str.get_string)("pluginname",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{const body=document.querySelector("body#page-question-type-multianswer, body#page-question-type-multianswerwiris,body#page-question-type-multianswerrgx");body&&-1!==editor.id.indexOf("questiontext")&&(-1===body.id.indexOf("multianswerrgx")&&(0,_options.disableQtypeMultianswerrgx)(editor),editor.ui.registry.addIcon(_common.icon,buttonImage.html),editor.ui.registry.addToggleButton(_common.clozeeditButtonName,{icon:_common.icon,tooltip:clozeButtonText,onAction:()=>(0,_ui.displayDialogue)(),onSetup:api=>{editor.on("click",(()=>{api.setActive(!1!==(0,_ui.resolveSubquestion)())}))}}),editor.ui.registry.addMenuItem(_common.clozeeditButtonName,{icon:_common.icon,text:clozeButtonText,onAction:()=>(0,_ui.displayDialogue)()}),editor.on("init",(()=>(0,_ui.onInit)(editor))),editor.on("BeforeGetContent",(format=>(0,_ui.onBeforeGetContent)(format))),editor.on("submit",(()=>(0,_ui.onSubmit)())),editor.on("dblclick",(e=>(0,_ui.displayDialogueForEdit)(e.target))))}}})); //# 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 a06b0ba..1a0c869 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_cloze plugin.\n *\n * @module tiny_cloze/commands\n * @copyright 2023 MoodleDACH\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 clozeeditButtonName,\n icon,\n} from './common';\nimport {displayDialogue, displayDialogueForEdit, resolveSubquestion, onInit, onBeforeGetContent, onSubmit} from './ui';\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 clozeButtonText,\n buttonImage,\n ] = await Promise.all([\n getString('pluginname', component),\n getButtonImage('icon', component),\n ]);\n\n return (editor) => {\n // Check whether we are editing a question.\n const body = document.querySelector('body#page-question-type-multianswer form, ' +\n 'body#page-question-type-multianswerwiris form,' +\n 'body#page-question-type-multianswerrgx form'\n );\n // And if the editor is used on the question text.\n if (!body || editor.id.indexOf('questiontext') === -1) {\n return;\n }\n // Only if both conditions are valid, then continue setting up the plugin.\n\n // Register the Moodle SVG as an icon suitable for use as a TinyMCE toolbar button.\n editor.ui.registry.addIcon(icon, buttonImage.html);\n\n // Register the clozeedit Toolbar Button.\n editor.ui.registry.addToggleButton(clozeeditButtonName, {\n icon,\n tooltip: clozeButtonText,\n onAction: () => displayDialogue(),\n onSetup: (api) => {\n editor.on('click', () => {\n api.setActive(resolveSubquestion() !== false);\n });\n }\n });\n\n // Register the menu item.\n editor.ui.registry.addMenuItem(clozeeditButtonName, {\n icon,\n text: clozeButtonText,\n onAction: () => displayDialogue(),\n });\n\n editor.on('init', () => onInit(editor));\n editor.on('BeforeGetContent', format => onBeforeGetContent(format));\n editor.on('submit', () => onSubmit());\n editor.on('dblclick', (e) => displayDialogueForEdit(e.target));\n };\n};\n"],"names":["async","clozeButtonText","buttonImage","Promise","all","component","editor","document","querySelector","id","indexOf","ui","registry","addIcon","icon","html","addToggleButton","clozeeditButtonName","tooltip","onAction","onSetup","api","on","setActive","addMenuItem","text","format","e","target"],"mappings":"yOAwCwBA,gBAEhBC,gBACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,aAAcC,oBACxB,yBAAe,OAAQA,4BAGnBC,SAESC,SAASC,cAAc,yIAKgB,IAAvCF,OAAOG,GAAGC,QAAQ,kBAM/BJ,OAAOK,GAAGC,SAASC,QAAQC,aAAMZ,YAAYa,MAG7CT,OAAOK,GAAGC,SAASI,gBAAgBC,4BAAqB,CACpDH,KAAAA,aACAI,QAASjB,gBACTkB,SAAU,KAAM,yBAChBC,QAAUC,MACNf,OAAOgB,GAAG,SAAS,KACdD,IAAIE,WAAmC,KAAzB,mCAM3BjB,OAAOK,GAAGC,SAASY,YAAYP,4BAAqB,CAChDH,KAAAA,aACAW,KAAMxB,gBACNkB,SAAU,KAAM,2BAGpBb,OAAOgB,GAAG,QAAQ,KAAM,cAAOhB,UAC/BA,OAAOgB,GAAG,oBAAoBI,SAAU,0BAAmBA,UAC3DpB,OAAOgB,GAAG,UAAU,KAAM,oBAC1BhB,OAAOgB,GAAG,YAAaK,IAAM,8BAAuBA,EAAEC"} \ 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_cloze plugin.\n *\n * @module tiny_cloze/commands\n * @copyright 2023 MoodleDACH\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 clozeeditButtonName,\n icon,\n} from './common';\nimport {\n displayDialogue,\n displayDialogueForEdit,\n resolveSubquestion,\n onInit,\n onBeforeGetContent,\n onSubmit\n} from './ui';\nimport {disableQtypeMultianswerrgx} from './options';\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 clozeButtonText,\n buttonImage,\n ] = await Promise.all([\n getString('pluginname', component),\n getButtonImage('icon', component),\n ]);\n\n return (editor) => {\n // Check whether we are editing a question.\n const body = document.querySelector('body#page-question-type-multianswer, ' +\n 'body#page-question-type-multianswerwiris,' +\n 'body#page-question-type-multianswerrgx'\n );\n // And if the editor is used on the question text.\n if (!body || editor.id.indexOf('questiontext') === -1) {\n return;\n }\n // Only if all conditions are valid, then continue setting up the plugin.\n // However, if we have not a body#page-question-type-multianswerrgx then disable the regex types.\n if (body.id.indexOf('multianswerrgx') === -1) {\n disableQtypeMultianswerrgx(editor);\n }\n\n // Register the Moodle SVG as an icon suitable for use as a TinyMCE toolbar button.\n editor.ui.registry.addIcon(icon, buttonImage.html);\n\n // Register the clozeedit Toolbar Button.\n editor.ui.registry.addToggleButton(clozeeditButtonName, {\n icon,\n tooltip: clozeButtonText,\n onAction: () => displayDialogue(),\n onSetup: (api) => {\n editor.on('click', () => {\n api.setActive(resolveSubquestion() !== false);\n });\n }\n });\n\n // Register the menu item.\n editor.ui.registry.addMenuItem(clozeeditButtonName, {\n icon,\n text: clozeButtonText,\n onAction: () => displayDialogue(),\n });\n\n editor.on('init', () => onInit(editor));\n editor.on('BeforeGetContent', format => onBeforeGetContent(format));\n editor.on('submit', () => onSubmit());\n editor.on('dblclick', (e) => displayDialogueForEdit(e.target));\n };\n};\n"],"names":["async","clozeButtonText","buttonImage","Promise","all","component","editor","body","document","querySelector","id","indexOf","ui","registry","addIcon","icon","html","addToggleButton","clozeeditButtonName","tooltip","onAction","onSetup","api","on","setActive","addMenuItem","text","format","e","target"],"mappings":"8PAgDwBA,gBAEhBC,gBACAC,mBACMC,QAAQC,IAAI,EAClB,mBAAU,aAAcC,oBACxB,yBAAe,OAAQA,4BAGnBC,eAEEC,KAAOC,SAASC,cAAc,wHAK/BF,OAA+C,IAAvCD,OAAOI,GAAGC,QAAQ,mBAKY,IAAvCJ,KAAKG,GAAGC,QAAQ,2DACWL,QAI/BA,OAAOM,GAAGC,SAASC,QAAQC,aAAMb,YAAYc,MAG7CV,OAAOM,GAAGC,SAASI,gBAAgBC,4BAAqB,CACpDH,KAAAA,aACAI,QAASlB,gBACTmB,SAAU,KAAM,yBAChBC,QAAUC,MACNhB,OAAOiB,GAAG,SAAS,KACdD,IAAIE,WAAmC,KAAzB,mCAM3BlB,OAAOM,GAAGC,SAASY,YAAYP,4BAAqB,CAChDH,KAAAA,aACAW,KAAMzB,gBACNmB,SAAU,KAAM,2BAGpBd,OAAOiB,GAAG,QAAQ,KAAM,cAAOjB,UAC/BA,OAAOiB,GAAG,oBAAoBI,SAAU,0BAAmBA,UAC3DrB,OAAOiB,GAAG,UAAU,KAAM,oBAC1BjB,OAAOiB,GAAG,YAAaK,IAAM,8BAAuBA,EAAEC"} \ No newline at end of file diff --git a/amd/build/options.min.js b/amd/build/options.min.js index f7743df..dc96050 100644 --- a/amd/build/options.min.js +++ b/amd/build/options.min.js @@ -1,4 +1,4 @@ -define("tiny_cloze/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.hasQtypeMultianswerrgx=void 0; +define("tiny_cloze/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.hasQtypeMultianswerrgx=_exports.disableQtypeMultianswerrgx=void 0; /** * Options helper for tiny_cloze plugin. * @@ -6,6 +6,6 @@ define("tiny_cloze/options",["exports","editor_tiny/options","./common"],(functi * @copyright 2024 Stephan Robotta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const multianswerrgx=(0,_options.getPluginOptionName)(_common.pluginName,"multianswerrgx");_exports.register=editor=>{editor.options.register(multianswerrgx,{processor:"boolean",default:!1})};_exports.hasQtypeMultianswerrgx=editor=>editor.options.get(multianswerrgx)})); +const multianswerrgx=(0,_options.getPluginOptionName)(_common.pluginName,"multianswerrgx");_exports.register=editor=>{editor.options.register(multianswerrgx,{processor:"boolean",default:!1})};_exports.hasQtypeMultianswerrgx=editor=>editor.options.get(multianswerrgx);_exports.disableQtypeMultianswerrgx=editor=>editor.options.set(multianswerrgx,!1)})); //# 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 index fe396e5..229dcb0 100644 --- a/amd/build/options.min.js.map +++ b/amd/build/options.min.js.map @@ -1 +1 @@ -{"version":3,"file":"options.min.js","sources":["../src/options.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\n/**\n * Options helper for tiny_cloze plugin.\n *\n * @module tiny_cloze\n * @copyright 2024 Stephan Robotta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst multianswerrgx = getPluginOptionName(pluginName, 'multianswerrgx');\n\n/**\n * Register the options for the Tiny Cloze question plugin.\n *\n * @param {tinymce.Editor} editor\n */\nexport const register = (editor) => {\n editor.options.register(multianswerrgx, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\nexport const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx);"],"names":["multianswerrgx","pluginName","editor","options","register","processor","get"],"mappings":";;;;;;;;MA0BMA,gBAAiB,gCAAoBC,mBAAY,oCAO9BC,SACrBA,OAAOC,QAAQC,SAASJ,eAAgB,CACpCK,UAAW,mBACA,qCAIoBH,QAAWA,OAAOC,QAAQG,IAAIN"} \ No newline at end of file +{"version":3,"file":"options.min.js","sources":["../src/options.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\n/**\n * Options helper for tiny_cloze plugin.\n *\n * @module tiny_cloze\n * @copyright 2024 Stephan Robotta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst multianswerrgx = getPluginOptionName(pluginName, 'multianswerrgx');\n\n/**\n * Register the options for the Tiny Cloze question plugin.\n *\n * @param {tinymce.Editor} editor\n */\nexport const register = (editor) => {\n editor.options.register(multianswerrgx, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\nexport const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx);\nexport const disableQtypeMultianswerrgx = (editor) => editor.options.set(multianswerrgx, false);"],"names":["multianswerrgx","pluginName","editor","options","register","processor","get","set"],"mappings":";;;;;;;;MA0BMA,gBAAiB,gCAAoBC,mBAAY,oCAO9BC,SACrBA,OAAOC,QAAQC,SAASJ,eAAgB,CACpCK,UAAW,mBACA,qCAIoBH,QAAWA,OAAOC,QAAQG,IAAIN,oDAC1BE,QAAWA,OAAOC,QAAQI,IAAIP,gBAAgB"} \ No newline at end of file diff --git a/amd/src/commands.js b/amd/src/commands.js index 534296e..ac40930 100644 --- a/amd/src/commands.js +++ b/amd/src/commands.js @@ -28,7 +28,15 @@ import { clozeeditButtonName, icon, } from './common'; -import {displayDialogue, displayDialogueForEdit, resolveSubquestion, onInit, onBeforeGetContent, onSubmit} from './ui'; +import { + displayDialogue, + displayDialogueForEdit, + resolveSubquestion, + onInit, + onBeforeGetContent, + onSubmit +} from './ui'; +import {disableQtypeMultianswerrgx} from './options'; /** * Get the setup function for the buttons. @@ -49,15 +57,19 @@ export const getSetup = async() => { return (editor) => { // Check whether we are editing a question. - const body = document.querySelector('body#page-question-type-multianswer form, ' + - 'body#page-question-type-multianswerwiris form,' + - 'body#page-question-type-multianswerrgx form' + const body = document.querySelector('body#page-question-type-multianswer, ' + + 'body#page-question-type-multianswerwiris,' + + 'body#page-question-type-multianswerrgx' ); // And if the editor is used on the question text. if (!body || editor.id.indexOf('questiontext') === -1) { return; } - // Only if both conditions are valid, then continue setting up the plugin. + // Only if all conditions are valid, then continue setting up the plugin. + // However, if we have not a body#page-question-type-multianswerrgx then disable the regex types. + if (body.id.indexOf('multianswerrgx') === -1) { + disableQtypeMultianswerrgx(editor); + } // Register the Moodle SVG as an icon suitable for use as a TinyMCE toolbar button. editor.ui.registry.addIcon(icon, buttonImage.html); diff --git a/amd/src/options.js b/amd/src/options.js index 65c939a..7baa34e 100644 --- a/amd/src/options.js +++ b/amd/src/options.js @@ -38,4 +38,5 @@ export const register = (editor) => { }); }; -export const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx); \ No newline at end of file +export const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx); +export const disableQtypeMultianswerrgx = (editor) => editor.options.set(multianswerrgx, false); \ No newline at end of file From deb19bd06ef27403654c779ab42e6db684a7121a Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Sun, 13 Oct 2024 12:57:40 +0200 Subject: [PATCH 3/5] Remove unnecessary parameter passing when fetching strings. --- amd/build/ui.min.js | 2 +- amd/build/ui.min.js.map | 2 +- amd/src/ui.js | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/amd/build/ui.min.js b/amd/build/ui.min.js index 84093bf..31e6721 100644 --- a/amd/build/ui.min.js +++ b/amd/build/ui.min.js @@ -5,6 +5,6 @@ define("tiny_cloze/ui",["exports","core/modal_events","core/modal","core/modal_f * @module tiny_cloze/ui * @copyright 2023 MoodleDACH * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.resolveSubquestion=_exports.onSubmit=_exports.onInit=_exports.onBeforeGetContent=_exports.displayDialogueForEdit=_exports.displayDialogue=void 0,_modal_events=_interopRequireDefault(_modal_events),_modal2=_interopRequireDefault(_modal2),_modal_factory=_interopRequireDefault(_modal_factory),_mustache=_interopRequireDefault(_mustache);const isNull=a=>null==a,strdecode=t=>String(t).replace(/\\(#|\}|~)/g,"$1"),strencode=t=>String(t).replace(/(#|\}|~)/g,"\\$1"),indexOfNode=(list,node)=>{for(let i=0;i{const attrSel=' selected="selected"';let isSel="="===s?attrSel:"",html='");return FRACTIONS.forEach((item=>{isSel=item.value.toString()===s?attrSel:"",html+='")})),isSel=""!==s&&-1===html.indexOf(attrSel)?attrSel:"",html+='"),html},isCustomGrade=s=>{if("="===s||""===s)return!1;let found=!1;return FRACTIONS.forEach((item=>{item.value.toString()===s&&(found=!0)})),!found},markerClass="cloze-question-marker",markerSpan='',CSS={ANSWER:"tiny_cloze_answer",ANSWERS:"tiny_cloze_answers",ADD:"tiny_cloze_add",CANCEL:"tiny_cloze_cancel",DELETE:"tiny_cloze_delete",FEEDBACK:"tiny_cloze_feedback",FRACTION:"tiny_cloze_fraction",FRAC_CUSTOM:"tiny_cloze_frac_custom",LEFT:"tiny_cloze_col0",LOWER:"tiny_cloze_down",RIGHT:"tiny_cloze_col1",MARKS:"tiny_cloze_marks",DUPLICATE:"tiny_cloze_duplicate",RAISE:"tiny_cloze_up",SUBMIT:"tiny_cloze_submit",SUMMARY:"tiny_cloze_summary",TOLERANCE:"tiny_cloze_tolerance",TYPE:"tiny_cloze_qtype"},TEMPLATE={FORM:'

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

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

{{STR.chooseqtypetoadd}}

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

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

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

{{STR.chooseqtypetoadd}}

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

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

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

{{STR.chooseqtypetoadd}}

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

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

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

{{STR.chooseqtypetoadd}}

' +\n '
' +\n '
' +\n '{{#types}}' +\n '
' +\n '' +\n '
' +\n '{{/types}}
' +\n '
',\n FOOTER: '' +\n '',\n};\nconst FRACTIONS = [\n {value: 100},\n {value: 50},\n {value: 0},\n];\n\n// Language strings used in the modal dialogue.\nconst STR = {};\n\n/**\n * The editor instance that is injected via the onInit() function.\n *\n * @type {tinymce.Editor}\n * @private\n */\nlet _editor = null;\n\n/**\n * A reference to the currently open form.\n *\n * @param _form\n * @type {Node}\n * @private\n */\nlet _form = null;\n\n/**\n * An array containing the current answers options\n *\n * @param _answerdata\n * @type {Array}\n * @private\n */\nlet _answerdata = [];\n\n/**\n * The sub question type to be edited\n *\n * @param _qtype\n * @type {string|null}\n * @private\n */\nlet _qtype = null;\n\n/**\n * Remember the pos of the selected node.\n * @type {number}\n * @private\n */\nlet _selectedOffset = -1;\n\n/**\n * The maximum marks for the sub question\n *\n * @param _marks\n * @type {Integer}\n * @private\n */\nlet _marks = 1;\n\n/**\n * The modal dialogue to be displayed when designing the cloze question types.\n * @type {Modal|null}\n */\nlet _modal = null;\n\n/**\n * If its a normal selection of text, use it for the first answer field.\n * @type {string|null}\n */\nlet _firstAnswer = null;\n\n/**\n * Inject the editor instance and add markers to the cloze question texts.\n * @param {tinymce.Editor} ed\n */\nconst onInit = function(ed) {\n _editor = ed; // The current editor instance.\n // Add the marker spans.\n _addMarkers();\n // And get the language strings.\n _getStr();\n};\n\n/**\n * Regex to recognize the question string in the text e.g. {1:NUMERICAL:...} or {:MULTICHOICE:...}\n * @param {tinymce.Editor} editor\n * @return {RegExp}\n * @private\n */\nconst _getRegexQtype = (editor) => {\n // eslint-disable-next-line max-len\n const baseQtypes = 'MULTICHOICE(_H|_V|_S|_HS|_VS)?|MULTIRESPONSE(_H|_S|_HS)?|NUMERICAL|SHORTANSWER(_C)?|SAC?|NM|MWC?|M[CR](V|H|VS|HS)?';\n const extQtypes = hasQtypeMultianswerrgx(editor) ? '|REGEXP(_C)?|RXC?' : '';\n return new RegExp('\\\\{([0-9]*):(' + baseQtypes + extQtypes + '):(.*?)(? {\n let strToFetch = [\n {key: 'answer', component: 'question'},\n {key: 'chooseqtypetoadd', component: 'question'},\n {key: 'defaultmark', component: 'question'},\n {key: 'feedback', component: 'question'},\n {key: 'correct', component: 'question'},\n {key: 'incorrect', component: 'question'},\n {key: 'addmoreanswerblanks', component: 'qtype_calculated'},\n {key: 'delete', component: 'core'},\n {key: 'up', component: 'core'},\n {key: 'down', component: 'core'},\n {key: 'tolerance', component: 'qtype_calculated'},\n {key: 'grade', component: 'grades'},\n {key: 'caseno', component: 'mod_quiz'},\n {key: 'caseyes', component: 'mod_quiz'},\n {key: 'answersingleno', component: 'qtype_multichoice'},\n {key: 'answersingleyes', component: 'qtype_multichoice'},\n {key: 'layoutselectinline', component: 'qtype_multianswer'},\n {key: 'layouthorizontal', component: 'qtype_multianswer'},\n {key: 'layoutvertical', component: 'qtype_multianswer'},\n {key: 'shufflewithin', component: 'mod_quiz'},\n {key: 'layoutmultiple_horizontal', component: 'qtype_multianswer'},\n {key: 'layoutmultiple_vertical', component: 'qtype_multianswer'},\n {key: 'pluginnamesummary', component: 'qtype_multichoice'},\n {key: 'pluginnamesummary', component: 'qtype_shortanswer'},\n {key: 'pluginnamesummary', component: 'qtype_numerical'},\n {key: 'multichoice', component},\n {key: 'multiresponse', component},\n {key: 'numerical', component: 'mod_quiz'},\n {key: 'shortanswer', component: 'mod_quiz'},\n {key: 'cancel', component: 'core'},\n {key: 'select', component},\n {key: 'insert', component},\n {key: 'pluginname', component},\n {key: 'customgrade', component},\n {key: 'err_custom_rate', component},\n {key: 'err_empty_answer', component},\n {key: 'err_none_correct', component},\n {key: 'err_not_numeric', component},\n ];\n let langKeys = [\n 'answer',\n 'chooseqtypetoadd',\n 'defaultmark',\n 'feedback',\n 'correct',\n 'incorrect',\n 'addmoreanswerblanks',\n 'delete',\n 'up',\n 'down',\n 'tolerance',\n 'grade',\n 'caseno',\n 'caseyes',\n 'singleno',\n 'singleyes',\n 'selectinline',\n 'horizontal',\n 'vertical',\n 'shuffle',\n 'multi_horizontal',\n 'multi_vertical',\n 'summary_multichoice',\n 'summary_shortanswer',\n 'summary_numerical',\n 'multichoice',\n 'multiresponse',\n 'numerical',\n 'shortanswer',\n 'btn_cancel',\n 'btn_select',\n 'btn_insert',\n 'title',\n 'custom_grade',\n 'err_custom_rate',\n 'err_empty_answer',\n 'err_none_correct',\n 'err_not_numeric',\n ];\n if (hasQtypeMultianswerrgx(_editor)) {\n strToFetch.push({key: 'regexp', component: 'qtype_regexp'});\n strToFetch.push({key: 'pluginnamesummary', component: 'qtype_regexp'});\n langKeys.push('regexp');\n langKeys.push('summary_regexp');\n }\n getStrings(strToFetch).then(function() {\n const args = Array.from(arguments);\n langKeys.map((l, i) => {\n STR[l] = args[0][i];\n return ''; // Make the linter happy.\n });\n return ''; // Make the linter happy.\n }).catch(() => {\n return '';\n });\n};\n\n/**\n * Return the question types that are available for the cloze question.\n * @returns {Array}\n * @private\n */\nconst _getQuestionTypes = function() {\n let qtypes = [\n {\n 'type': 'MULTICHOICE',\n 'abbr': ['MC'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.selectinline, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_H',\n 'abbr': ['MCH'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.horizontal, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_V',\n 'abbr': ['MCV'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.vertical, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_S',\n 'abbr': ['MCS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.selectinline, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_HS',\n 'abbr': ['MCHS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.horizontal, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTICHOICE_VS',\n 'abbr': ['MCVS'],\n 'name': STR.multichoice,\n 'summary': STR.summary_multichoice,\n 'options': [STR.vertical, STR.shuffle, STR.singleyes],\n },\n {\n 'type': 'MULTIRESPONSE',\n 'abbr': ['MR'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_vertical, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_H',\n 'abbr': ['MRH'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_horizontal, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_S',\n 'abbr': ['MRS'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_vertical, STR.shuffle, STR.singleno],\n },\n {\n 'type': 'MULTIRESPONSE_HS',\n 'abbr': ['MRHS'],\n 'name': STR.multiresponse,\n 'summary': STR.summary_multichoice,\n 'options': [STR.multi_horizontal, STR.shuffle, STR.singleno],\n },\n {\n 'type': 'NUMERICAL',\n 'abbr': ['NM'],\n 'name': STR.numerical,\n 'summary': STR.summary_numerical,\n },\n {\n 'type': 'SHORTANSWER',\n 'abbr': ['SA', 'MW'],\n 'name': STR.shortanswer,\n 'summary': STR.summary_shortanswer,\n 'options': [STR.caseno],\n },\n {\n 'type': 'SHORTANSWER_C',\n 'abbr': ['SAC', 'MWC'],\n 'name': STR.shortanswer,\n 'summary': STR.summary_shortanswer,\n 'options': [STR.caseyes],\n },\n ];\n if (hasQtypeMultianswerrgx(_editor)) {\n qtypes.splice(11, 0, {\n 'type': 'REGEXP',\n 'abbr': ['RX'],\n 'name': STR.regexp,\n 'summary': STR.summary_regexp,\n 'options': [STR.caseno],\n }, {\n 'type': 'REGEXP_C',\n 'abbr': ['RXC'],\n 'name': STR.regexp,\n 'summary': STR.summary_regexp,\n 'options': [STR.caseyes],\n });\n }\n return qtypes;\n};\n\n/**\n * Create the modal.\n * @return {Promise}\n * @private\n */\nconst _createModal = async function() {\n // Create the modal dialogue. Depending on whether we have a selected node or not, the content is different.\n const cfg = {\n title: STR.title,\n templateContext: {\n elementid: _editor.id\n },\n removeOnClose: true,\n large: true,\n };\n if (typeof Modal.create === 'function') {\n _modal = await Modal.create(cfg);\n } else {\n _modal = await ModalFactory.create(cfg);\n }\n};\n\n/**\n * Display modal dialogue to edit a cloze question. Either a form is displayed to edit subquestion or a list\n * of possible questions is show.\n *\n * @method displayDialogue\n * @public\n */\nconst displayDialogue = async function() {\n await _createModal();\n\n // Resolve whether cursor is in a subquestion.\n const subquestion = resolveSubquestion();\n if (subquestion) {\n _firstAnswer = null;\n // Subquestion found, remember which node of the marker nodes is selected.\n _selectedOffset = indexOfNode(_editor.dom.select('.' + markerClass), subquestion);\n _parseSubquestion(subquestion.innerHTML);\n _setDialogueContent(_qtype);\n } else {\n // No subquestion found, no offset to remember.\n _firstAnswer = _editor.selection.getContent();\n _selectedOffset = -1;\n _setDialogueContent();\n }\n};\n\n/**\n * On double click, check that we are on a question and display the dialogue with the question to edit.\n * @method displayDialogueForEdit\n * @param {Node} target\n * @public\n */\nconst displayDialogueForEdit = async function(target) {\n\n const subquestion = resolveSubquestion(target);\n if (!subquestion) {\n return;\n }\n await _createModal();\n _selectedOffset = indexOfNode(_editor.dom.select('.' + markerClass), subquestion);\n _parseSubquestion(subquestion.innerHTML);\n _setDialogueContent(_qtype);\n};\n\n/**\n * Search for cloze questions based on a regular expression. All the matching snippets at least contain the cloze\n * question definition. Although Moodle does not support encapsulated other functions within curly brackets, we\n * still try to find the correct closing bracket. The so extracted cloze question is surrounded by a marker span\n * element, that contains attributes so that the content inside the span cannot be modified by the editor (in the\n * textarea). Also, this makes it a lot easier to select the question, edit it in the dialogue and replace the result\n * in the existing text area.\n *\n * @method _addMarkers\n * @private\n */\nconst _addMarkers = function() {\n\n let content = _editor.getContent();\n let newContent = '';\n\n // Check if there is already a marker span. In this case we do not have to do anything.\n if (content.indexOf(markerClass) !== -1) {\n return;\n }\n\n let m;\n do {\n m = content.match((_getRegexQtype(_editor)));\n if (!m) { // No match of a cloze question, then we are done.\n newContent += content;\n break;\n }\n // Copy the current match to the new string preceded with the .\n const pos = content.indexOf(m[0]);\n newContent += content.substring(0, pos) + markerSpan + content.substring(pos, pos + m[0].length);\n content = content.substring(pos + m[0].length);\n\n // Count the { in the string, should be just one (the very first one at position 0).\n let level = (m[0].match(/\\{/g) || []).length;\n if (level === 1) {\n // If that's the case, we close the span and the cloze question text is the innerHTML of that marker span.\n newContent += '';\n continue; // Look for the next matching cloze question.\n }\n // If there are more { than } in the string, then we did not find the corresponding } that belongs to the cloze string.\n while (level > 1) {\n const a = content.indexOf('{');\n const b = content.indexOf('}');\n if (a > -1 && b > -1 && a < b) { // The { is before another } so remember to find as many } until we back at level 1.\n level++;\n newContent = content.substring(0, a);\n content = content.substring(a + 1);\n } else if (b > -1) { // We found a closing } to a previously {.\n newContent = content.substring(0, b);\n content = content.substring(b + 1);\n level--;\n } else {\n level = 1; // Should not happen, just to stop the endless loop.\n }\n }\n newContent += '
';\n } while (m);\n _editor.setContent(newContent);\n};\n\n/**\n * Look for the marker span elements around a cloze question and remove that span. Also, the marker for a new\n * node to be inserted would be removed here as well.\n */\nconst _removeMarkers = function() {\n for (const span of _editor.dom.select('span.' + markerClass)) {\n _editor.dom.setOuterHTML(span, span.classList.contains('new') ? '' : span.innerHTML);\n }\n};\n\n/**\n * When the source code view dialogue is show, we must remove the spans around the cloze question strings\n * from the editor content and add them again when the dialogue is closed.\n * Since this event is also triggered when the editor data is saved, we use this function to remove the\n * highlighting content at that time.\n *\n * @method onBeforeGetContent\n * @param {object} content\n * @public\n */\nconst onBeforeGetContent = function(content) {\n if (!isNull(content.source_view) && content.source_view === true) {\n // If the user clicks on 'Cancel' or the close button on the html\n // source code dialog view, make sure we re-add the visual styling.\n var onClose = function() {\n _editor.off('close', onClose);\n _addMarkers();\n };\n _editor.on('CloseWindow', () => {\n onClose();\n });\n // Remove markers only if modal is not called, otherwise we will lose our new question marker.\n if (!_modal) {\n _removeMarkers();\n }\n }\n};\n\n/**\n * Fires when the form containing the editor is submitted.\n *\n * @method onSubmit\n * @public\n */\nconst onSubmit = function() {\n _removeMarkers();\n};\n\n/**\n * Set the dialogue content for the tool, attaching any required events. Either the modal dialogue displays\n * a list of the question types for the form for a particular question to edit. The set content is also\n * called when the form has changed (up or down move, deletion and adding a response). We must be aware of that\n * an event to the dialogue buttons must be attached once only. Therefore, when the form content is modified, only\n * the form events for the answers are set again, the general events are nor (nomodalevents is true then).\n *\n * @method _setDialogueContent\n * @param {String} qtype The question type to be used\n * @param {boolean} nomodalevents Optional do not attach events.\n * @private\n */\nconst _setDialogueContent = function(qtype, nomodalevents) {\n const footer = Mustache.render(TEMPLATE.FOOTER, {\n cancel: STR.btn_cancel,\n submit: !qtype ? STR.btn_select : STR.btn_insert,\n });\n let contentText;\n if (!qtype) {\n contentText = Mustache.render(TEMPLATE.TYPE, {\n CSS: CSS,\n STR: STR,\n qtype: _qtype,\n types: _getQuestionTypes()\n });\n } else {\n contentText = Mustache.render(TEMPLATE.FORM, {\n CSS: CSS,\n STR: STR,\n answerdata: _answerdata,\n elementid: getUuid(),\n qtype: _qtype,\n name: _getQuestionTypes().filter(q => _qtype === q.type)[0].name,\n marks: _marks,\n numerical: (_qtype === 'NUMERICAL' || _qtype === 'NM')\n });\n }\n _modal.setBody(contentText);\n _modal.setFooter(footer);\n _modal.show();\n const $root = _modal.getRoot();\n _form = $root.get(0).querySelector('form');\n _toggleDeleteIcon();\n\n if (!nomodalevents) {\n _modal.registerEventListeners();\n _modal.registerCloseOnSave();\n _modal.registerCloseOnCancel();\n $root.on(ModalEvents.cancel, _cancel);\n\n if (!qtype) { // For the question list we need the choice handler only, and we are done.\n $root.on(ModalEvents.save, _choiceHandler);\n return;\n } // Handler to add the question string to the editor content.\n $root.on(ModalEvents.save, _setSubquestion);\n }\n // The form needs events for the icons to move up/down, add or delete a response.\n const getTarget = e => {\n let p = e.target;\n while (!isNull(p) && p.nodeType === 1 && p.tagName !== 'A') {\n p = p.parentNode;\n }\n if (isNull(p.classList)) {\n return null;\n }\n return p;\n };\n\n _form.addEventListener('click', e => {\n const p = getTarget(e);\n if (isNull(p)) {\n return;\n }\n if (p.classList.contains(CSS.DELETE)) {\n e.preventDefault();\n _deleteAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.ADD)) {\n e.preventDefault();\n _addAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.LOWER)) {\n e.preventDefault();\n _lowerAnswer(p);\n return;\n }\n if (p.classList.contains(CSS.RAISE)) {\n e.preventDefault();\n _raiseAnswer(p);\n }\n });\n _form.addEventListener('keyup', e => {\n const p = getTarget(e);\n if (isNull(p)) {\n return;\n }\n if (p.classList.contains(CSS.ANSWER) || p.classList.contains(CSS.FEEDBACK)) {\n e.preventDefault();\n _addAnswer(p);\n }\n });\n _form.querySelectorAll('.' + CSS.FRACTION).forEach((sel) => {\n sel.addEventListener('change', e => {\n const id = e.target.getAttribute('id');\n if (e.target.value === selectCustomPercent) {\n document.getElementById(id + '_custom').parentNode.classList.remove('hidden');\n } else {\n document.getElementById(id + '_custom').parentNode.classList.add('hidden');\n }\n });\n });\n};\n\n/**\n * If there is one answer field, hide the delete icon. Otherwise show them\n * all to allow deletion of any answer.\n *\n * @private\n */\nconst _toggleDeleteIcon = function() {\n const deleteIcons = _form.querySelectorAll('.' + CSS.DELETE);\n if (deleteIcons.length === 1) {\n deleteIcons[0].classList.add('hidden');\n return;\n }\n for (let i = 0; i < deleteIcons.length; i++) {\n deleteIcons[i].classList.remove('hidden');\n }\n};\n\n/**\n * Handle question choice.\n *\n * @method _choiceHandler\n * @private\n * @param {Event} e Event from button click in chooser\n */\nconst _choiceHandler = function(e) {\n e.preventDefault();\n let qtype = _form.querySelector('input[name=qtype]:checked');\n if (qtype) {\n _qtype = qtype.value;\n }\n // For numerical and short answer questions (and when installed regexp) we offer one response field only.\n // All other question types have three empty response fields.\n const max = (_qtype.indexOf('SHORTANSWER') !== -1 || _qtype === 'NUMERICAL' || _qtype.indexOf('REGEXP') !== -1) ? 1 : 3;\n const blankAnswer = {\n id: getUuid(),\n answer: '',\n feedback: '',\n fraction: 100,\n fractionOptions: getFractionOptions(''),\n tolerance: 0,\n isCustomGrade: false,\n };\n _answerdata = [];\n for (let x = 0; x < max; x++) {\n _answerdata.push({...blankAnswer, id: getUuid()});\n }\n // The first response field gets the default grade correct.\n _answerdata[0].fractionOptions = getFractionOptions('=');\n // In case the user seleced some text, this is used as the first answer.\n if (_firstAnswer) {\n _answerdata[0].answer = _firstAnswer;\n }\n _modal.destroy();\n // Our choice is stored in _qtype. We need to create the modal dialogue with the form now.\n _createModal().then(() => {\n _setDialogueContent(_qtype);\n _form.querySelector('.' + CSS.ANSWER).focus();\n return ''; // Make the linter happy.\n }).catch(() => {\n return '';\n });\n};\n\n/**\n * Parse question and set properties found.\n *\n * @method _parseSubquestion\n * @private\n * @param {String} question The question string\n */\nconst _parseSubquestion = function(question) {\n _answerdata = []; // Flush answers to have an empty dialogue if something goes wrong parsing the question string.\n const regexQtype = _getRegexQtype(_editor);\n const parts = regexQtype.exec(question);\n regexQtype.lastIndex = 0; // Reset lastIndex so that the next match starts from the beginning of the question string.\n if (!parts) {\n return;\n }\n _marks = parts[1];\n _qtype = parts[2];\n // Convert the short notation to the long form e.g. SA to SHORTANSWER.\n if (_qtype.length < 5) {\n _getQuestionTypes().forEach(l => {\n for (const a of l.abbr) {\n if (a === _qtype) {\n _qtype = l.type;\n return;\n }\n }\n });\n }\n // Depending on the regex the position of the answers is different.\n const answers = parts[hasQtypeMultianswerrgx(_editor) ? 8 : 7].match(/(\\\\.|[^~])*/g);\n if (!answers) {\n return;\n }\n answers.forEach(function(answer) {\n const options = /^(%(-?[.0-9]+)%|(=?))((\\\\.|[^#])*)#?(.*)/.exec(answer);\n if (options && options[4]) {\n let frac = '';\n if (options[3]) {\n frac = options[3] === '=' ? '=' : 100;\n } else if (options[2]) {\n frac = options[2];\n }\n if (_qtype === 'NUMERICAL' || _qtype === 'NM') {\n const tolerance = /^([^:]*):?(.*)/.exec(options[4])[2] || 0;\n _answerdata.push({\n id: getUuid(),\n answer: strdecode(options[4].replace(/:.*/, '')),\n feedback: strdecode(options[6]),\n tolerance: tolerance,\n fraction: frac,\n fractionOptions: getFractionOptions(frac),\n isCustomGrade: isCustomGrade(frac),\n });\n return;\n }\n _answerdata.push({\n answer: strdecode(options[4]),\n id: getUuid(),\n feedback: strdecode(options[6]),\n fraction: frac,\n fractionOptions: getFractionOptions(frac),\n isCustomGrade: isCustomGrade(frac),\n });\n }\n });\n};\n\n/**\n * Insert a new set of answer blanks below the button.\n *\n * @method _addAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _addAnswer = function(a) {\n let index = indexOfNode(_form.querySelectorAll('.' + CSS.ADD), a);\n if (index === -1) {\n index = 0;\n }\n let fraction = '';\n let answer = '';\n let feedback = '';\n let tolerance = 0;\n if (a.closest('li')) {\n fraction = a.closest('li').querySelector('.' + CSS.FRACTION).value;\n if (fraction === selectCustomPercent) {\n fraction = a.closest('li').querySelector('.' + CSS.FRAC_CUSTOM).value;\n }\n answer = a.closest('li').querySelector('.' + CSS.ANSWER).value;\n feedback = a.closest('li').querySelector('.' + CSS.FEEDBACK).value;\n if (a.closest('li').querySelector('.' + CSS.TOLERANCE)) {\n tolerance = a.closest('li').querySelector('.' + CSS.TOLERANCE).value;\n }\n }\n _processFormData();\n _answerdata.splice(index, 0, {\n id: getUuid(),\n answer: answer,\n feedback: feedback,\n fraction: fraction,\n fractionOptions: getFractionOptions(fraction),\n tolerance: tolerance,\n isCustomGrade: isCustomGrade(fraction)\n });\n _setDialogueContent(_qtype, true);\n _toggleDeleteIcon();\n _form.querySelectorAll('.' + CSS.ANSWER).item(index).focus();\n};\n\n/**\n * Delete set of answer next to the button.\n *\n * @method _deleteAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _deleteAnswer = function(a) {\n let index = indexOfNode(_form.querySelectorAll('.' + CSS.DELETE), a);\n if (index === -1) {\n index = indexOfNode(_form.querySelectorAll('li'), a.closest('li'));\n }\n _processFormData();\n _answerdata.splice(index, 1);\n _setDialogueContent(_qtype, true);\n const answers = _form.querySelectorAll('.' + CSS.ANSWER);\n index = Math.min(index, answers.length - 1);\n answers.item(index).focus();\n _toggleDeleteIcon();\n};\n\n/**\n * Lower answer option\n *\n * @method _lowerAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _lowerAnswer = function(a) {\n const li = a.closest('li');\n li.before(li.nextSibling);\n li.querySelector('.' + CSS.ANSWER).focus();\n};\n\n/**\n * Raise answer option\n *\n * @method _raiseAnswer\n * @param {Node} a Node that is the referred element\n * @private\n */\nconst _raiseAnswer = function(a) {\n const li = a.closest('li');\n li.after(li.previousSibling);\n li.querySelector('.' + CSS.ANSWER).focus();\n};\n\n/**\n * Reset and hide form.\n *\n * @method _cancel\n * @param {Event} e Event from button click\n * @private\n */\nconst _cancel = function(e) {\n e.preventDefault();\n // In case there is a marker where the new question should be inserted in the text it needs to be removed.\n for (const span of _editor.dom.select('.' + markerClass + '.new')) {\n span.remove();\n }\n _modal.destroy();\n _editor.focus();\n _modal = null;\n};\n\n/**\n * Insert question string into editor content and reset and hide form. If the form contains an error\n * nothing happens.\n *\n * @method _setSubquestion\n * @param {Event} e Event from button click\n * @private\n */\nconst _setSubquestion = function(e) {\n e.preventDefault();\n // Check if there are any errors and if so, fill the error container with the\n // messages and return without going any further and closing the dialogue.\n const errMsg = _form.querySelector('.msg-error');\n const formErrors = _processFormData(true);\n if (formErrors.length > 0) {\n errMsg.innerHTML = '
  • ' + formErrors.join('
  • ') + '
';\n errMsg.classList.remove('hidden');\n return;\n } else {\n errMsg.classList.add('hidden');\n }\n // Build the parser function from the data, that is going to be placed into the editor content.\n let question = '{' + _marks + ':' + _qtype + ':';\n\n // Filter all empty responses\n for (let i = 0; i < _answerdata.length; i++) {\n if (_answerdata[i].raw === '') {\n continue;\n }\n question += _answerdata[i].fraction && !isNaN(_answerdata[i].fraction)\n ? '%' + _answerdata[i].fraction + '%' : _answerdata[i].fraction;\n question += strencode(_answerdata[i].answer);\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n question += ':' + _answerdata[i].tolerance;\n }\n if (_answerdata[i].feedback) {\n question += '#' + strencode(_answerdata[i].feedback);\n }\n if (i < _answerdata.length - 1) {\n question += '~';\n }\n }\n if (question.slice(-1) === '~') {\n question = question.substring(0, question.length - 1);\n }\n question += '}';\n\n _modal.destroy();\n _modal = null;\n _editor.focus();\n if (_selectedOffset > -1) { // We have to replace one of the marker spans (the innerHTML contains the question string).\n _editor.dom.select('.' + markerClass)[_selectedOffset].innerHTML = question;\n } else {\n // Just add the question text with markup.\n _editor.insertContent(markerSpan + question + '');\n }\n};\n\n/**\n * Read the form data, process it and store the result in the internal _answerdata array.\n * Also, if validation is enabled, the fields are checked for invalid values e.g.\n * - answer field is empty (if a correct answer is contained, empty fields are eliminated).\n * - custom_grade field whenin use and does not contain a number.\n * - no field is marked as a correct answer.\n * - tolerance field must be in percentage of min -100 and max 100.\n * Any field with an error is maked and the first field containing an error gets the focus.\n *\n * @method _processFormData\n * @param {boolean} validate\n * @return {Array}\n * @private\n */\nconst _processFormData = function(validate) {\n _answerdata = [];\n let globalErrors = [];\n const answers = _form.querySelectorAll('.' + CSS.ANSWER);\n const feedbacks = _form.querySelectorAll('.' + CSS.FEEDBACK);\n const fractions = _form.querySelectorAll('.' + CSS.FRACTION);\n const customGrades = _form.querySelectorAll('.' + CSS.FRAC_CUSTOM);\n const tolerances = _form.querySelectorAll('.' + CSS.TOLERANCE);\n // Remove any error classes.\n for (let i = 0; i < answers.length; i++) {\n answers.item(i).classList.remove('error');\n customGrades.item(i).classList.remove('error');\n const currentAnswer = {\n raw: answers.item(i).value.trim(),\n answer: answers.item(i).value.trim(),\n id: getUuid(),\n feedback: feedbacks.item(i).value,\n fraction: fractions.item(i).value === selectCustomPercent ? customGrades.item(i).value : fractions.item(i).value,\n fractionOptions: getFractionOptions(fractions.item(i).value),\n tolerance: tolerances.length > 0 ? tolerances.item(i).value : 0,\n isCustomGrade: fractions.item(i).value === selectCustomPercent\n };\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n tolerances.item(i).classList.remove('error');\n // In numeric questions convert answer and tolerance to numeric values (this filters non numeric values).\n currentAnswer.answer = Number(currentAnswer.answer);\n currentAnswer.tolerance = Number(currentAnswer.tolerance);\n }\n _answerdata.push(currentAnswer);\n }\n _marks = _form.querySelector('.' + CSS.MARKS).value;\n\n if (validate) {\n const {hasCorrectAnswer, errors} = _validateAnswers();\n for (let i = 0; i < _answerdata.length; i++) {\n for (const err of _answerdata[i].hasErrors) {\n if (hasCorrectAnswer && (err === 'empty_answer' || err === 'correct_but_empty')) {\n break;\n }\n if (err === 'answer_not_numeric' || err === 'empty_answer' || err === 'correct_but_empty') {\n answers.item(i).classList.add('error');\n } else if (err === 'tolerance_not_numeric') {\n tolerances.item(i).classList.add('error');\n } else if (err === 'error_custom_rate') {\n customGrades.item(i).classList.add('error');\n }\n }\n }\n globalErrors = _translateGlobalErrors(hasCorrectAnswer, errors);\n // If we have errors, we focus the first field that contains an error.\n if (globalErrors.length > 0) {\n _form.querySelector('input.error').focus();\n }\n }\n return globalErrors;\n};\n\n/**\n * Validates the answer array. Checks for each question if the data from the form is\n * incomplete or has other errors. These are flagged accordingly in the array element.\n * The retruned object contains the properties:\n * - hasCorrectAnswer {boolean} is true if there is at least one correct answer.\n * - errors {Array} list of strings that contain an error code that is globaly used for error messages.\n *\n * @return {Array}\n * @private\n */\nconst _validateAnswers = function() {\n let errors = [];\n let hasCorrect = false;\n for (let i = 0; i < _answerdata.length; i++) {\n _answerdata[i].hasErrors = [];\n // Check if we have an empty answer string.\n if (_answerdata[i].raw === '') {\n _answerdata[i].hasErrors.push('empty_answer');\n }\n // When there are numeric questions, check that the answer and tolerance is a valid number.\n if (_qtype === 'NM' || _qtype === 'NUMERICAL') {\n if (isNaN(_answerdata[i].answer) && _answerdata[i].raw !== '') {\n _answerdata[i].hasErrors.push('answer_not_numeric');\n }\n if (isNaN(_answerdata[i].tolerance)) {\n _answerdata[i].hasErrors.push('tolerance_not_numeric');\n }\n }\n // Check the custom grade, that must be a percentage number between -100 and 100.\n if (_answerdata[i].isCustomGrade &&\n (isNaN(_answerdata[i].fraction) || _answerdata[i].fraction < -100 || _answerdata[i].fraction > 100\n || _answerdata[i].fraction.trim() === '')\n ) {\n _answerdata[i].hasErrors.push('error_custom_rate');\n }\n // We found a correct answer, when grade is marked as 100 or \"=\" and the answer is not empty.\n if (_answerdata[i].fraction === '100' || _answerdata[i].fraction === '=') {\n if (_answerdata[i].raw !== '') {\n _answerdata[i].isCorrect = true;\n hasCorrect = true;\n } else {\n _answerdata[i].hasErrors.push('correct_but_empty');\n }\n }\n errors = errors.concat(_answerdata[i].hasErrors);\n }\n\n return {\n hasCorrectAnswer: hasCorrect,\n errors: _combineGlobalErrors(hasCorrect, errors),\n };\n};\n\n/**\n * Translate the errors into a readable string for a list that is used on top of the\n * input fields, to indicate what part of the data is incorrect.\n *\n * @param {Boolean} hasCorrectAnswer\n * @param {Array} errors\n * @return {Array}\n * @private\n */\nconst _translateGlobalErrors = function(hasCorrectAnswer, errors) {\n const errTranslated = [];\n // Translate the error strings into a string that can be displayed in the form.\n const trMsg = {\n emptyanswer: STR.err_empty_answer,\n answernotnumeric: STR.err_not_numeric,\n tolerancenotnumeric: STR.err_not_numeric,\n errorcustomrate: STR.err_custom_rate,\n nonecorrect: STR.err_none_correct,\n };\n for (const err of errors) {\n // If there's at least one correct answer, we filter out all empty answers and therefore do not\n // show the error message.\n if (hasCorrectAnswer && err === 'empty_answer' || err === 'correct_but_empty') {\n continue;\n }\n // Remove underscore (we do this only because of the js linter).\n const key = err.replace(/_/g, '');\n errTranslated.push(trMsg[key]);\n }\n return errTranslated;\n};\n\n/**\n * Combine the error list from the answers to a global list.\n *\n * @param {Boolean} hasCorrectAnswer\n * @param {Array} errors\n * @return {Array}\n * @private\n */\nconst _combineGlobalErrors = function(hasCorrectAnswer, errors) {\n // Unique errors for the global error list.\n const errUnique = errors.filter((value, index, array) => array.indexOf(value) === index);\n // If we have a correct answer, do not show the empty answer error, because empty responses are filtered.\n if (hasCorrectAnswer) {\n const i = errUnique.indexOf('empty_answer');\n if (i > -1) {\n errUnique.splice(i, 1);\n }\n } else if (!errUnique.includes('correct_but_empty')) {\n errUnique.push('none_correct');\n }\n return errUnique;\n};\n\n/**\n * Check whether cursor is in a subquestion and return subquestion text if\n * true.\n *\n * @method resolveSubquestion\n * @param {Node|null} element The element to check if it is a subquestion.\n * @return {Mixed} The selected node of with the subquestion if found, false otherwise.\n */\nconst resolveSubquestion = function(element) {\n let span = element || _editor.selection.getStart();\n if (!isNull(span.classList) && span.classList.contains(markerClass)) {\n return span;\n }\n _editor.dom.getParents(span, elm => {\n // Are we in a span that encapsulates the cloze question?\n if (!isNull(elm.classList) && elm.classList.contains(markerClass)) {\n return elm;\n }\n return false;\n });\n return false;\n};\n\nexport {\n displayDialogue,\n displayDialogueForEdit,\n resolveSubquestion,\n onInit,\n onBeforeGetContent,\n onSubmit,\n};\n"],"names":["isNull","a","strdecode","t","String","replace","strencode","indexOfNode","list","node","i","length","getUuid","crypto","randomUUID","Math","floor","random","toString","getFractionOptions","s","attrSel","isSel","html","STR","incorrect","correct","FRACTIONS","forEach","item","value","indexOf","custom_grade","isCustomGrade","found","markerClass","markerSpan","CSS","ANSWER","ANSWERS","ADD","CANCEL","DELETE","FEEDBACK","FRACTION","FRAC_CUSTOM","LEFT","LOWER","RIGHT","MARKS","DUPLICATE","RAISE","SUBMIT","SUMMARY","TOLERANCE","TYPE","TEMPLATE","FORM","M","util","image_url","FOOTER","_editor","_form","_answerdata","_qtype","_selectedOffset","_marks","_modal","_firstAnswer","ed","_addMarkers","_getStr","_getRegexQtype","editor","extQtypes","RegExp","async","strToFetch","key","component","langKeys","push","then","args","Array","from","arguments","map","l","catch","_getQuestionTypes","qtypes","multichoice","summary_multichoice","selectinline","singleyes","horizontal","vertical","shuffle","multiresponse","multi_vertical","singleno","multi_horizontal","numerical","summary_numerical","shortanswer","summary_shortanswer","caseno","caseyes","splice","regexp","summary_regexp","_createModal","cfg","title","templateContext","elementid","id","removeOnClose","large","Modal","create","ModalFactory","subquestion","resolveSubquestion","dom","select","_parseSubquestion","innerHTML","_setDialogueContent","selection","getContent","target","m","content","newContent","match","pos","substring","level","b","setContent","_removeMarkers","span","setOuterHTML","classList","contains","source_view","onClose","off","on","qtype","nomodalevents","footer","Mustache","render","cancel","btn_cancel","submit","btn_insert","btn_select","contentText","answerdata","name","filter","q","type","marks","types","setBody","setFooter","show","$root","getRoot","get","querySelector","_toggleDeleteIcon","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","ModalEvents","_cancel","save","_choiceHandler","_setSubquestion","getTarget","e","p","nodeType","tagName","parentNode","addEventListener","preventDefault","_deleteAnswer","_addAnswer","_lowerAnswer","_raiseAnswer","querySelectorAll","sel","getAttribute","document","getElementById","remove","add","deleteIcons","max","blankAnswer","answer","feedback","fraction","fractionOptions","tolerance","x","destroy","focus","question","regexQtype","parts","exec","lastIndex","abbr","answers","options","frac","index","closest","_processFormData","min","li","before","nextSibling","after","previousSibling","errMsg","formErrors","join","raw","isNaN","slice","insertContent","validate","globalErrors","feedbacks","fractions","customGrades","tolerances","currentAnswer","trim","Number","hasCorrectAnswer","errors","_validateAnswers","err","hasErrors","_translateGlobalErrors","hasCorrect","isCorrect","concat","_combineGlobalErrors","errTranslated","trMsg","emptyanswer","err_empty_answer","answernotnumeric","err_not_numeric","tolerancenotnumeric","errorcustomrate","err_custom_rate","nonecorrect","err_none_correct","errUnique","array","includes","element","getStart","getParents","elm"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/ui.js b/amd/src/ui.js index 86ac690..98f8796 100644 --- a/amd/src/ui.js +++ b/amd/src/ui.js @@ -268,7 +268,7 @@ const onInit = function(ed) { // Add the marker spans. _addMarkers(); // And get the language strings. - _getStr(ed); + _getStr(); }; /** @@ -286,10 +286,9 @@ const _getRegexQtype = (editor) => { /** * Load strings for the modal dialogue from the language packs. - * @param {tinymce.Editor} editor * @private */ -const _getStr = async(editor) => { +const _getStr = async() => { let strToFetch = [ {key: 'answer', component: 'question'}, {key: 'chooseqtypetoadd', component: 'question'}, @@ -370,7 +369,7 @@ const _getStr = async(editor) => { 'err_none_correct', 'err_not_numeric', ]; - if (hasQtypeMultianswerrgx(editor)) { + if (hasQtypeMultianswerrgx(_editor)) { strToFetch.push({key: 'regexp', component: 'qtype_regexp'}); strToFetch.push({key: 'pluginnamesummary', component: 'qtype_regexp'}); langKeys.push('regexp'); From 773adf6dee98203c57228cd82edc8d2925d241bd Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Mon, 14 Oct 2024 16:47:25 +0200 Subject: [PATCH 4/5] Add first Behat tests for the multianswerrgx type. --- amd/build/options.min.js | 2 +- amd/build/options.min.js.map | 2 +- amd/src/options.js | 24 +++++++++++- classes/plugininfo.php | 16 ++++++-- tests/behat/multianswerrgx.feature | 63 ++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 tests/behat/multianswerrgx.feature diff --git a/amd/build/options.min.js b/amd/build/options.min.js index dc96050..8e27728 100644 --- a/amd/build/options.min.js +++ b/amd/build/options.min.js @@ -6,6 +6,6 @@ define("tiny_cloze/options",["exports","editor_tiny/options","./common"],(functi * @copyright 2024 Stephan Robotta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -const multianswerrgx=(0,_options.getPluginOptionName)(_common.pluginName,"multianswerrgx");_exports.register=editor=>{editor.options.register(multianswerrgx,{processor:"boolean",default:!1})};_exports.hasQtypeMultianswerrgx=editor=>editor.options.get(multianswerrgx);_exports.disableQtypeMultianswerrgx=editor=>editor.options.set(multianswerrgx,!1)})); +const multianswerrgx=(0,_options.getPluginOptionName)(_common.pluginName,"multianswerrgx"),testsite=(0,_options.getPluginOptionName)(_common.pluginName,"testsite");_exports.register=editor=>{editor.options.register(multianswerrgx,{processor:"boolean",default:!1}),editor.options.register(testsite,{processor:"boolean",default:!1})};_exports.hasQtypeMultianswerrgx=editor=>editor.options.get(multianswerrgx);_exports.disableQtypeMultianswerrgx=editor=>{editor.options.get(testsite)||editor.options.set(multianswerrgx,!1)}})); //# 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 index 229dcb0..90cb52d 100644 --- a/amd/build/options.min.js.map +++ b/amd/build/options.min.js.map @@ -1 +1 @@ -{"version":3,"file":"options.min.js","sources":["../src/options.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\n/**\n * Options helper for tiny_cloze plugin.\n *\n * @module tiny_cloze\n * @copyright 2024 Stephan Robotta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst multianswerrgx = getPluginOptionName(pluginName, 'multianswerrgx');\n\n/**\n * Register the options for the Tiny Cloze question plugin.\n *\n * @param {tinymce.Editor} editor\n */\nexport const register = (editor) => {\n editor.options.register(multianswerrgx, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\nexport const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx);\nexport const disableQtypeMultianswerrgx = (editor) => editor.options.set(multianswerrgx, false);"],"names":["multianswerrgx","pluginName","editor","options","register","processor","get","set"],"mappings":";;;;;;;;MA0BMA,gBAAiB,gCAAoBC,mBAAY,oCAO9BC,SACrBA,OAAOC,QAAQC,SAASJ,eAAgB,CACpCK,UAAW,mBACA,qCAIoBH,QAAWA,OAAOC,QAAQG,IAAIN,oDAC1BE,QAAWA,OAAOC,QAAQI,IAAIP,gBAAgB"} \ No newline at end of file +{"version":3,"file":"options.min.js","sources":["../src/options.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\n/**\n * Options helper for tiny_cloze plugin.\n *\n * @module tiny_cloze\n * @copyright 2024 Stephan Robotta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst multianswerrgx = getPluginOptionName(pluginName, 'multianswerrgx');\nconst testsite = getPluginOptionName(pluginName, 'testsite');\n\n/**\n * Register the options for the Tiny Cloze question plugin.\n *\n * @param {tinymce.Editor} editor\n */\nexport const register = (editor) => {\n editor.options.register(multianswerrgx, {\n processor: 'boolean',\n \"default\": false,\n });\n editor.options.register(testsite, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\n/**\n * Is the Qtype Multianswerrgx plugin enabled?\n * @param {tinymce.Editor} editor\n * @return {boolean}\n */\nexport const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx);\n\n/**\n * Disable the Qtype Multianswerrgx plugin option. The specific question types\n * of this plugin must not appear in the normal cloze question. However,\n * if we are on a testsite, then do not change the behaviour.\n *\n * @param {tinymce.Editor} editor\n */\nexport const disableQtypeMultianswerrgx = (editor) => {\n if (!editor.options.get(testsite)) {\n editor.options.set(multianswerrgx, false);\n }\n};"],"names":["multianswerrgx","pluginName","testsite","editor","options","register","processor","get","set"],"mappings":";;;;;;;;MA0BMA,gBAAiB,gCAAoBC,mBAAY,kBACjDC,UAAW,gCAAoBD,mBAAY,8BAOxBE,SACrBA,OAAOC,QAAQC,SAASL,eAAgB,CACpCM,UAAW,mBACA,IAEfH,OAAOC,QAAQC,SAASH,SAAU,CAC9BI,UAAW,mBACA,qCASoBH,QAAWA,OAAOC,QAAQG,IAAIP,oDAS1BG,SAClCA,OAAOC,QAAQG,IAAIL,WACpBC,OAAOC,QAAQI,IAAIR,gBAAgB"} \ No newline at end of file diff --git a/amd/src/options.js b/amd/src/options.js index 7baa34e..d92f43d 100644 --- a/amd/src/options.js +++ b/amd/src/options.js @@ -25,6 +25,7 @@ import {getPluginOptionName} from 'editor_tiny/options'; import {pluginName} from './common'; const multianswerrgx = getPluginOptionName(pluginName, 'multianswerrgx'); +const testsite = getPluginOptionName(pluginName, 'testsite'); /** * Register the options for the Tiny Cloze question plugin. @@ -36,7 +37,28 @@ export const register = (editor) => { processor: 'boolean', "default": false, }); + editor.options.register(testsite, { + processor: 'boolean', + "default": false, + }); }; +/** + * Is the Qtype Multianswerrgx plugin enabled? + * @param {tinymce.Editor} editor + * @return {boolean} + */ export const hasQtypeMultianswerrgx = (editor) => editor.options.get(multianswerrgx); -export const disableQtypeMultianswerrgx = (editor) => editor.options.set(multianswerrgx, false); \ No newline at end of file + +/** + * Disable the Qtype Multianswerrgx plugin option. The specific question types + * of this plugin must not appear in the normal cloze question. However, + * if we are on a testsite, then do not change the behaviour. + * + * @param {tinymce.Editor} editor + */ +export const disableQtypeMultianswerrgx = (editor) => { + if (!editor.options.get(testsite)) { + editor.options.set(multianswerrgx, false); + } +}; \ No newline at end of file diff --git a/classes/plugininfo.php b/classes/plugininfo.php index b392400..92a2c78 100644 --- a/classes/plugininfo.php +++ b/classes/plugininfo.php @@ -79,18 +79,26 @@ public static function get_plugin_configuration_for_context( // When on the test site, check that the simulation config for an existing regex question type is set. if (\behat_util::is_test_site()) { - return ['multianswerrgx' => (bool)get_config('tiny_cloze', 'simulate_multianswerrgx')]; + return [ + 'testsite' => true, + 'multianswerrgx' => (bool)get_config('tiny_cloze', 'simulate_multianswerrgx'), + ]; } + $config = [ + 'testsite' => false, + 'multianswerrgx' => false, + ]; try { // Check if the multianswerrgx question type is available. $instance = question_bank::get_qtype('multianswerrgx'); - return ['multianswerrgx' => is_object($instance)]; + $config['multianswerrgx'] = is_object($instance); + return $config; } catch (\exception $e) { // The multianswerrgx question type is not available. - return ['multianswerrgx' => false]; + return $config; } - return ['multianswerrgx' => false]; + return $config; } } diff --git a/tests/behat/multianswerrgx.feature b/tests/behat/multianswerrgx.feature new file mode 100644 index 0000000..c212a51 --- /dev/null +++ b/tests/behat/multianswerrgx.feature @@ -0,0 +1,63 @@ +@editor @tiny @editor_tiny @tiny_cloze @javascript +Feature: Test the multianswerrgx question type (simulate that the plugin is enabled) + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher | Mark | Allright | 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 "user preferences" exist: + | user | preference | value | + | teacher | htmleditor | tiny | + And the following config values are set as admin: + | simulate_multianswerrgx | 1 | tiny_cloze | + + Scenario: Create a new question and the regex types must not appear in the list + When the following config values are set as admin: + | simulate_multianswerrgx | 0 | tiny_cloze | + And I am on the "Course 1" "core_question > course question bank" page logged in as teacher + And I press "Create a new question ..." + And I set the field "Embedded answers (Cloze)" to "1" + And I click on "Add" "button" in the "Choose a question type to add" "dialogue" + And I set the field "Question name" to "multianswer-001" + And I click on "Cloze question editor" "button" + Then I should see "MULTICHOICE_S" + And I should see "NUMERICAL" + And I should see "SHORTANSWER" + And I should not see "REGEXP" + And I should not see "REGEXP_C" + + Scenario: Create a new question and the regex types must appear in the list + When I am on the "Course 1" "core_question > course question bank" page logged in as teacher + And I press "Create a new question ..." + And I set the field "Embedded answers (Cloze)" to "1" + And I click on "Add" "button" in the "Choose a question type to add" "dialogue" + And I set the field "Question name" to "multianswer-001" + And I click on "Cloze question editor" "button" + Then I should see "MULTICHOICE_S" + And I should see "NUMERICAL" + And I should see "SHORTANSWER" + And I should see "REGEXP" + And I should see "REGEXP_C" + + Scenario: Load REGEXP question string with feedback + When I am on the "Course 1" "core_question > course question bank" page logged in as teacher + And I press "Create a new question ..." + And I set the field "Embedded answers (Cloze)" to "1" + And I click on "Add" "button" in the "Choose a question type to add" "dialogue" + And I set the field "Question name" to "multianswer-001" + And I set the field "Question text" to multiline: + """ +

{1:REGEXP:%100%blue, white and red#Congratulations!~--.*(blue|red|white).*#You have not even found one of the colours of the French flag!~--.*(&&blue&&red&&white).*#You have not found all the colours of the French flag~--.*\bblue\b.*#The colour of the sky is missing!}

+ """ + And I select the "span" element in position "0" of the "Question text" TinyMCE editor + And I click on "Cloze question editor" "button" + Then I should see "Regular expression short answer (REGEXP)" + And the field with xpath "//form[@name='tiny_cloze_form']//li[1]//input[contains(@class, 'tiny_cloze_answer')]" matches value "blue, white and red" + And the field with xpath "//form[@name='tiny_cloze_form']//li[1]//select[contains(@class, 'tiny_cloze_frac')]" matches value "100%" + And the field with xpath "//form[@name='tiny_cloze_form']//li[1]//input[contains(@class, 'tiny_cloze_feedback')]" matches value "Congratulations!" From 0630af84434d57218dc60acd4dd45e2a25d5c76a Mon Sep 17 00:00:00 2001 From: Stephan Robotta Date: Tue, 15 Oct 2024 16:24:04 +0200 Subject: [PATCH 5/5] Try to fix behat test for ci which runs locally. --- tests/behat/multianswerrgx.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/behat/multianswerrgx.feature b/tests/behat/multianswerrgx.feature index c212a51..f51b785 100644 --- a/tests/behat/multianswerrgx.feature +++ b/tests/behat/multianswerrgx.feature @@ -57,7 +57,8 @@ Feature: Test the multianswerrgx question type (simulate that the plugin is enab """ And I select the "span" element in position "0" of the "Question text" TinyMCE editor And I click on "Cloze question editor" "button" - Then I should see "Regular expression short answer (REGEXP)" + # The following step works locally but fails on Github for some reason. + #Then I should see "Regular expression short answer (REGEXP)" And the field with xpath "//form[@name='tiny_cloze_form']//li[1]//input[contains(@class, 'tiny_cloze_answer')]" matches value "blue, white and red" And the field with xpath "//form[@name='tiny_cloze_form']//li[1]//select[contains(@class, 'tiny_cloze_frac')]" matches value "100%" And the field with xpath "//form[@name='tiny_cloze_form']//li[1]//input[contains(@class, 'tiny_cloze_feedback')]" matches value "Congratulations!"