From ebdd862f3c4245b2bcc7820165328945ff0b0b6f Mon Sep 17 00:00:00 2001 From: Mark Sharp Date: Fri, 19 Sep 2025 14:46:51 +0100 Subject: [PATCH] MDL-86586 core_form: Fixed dropdown JS error if no element found --- lib/form/amd/build/choicedropdown.min.js | 2 +- lib/form/amd/build/choicedropdown.min.js.map | 2 +- lib/form/amd/src/choicedropdown.js | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/form/amd/build/choicedropdown.min.js b/lib/form/amd/build/choicedropdown.min.js index 9ac51fd7445..4d5b2c5ac18 100644 --- a/lib/form/amd/build/choicedropdown.min.js +++ b/lib/form/amd/build/choicedropdown.min.js @@ -14,6 +14,6 @@ const Classes_hidden="d-none"; * @class FieldController * @copyright 2023 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */class FieldController{constructor(elementId){this.elementId=elementId,this.mainSelect=document.getElementById(this.elementId),this.dropdown=(0,_status.getDropdownStatus)('[data-form-controls="'.concat(this.elementId,'"]')),this.dropdown.getElement().classList.remove(Classes_hidden)}addEventListeners(){this.dropdown.getElement().addEventListener("change",this.updateSelect.bind(this)),this.dropdown.getElement().addEventListener("click",(event=>event.preventDefault())),this.mainSelect.addEventListener("change",this.updateDropdown.bind(this));new MutationObserver((mutations=>{mutations.forEach((mutation=>{"attributes"===mutation.type&&"disabled"===mutation.attributeName&&this.updateDropdown()}))})).observe(this.mainSelect,{attributeFilter:["disabled"]})}isDisabled(){var _this$mainSelect;return null===(_this$mainSelect=this.mainSelect)||void 0===_this$mainSelect?void 0:_this$mainSelect.hasAttribute("disabled")}async updateDropdown(){this.dropdown.setButtonDisabled(this.isDisabled()),this.dropdown.getSelectedValue()!=this.mainSelect.value&&this.dropdown.setSelectedValue(this.mainSelect.value)}async updateSelect(){this.dropdown.getSelectedValue()!=this.mainSelect.value&&(this.mainSelect.value=this.dropdown.getSelectedValue(),(0,_changechecker.markFormAsDirty)(this.mainSelect.closest("form")),this.mainSelect.dispatchEvent(new Event("change")))}disableInteractiveDialog(){var _this$mainSelect2;null===(_this$mainSelect2=this.mainSelect)||void 0===_this$mainSelect2||_this$mainSelect2.classList.remove(Classes_hidden);this.dropdown.getElement().classList.add(Classes_hidden)}hasForceDialog(){var _this$mainSelect3;return!(null===(_this$mainSelect3=this.mainSelect)||void 0===_this$mainSelect3||!_this$mainSelect3.dataset.forceDialog)}}_exports.init=elementId=>{const field=new FieldController(elementId);!document.body.classList.contains("behat-site")||field.hasForceDialog()?field.addEventListeners():field.disableInteractiveDialog()}})); + */class FieldController{constructor(elementId){var _this$dropdown;this.elementId=elementId,this.mainSelect=document.getElementById(this.elementId),this.dropdown=(0,_status.getDropdownStatus)('[data-form-controls="'.concat(this.elementId,'"]')),null===(_this$dropdown=this.dropdown)||void 0===_this$dropdown||_this$dropdown.getElement().classList.remove(Classes_hidden)}addEventListeners(){this.dropdown.getElement().addEventListener("change",this.updateSelect.bind(this)),this.dropdown.getElement().addEventListener("click",(event=>event.preventDefault())),this.mainSelect.addEventListener("change",this.updateDropdown.bind(this));new MutationObserver((mutations=>{mutations.forEach((mutation=>{"attributes"===mutation.type&&"disabled"===mutation.attributeName&&this.updateDropdown()}))})).observe(this.mainSelect,{attributeFilter:["disabled"]})}isDisabled(){var _this$mainSelect;return null===(_this$mainSelect=this.mainSelect)||void 0===_this$mainSelect?void 0:_this$mainSelect.hasAttribute("disabled")}async updateDropdown(){this.dropdown.setButtonDisabled(this.isDisabled()),this.dropdown.getSelectedValue()!=this.mainSelect.value&&this.dropdown.setSelectedValue(this.mainSelect.value)}async updateSelect(){this.dropdown.getSelectedValue()!=this.mainSelect.value&&(this.mainSelect.value=this.dropdown.getSelectedValue(),(0,_changechecker.markFormAsDirty)(this.mainSelect.closest("form")),this.mainSelect.dispatchEvent(new Event("change")))}disableInteractiveDialog(){var _this$mainSelect2;null===(_this$mainSelect2=this.mainSelect)||void 0===_this$mainSelect2||_this$mainSelect2.classList.remove(Classes_hidden);this.dropdown.getElement().classList.add(Classes_hidden)}hasForceDialog(){var _this$mainSelect3;return!(null===(_this$mainSelect3=this.mainSelect)||void 0===_this$mainSelect3||!_this$mainSelect3.dataset.forceDialog)}}_exports.init=elementId=>{const field=new FieldController(elementId);!document.body.classList.contains("behat-site")||field.hasForceDialog()?null!==field.dropdown&&field.addEventListeners():field.disableInteractiveDialog()}})); //# sourceMappingURL=choicedropdown.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/choicedropdown.min.js.map b/lib/form/amd/build/choicedropdown.min.js.map index 8e2d0437a09..84854369b41 100644 --- a/lib/form/amd/build/choicedropdown.min.js.map +++ b/lib/form/amd/build/choicedropdown.min.js.map @@ -1 +1 @@ -{"version":3,"file":"choicedropdown.min.js","sources":["../src/choicedropdown.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 * Field controller for choicedropdown field.\n *\n * @module core_form/choicedropdown\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getDropdownStatus} from 'core/local/dropdown/status';\nimport {markFormAsDirty} from 'core_form/changechecker';\n\nconst Classes = {\n notClickable: 'not-clickable',\n hidden: 'd-none',\n};\n\n/**\n * Internal form element class.\n *\n * @private\n * @class FieldController\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass FieldController {\n /**\n * Class constructor.\n *\n * @param {String} elementId Form element id\n */\n constructor(elementId) {\n this.elementId = elementId;\n this.mainSelect = document.getElementById(this.elementId);\n this.dropdown = getDropdownStatus(`[data-form-controls=\"${this.elementId}\"]`);\n this.dropdown.getElement().classList.remove(Classes.hidden);\n }\n\n /**\n * Add form element event listener.\n */\n addEventListeners() {\n this.dropdown.getElement().addEventListener(\n 'change',\n this.updateSelect.bind(this)\n );\n // Click on a dropdown link can trigger a wrong dirty form reload warning.\n this.dropdown.getElement().addEventListener(\n 'click',\n (event) => event.preventDefault()\n );\n this.mainSelect.addEventListener(\n 'change',\n this.updateDropdown.bind(this)\n );\n // Enabling or disabling the select does not trigger any JS event.\n const observerCallback = (mutations) => {\n mutations.forEach((mutation) => {\n if (mutation.type !== 'attributes' || mutation.attributeName !== 'disabled') {\n return;\n }\n this.updateDropdown();\n });\n };\n new MutationObserver(observerCallback).observe(\n this.mainSelect,\n {attributeFilter: ['disabled']}\n );\n }\n\n /**\n * Check if the field is disabled.\n * @returns {Boolean}\n */\n isDisabled() {\n return this.mainSelect?.hasAttribute('disabled');\n }\n\n /**\n * Update selected option preview in form.\n */\n async updateDropdown() {\n this.dropdown.setButtonDisabled(this.isDisabled());\n if (this.dropdown.getSelectedValue() == this.mainSelect.value) {\n return;\n }\n this.dropdown.setSelectedValue(this.mainSelect.value);\n }\n\n /**\n * Update selected option preview in form.\n */\n async updateSelect() {\n if (this.dropdown.getSelectedValue() == this.mainSelect.value) {\n return;\n }\n this.mainSelect.value = this.dropdown.getSelectedValue();\n markFormAsDirty(this.mainSelect.closest('form'));\n // Change the select element via JS does not trigger the standard change event.\n this.mainSelect.dispatchEvent(new Event('change'));\n }\n\n /**\n * Disable the choice dialog and convert it into a regular select field.\n */\n disableInteractiveDialog() {\n this.mainSelect?.classList.remove(Classes.hidden);\n const dropdownElement = this.dropdown.getElement();\n dropdownElement.classList.add(Classes.hidden);\n }\n\n /**\n * Check if the field has a force dialog attribute.\n // *\n * The force dialog is a setting to force the javascript control even in\n * behat test.\n *\n * @returns {Boolean} if the dialog modal should be forced or not\n */\n hasForceDialog() {\n return !!this.mainSelect?.dataset.forceDialog;\n }\n}\n\n/**\n * Initialises a choice dialog field.\n *\n * @method init\n * @param {String} elementId Form element id\n * @listens event:uploadStarted\n * @listens event:uploadCompleted\n */\nexport const init = (elementId) => {\n const field = new FieldController(elementId);\n // This field is just a select wrapper. To optimize tests, we don't want to keep behat\n // waiting for extra loadings in this case. The set field steps are about testing other\n // stuff, not to test fancy javascript form fields. However, we keep the possibility of\n // testing the javascript part using behat when necessary.\n if (document.body.classList.contains('behat-site') && !field.hasForceDialog()) {\n field.disableInteractiveDialog();\n return;\n }\n field.addEventListeners();\n};\n"],"names":["Classes","FieldController","constructor","elementId","mainSelect","document","getElementById","this","dropdown","getElement","classList","remove","addEventListeners","addEventListener","updateSelect","bind","event","preventDefault","updateDropdown","MutationObserver","mutations","forEach","mutation","type","attributeName","observe","attributeFilter","isDisabled","_this$mainSelect","hasAttribute","setButtonDisabled","getSelectedValue","value","setSelectedValue","closest","dispatchEvent","Event","disableInteractiveDialog","add","hasForceDialog","_this$mainSelect3","dataset","forceDialog","field","body","contains"],"mappings":";;;;;;;;MA0BMA,eAEM;;;;;;;;WAWNC,gBAMFC,YAAYC,gBACHA,UAAYA,eACZC,WAAaC,SAASC,eAAeC,KAAKJ,gBAC1CK,UAAW,4DAA0CD,KAAKJ,sBAC1DK,SAASC,aAAaC,UAAUC,OAAOX,gBAMhDY,yBACSJ,SAASC,aAAaI,iBACvB,SACAN,KAAKO,aAAaC,KAAKR,YAGtBC,SAASC,aAAaI,iBACvB,SACCG,OAAUA,MAAMC,wBAEhBb,WAAWS,iBACZ,SACAN,KAAKW,eAAeH,KAAKR,WAWzBY,kBARsBC,YACtBA,UAAUC,SAASC,WACO,eAAlBA,SAASC,MAAoD,aAA3BD,SAASE,oBAG1CN,uBAG0BO,QACnClB,KAAKH,WACL,CAACsB,gBAAiB,CAAC,cAQ3BC,kEACWpB,KAAKH,8CAALwB,iBAAiBC,aAAa,wCAOhCrB,SAASsB,kBAAkBvB,KAAKoB,cACjCpB,KAAKC,SAASuB,oBAAsBxB,KAAKH,WAAW4B,YAGnDxB,SAASyB,iBAAiB1B,KAAKH,WAAW4B,4BAO3CzB,KAAKC,SAASuB,oBAAsBxB,KAAKH,WAAW4B,aAGnD5B,WAAW4B,MAAQzB,KAAKC,SAASuB,sDACtBxB,KAAKH,WAAW8B,QAAQ,cAEnC9B,WAAW+B,cAAc,IAAIC,MAAM,YAM5CC,gFACSjC,2DAAYM,UAAUC,OAAOX,gBACVO,KAAKC,SAASC,aACtBC,UAAU4B,IAAItC,gBAWlCuC,yEACahC,KAAKH,0CAALoC,kBAAiBC,QAAQC,4BAYrBvC,kBACXwC,MAAQ,IAAI1C,gBAAgBE,YAK9BE,SAASuC,KAAKlC,UAAUmC,SAAS,eAAkBF,MAAMJ,iBAI7DI,MAAM/B,oBAHF+B,MAAMN"} \ No newline at end of file +{"version":3,"file":"choicedropdown.min.js","sources":["../src/choicedropdown.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 * Field controller for choicedropdown field.\n *\n * @module core_form/choicedropdown\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getDropdownStatus} from 'core/local/dropdown/status';\nimport {markFormAsDirty} from 'core_form/changechecker';\n\nconst Classes = {\n notClickable: 'not-clickable',\n hidden: 'd-none',\n};\n\n/**\n * Internal form element class.\n *\n * @private\n * @class FieldController\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass FieldController {\n /**\n * Class constructor.\n *\n * @param {String} elementId Form element id\n */\n constructor(elementId) {\n this.elementId = elementId;\n this.mainSelect = document.getElementById(this.elementId);\n this.dropdown = getDropdownStatus(`[data-form-controls=\"${this.elementId}\"]`);\n this.dropdown?.getElement().classList.remove(Classes.hidden);\n }\n\n /**\n * Add form element event listener.\n */\n addEventListeners() {\n this.dropdown.getElement().addEventListener(\n 'change',\n this.updateSelect.bind(this)\n );\n // Click on a dropdown link can trigger a wrong dirty form reload warning.\n this.dropdown.getElement().addEventListener(\n 'click',\n (event) => event.preventDefault()\n );\n this.mainSelect.addEventListener(\n 'change',\n this.updateDropdown.bind(this)\n );\n // Enabling or disabling the select does not trigger any JS event.\n const observerCallback = (mutations) => {\n mutations.forEach((mutation) => {\n if (mutation.type !== 'attributes' || mutation.attributeName !== 'disabled') {\n return;\n }\n this.updateDropdown();\n });\n };\n new MutationObserver(observerCallback).observe(\n this.mainSelect,\n {attributeFilter: ['disabled']}\n );\n }\n\n /**\n * Check if the field is disabled.\n * @returns {Boolean}\n */\n isDisabled() {\n return this.mainSelect?.hasAttribute('disabled');\n }\n\n /**\n * Update selected option preview in form.\n */\n async updateDropdown() {\n this.dropdown.setButtonDisabled(this.isDisabled());\n if (this.dropdown.getSelectedValue() == this.mainSelect.value) {\n return;\n }\n this.dropdown.setSelectedValue(this.mainSelect.value);\n }\n\n /**\n * Update selected option preview in form.\n */\n async updateSelect() {\n if (this.dropdown.getSelectedValue() == this.mainSelect.value) {\n return;\n }\n this.mainSelect.value = this.dropdown.getSelectedValue();\n markFormAsDirty(this.mainSelect.closest('form'));\n // Change the select element via JS does not trigger the standard change event.\n this.mainSelect.dispatchEvent(new Event('change'));\n }\n\n /**\n * Disable the choice dialog and convert it into a regular select field.\n */\n disableInteractiveDialog() {\n this.mainSelect?.classList.remove(Classes.hidden);\n const dropdownElement = this.dropdown.getElement();\n dropdownElement.classList.add(Classes.hidden);\n }\n\n /**\n * Check if the field has a force dialog attribute.\n // *\n * The force dialog is a setting to force the javascript control even in\n * behat test.\n *\n * @returns {Boolean} if the dialog modal should be forced or not\n */\n hasForceDialog() {\n return !!this.mainSelect?.dataset.forceDialog;\n }\n}\n\n/**\n * Initialises a choice dialog field.\n *\n * @method init\n * @param {String} elementId Form element id\n * @listens event:uploadStarted\n * @listens event:uploadCompleted\n */\nexport const init = (elementId) => {\n const field = new FieldController(elementId);\n // This field is just a select wrapper. To optimize tests, we don't want to keep behat\n // waiting for extra loadings in this case. The set field steps are about testing other\n // stuff, not to test fancy javascript form fields. However, we keep the possibility of\n // testing the javascript part using behat when necessary.\n if (document.body.classList.contains('behat-site') && !field.hasForceDialog()) {\n field.disableInteractiveDialog();\n return;\n }\n if (field.dropdown !== null) {\n field.addEventListeners();\n }\n};\n"],"names":["Classes","FieldController","constructor","elementId","mainSelect","document","getElementById","this","dropdown","getElement","classList","remove","addEventListeners","addEventListener","updateSelect","bind","event","preventDefault","updateDropdown","MutationObserver","mutations","forEach","mutation","type","attributeName","observe","attributeFilter","isDisabled","_this$mainSelect","hasAttribute","setButtonDisabled","getSelectedValue","value","setSelectedValue","closest","dispatchEvent","Event","disableInteractiveDialog","add","hasForceDialog","_this$mainSelect3","dataset","forceDialog","field","body","contains"],"mappings":";;;;;;;;MA0BMA,eAEM;;;;;;;;WAWNC,gBAMFC,YAAYC,mCACHA,UAAYA,eACZC,WAAaC,SAASC,eAAeC,KAAKJ,gBAC1CK,UAAW,4DAA0CD,KAAKJ,6CAC1DK,mDAAUC,aAAaC,UAAUC,OAAOX,gBAMjDY,yBACSJ,SAASC,aAAaI,iBACvB,SACAN,KAAKO,aAAaC,KAAKR,YAGtBC,SAASC,aAAaI,iBACvB,SACCG,OAAUA,MAAMC,wBAEhBb,WAAWS,iBACZ,SACAN,KAAKW,eAAeH,KAAKR,WAWzBY,kBARsBC,YACtBA,UAAUC,SAASC,WACO,eAAlBA,SAASC,MAAoD,aAA3BD,SAASE,oBAG1CN,uBAG0BO,QACnClB,KAAKH,WACL,CAACsB,gBAAiB,CAAC,cAQ3BC,kEACWpB,KAAKH,8CAALwB,iBAAiBC,aAAa,wCAOhCrB,SAASsB,kBAAkBvB,KAAKoB,cACjCpB,KAAKC,SAASuB,oBAAsBxB,KAAKH,WAAW4B,YAGnDxB,SAASyB,iBAAiB1B,KAAKH,WAAW4B,4BAO3CzB,KAAKC,SAASuB,oBAAsBxB,KAAKH,WAAW4B,aAGnD5B,WAAW4B,MAAQzB,KAAKC,SAASuB,sDACtBxB,KAAKH,WAAW8B,QAAQ,cAEnC9B,WAAW+B,cAAc,IAAIC,MAAM,YAM5CC,gFACSjC,2DAAYM,UAAUC,OAAOX,gBACVO,KAAKC,SAASC,aACtBC,UAAU4B,IAAItC,gBAWlCuC,yEACahC,KAAKH,0CAALoC,kBAAiBC,QAAQC,4BAYrBvC,kBACXwC,MAAQ,IAAI1C,gBAAgBE,YAK9BE,SAASuC,KAAKlC,UAAUmC,SAAS,eAAkBF,MAAMJ,iBAItC,OAAnBI,MAAMnC,UACNmC,MAAM/B,oBAJN+B,MAAMN"} \ No newline at end of file diff --git a/lib/form/amd/src/choicedropdown.js b/lib/form/amd/src/choicedropdown.js index 71da5974fc1..0a30f8fe759 100644 --- a/lib/form/amd/src/choicedropdown.js +++ b/lib/form/amd/src/choicedropdown.js @@ -47,7 +47,7 @@ class FieldController { this.elementId = elementId; this.mainSelect = document.getElementById(this.elementId); this.dropdown = getDropdownStatus(`[data-form-controls="${this.elementId}"]`); - this.dropdown.getElement().classList.remove(Classes.hidden); + this.dropdown?.getElement().classList.remove(Classes.hidden); } /** @@ -154,5 +154,7 @@ export const init = (elementId) => { field.disableInteractiveDialog(); return; } - field.addEventListeners(); + if (field.dropdown !== null) { + field.addEventListeners(); + } };