diff --git a/backup/util/ui/amd/build/schema_backup_form.min.js b/backup/util/ui/amd/build/schema_backup_form.min.js index db8aee7a61dd1..5c3161d6b85f9 100644 --- a/backup/util/ui/amd/build/schema_backup_form.min.js +++ b/backup/util/ui/amd/build/schema_backup_form.min.js @@ -9,6 +9,6 @@ define("core_backup/schema_backup_form",["exports","core/notification","core/tem * @module core_backup/schema_backup_form * @copyright 2024 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */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,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj},Templates=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}(Templates);const Selectors_action="[data-mdl-action]",Selectors_checkboxes='#id_coursesettings input[type="checkbox"]',Selectors_firstSection="fieldset#id_coursesettings .fcontainer .grouped_settings.section_level",Selectors_modCheckboxes=modName=>"setting_activity_".concat(modName,"_"),Suffixes_userData="_userdata",Suffixes_userInfo="_userinfo",Suffixes_included="_included";class BackupFormController{static init(modNames){return new BackupFormController(modNames)}constructor(modNames){this.modNames=modNames,this.scanFormUserData(),this.addSelectorsToPage()}scanFormUserData(){this.withuserdata=!1,this.userDataSuffix=Suffixes_userData;const checkboxes=document.querySelectorAll(Selectors_checkboxes);if(checkboxes)for(const checkbox of checkboxes){const name=checkbox.name;if(name.endsWith(Suffixes_userData)){this.withuserdata=!0;break}if(name.endsWith(Suffixes_userInfo)){this.withuserdata=!0,this.userDataSuffix=Suffixes_userInfo;break}}}initEvents(element){element.addEventListener("click",(event=>{var _action$dataset,_action$dataset$mdlMo,_action$dataset2;const action=event.target.closest(Selectors_action);if(!action)return;event.preventDefault();const suffix="userdata"==(null===(_action$dataset=action.dataset)||void 0===_action$dataset?void 0:_action$dataset.mdlType)?this.userDataSuffix:Suffixes_included;this.changeSelection("selectall"==action.dataset.mdlAction,suffix,null!==(_action$dataset$mdlMo=null===(_action$dataset2=action.dataset)||void 0===_action$dataset2?void 0:_action$dataset2.mdlMod)&&void 0!==_action$dataset$mdlMo?_action$dataset$mdlMo:null)}))}changeSelection(checked,suffix,modName){const prefix=modName?Selectors_modCheckboxes(modName):null;let formId;const checkboxes=document.querySelectorAll(Selectors_checkboxes);for(const checkbox of checkboxes){var _formId;formId=null!==(_formId=formId)&&void 0!==_formId?_formId:checkbox.closest("form").getAttribute("id"),prefix&&!checkbox.name.startsWith(prefix)||checkbox.name.endsWith(suffix)&&(checkbox.checked=checked)}formId&&M.form&&M.form.updateFormState(formId)}generateSelectorsElement(){const links=document.createElement("div");return links.id="backup_selectors",this.initEvents(links),this.renderSelectorsTemplate(links),links}renderSelectorsTemplate(element){const data={modules:this.getModulesTemplateData(),withuserdata:!!this.withuserdata||void 0};Templates.renderForPromise("core_backup/formselectall",data).then((_ref=>{let{html:html,js:js}=_ref;return Templates.replaceNodeContents(element,html,js)})).catch(_notification.default.exception)}getModulesTemplateData(){const modules=[];for(const modName in this.modNames)this.modNames.hasOwnProperty(modName)&&modules.push({modname:modName,heading:this.modNames[modName]});return modules}addSelectorsToPage(){const firstSection=document.querySelector(Selectors_firstSection);if(!firstSection)return;if(!firstSection.querySelector(Selectors_checkboxes))return;const selector=this.generateSelectorsElement();firstSection.parentNode.insertBefore(selector,firstSection)}}return _exports.default=BackupFormController,_exports.default})); + */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,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj},Templates=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}(Templates);const Selectors_action="[data-mdl-action]",Selectors_checkboxes='#id_coursesettings input[type="checkbox"]',Selectors_firstSection="fieldset#id_coursesettings .fcontainer .grouped_settings.section_level",Selectors_modCheckboxes=modName=>"setting_activity_".concat(modName,"_"),Suffixes_userData="_userdata",Suffixes_userInfo="_userinfo",Suffixes_included="_included";class BackupFormController{static init(modNames){return new BackupFormController(modNames)}constructor(modNames){this.modNames=modNames,this.scanFormUserData(),this.addSelectorsToPage()}scanFormUserData(){this.withuserdata=!1,this.userDataSuffix=Suffixes_userData;const checkboxes=document.querySelectorAll(Selectors_checkboxes);if(checkboxes)for(const checkbox of checkboxes){const name=checkbox.name;if(name.endsWith(Suffixes_userData)){this.withuserdata=!0;break}if(name.endsWith(Suffixes_userInfo)){this.withuserdata=!0,this.userDataSuffix=Suffixes_userInfo;break}}}initEvents(element){element.addEventListener("click",(event=>{var _action$dataset,_action$dataset$mdlMo,_action$dataset2;const action=event.target.closest(Selectors_action);if(!action)return;event.preventDefault();const suffix="userdata"==(null===(_action$dataset=action.dataset)||void 0===_action$dataset?void 0:_action$dataset.mdlType)?this.userDataSuffix:Suffixes_included;this.changeSelection("selectall"==action.dataset.mdlAction,suffix,null!==(_action$dataset$mdlMo=null===(_action$dataset2=action.dataset)||void 0===_action$dataset2?void 0:_action$dataset2.mdlMod)&&void 0!==_action$dataset$mdlMo?_action$dataset$mdlMo:null)}))}changeSelection(checked,suffix,modName){const prefix=modName?Selectors_modCheckboxes(modName):null;let formId;const checkboxes=document.querySelectorAll(Selectors_checkboxes);for(const checkbox of checkboxes){var _formId;formId=null!==(_formId=formId)&&void 0!==_formId?_formId:checkbox.closest("form").getAttribute("id"),prefix&&!checkbox.name.startsWith(prefix)||checkbox.name.endsWith(suffix)&&(checkbox.checked=checked)}formId&&M.form2&&M.form2.get(formId).formUpdatedExternally()}generateSelectorsElement(){const links=document.createElement("div");return links.id="backup_selectors",this.initEvents(links),this.renderSelectorsTemplate(links),links}renderSelectorsTemplate(element){const data={modules:this.getModulesTemplateData(),withuserdata:!!this.withuserdata||void 0};Templates.renderForPromise("core_backup/formselectall",data).then((_ref=>{let{html:html,js:js}=_ref;return Templates.replaceNodeContents(element,html,js)})).catch(_notification.default.exception)}getModulesTemplateData(){const modules=[];for(const modName in this.modNames)this.modNames.hasOwnProperty(modName)&&modules.push({modname:modName,heading:this.modNames[modName]});return modules}addSelectorsToPage(){const firstSection=document.querySelector(Selectors_firstSection);if(!firstSection)return;if(!firstSection.querySelector(Selectors_checkboxes))return;const selector=this.generateSelectorsElement();firstSection.parentNode.insertBefore(selector,firstSection)}}return _exports.default=BackupFormController,_exports.default})); //# sourceMappingURL=schema_backup_form.min.js.map \ No newline at end of file diff --git a/backup/util/ui/amd/build/schema_backup_form.min.js.map b/backup/util/ui/amd/build/schema_backup_form.min.js.map index a7dd9eb976012..e47f1e8b55385 100644 --- a/backup/util/ui/amd/build/schema_backup_form.min.js.map +++ b/backup/util/ui/amd/build/schema_backup_form.min.js.map @@ -1 +1 @@ -{"version":3,"file":"schema_backup_form.min.js","sources":["../src/schema_backup_form.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 * Schema selector javascript controls.\n *\n * This module controls:\n * - The select all feature.\n * - Disabling activities checkboxes when the section is not selected.\n *\n * @module core_backup/schema_backup_form\n * @copyright 2024 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport * as Templates from 'core/templates';\n\nconst Selectors = {\n action: '[data-mdl-action]',\n checkboxes: '#id_coursesettings input[type=\"checkbox\"]',\n firstSection: 'fieldset#id_coursesettings .fcontainer .grouped_settings.section_level',\n modCheckboxes: (modName) => `setting_activity_${modName}_`,\n};\n\nconst Suffixes = {\n userData: '_userdata',\n userInfo: '_userinfo',\n included: '_included',\n};\n\n/**\n * Adds select all/none links to the top of the backup/restore/import schema page.\n */\nexport default class BackupFormController {\n\n /**\n * Static module init method.\n * @param {Array} modNames - The names of the modules.\n * @returns {BackupFormController}\n */\n static init(modNames) {\n return new BackupFormController(modNames);\n }\n\n /**\n * Creates a new instance of the SchemaBackupForm class.\n * @param {Array} modNames - The names of the modules.\n */\n constructor(modNames) {\n this.modNames = modNames;\n this.scanFormUserData();\n this.addSelectorsToPage();\n }\n\n /**\n * Detect the user data attribute from the form.\n *\n * @private\n */\n scanFormUserData() {\n this.withuserdata = false;\n this.userDataSuffix = Suffixes.userData;\n\n const checkboxes = document.querySelectorAll(Selectors.checkboxes);\n if (!checkboxes) {\n return;\n }\n // Depending on the form, user data inclusion is called userinfo or userdata.\n for (const checkbox of checkboxes) {\n const name = checkbox.name;\n if (name.endsWith(Suffixes.userData)) {\n this.withuserdata = true;\n break;\n } else if (name.endsWith(Suffixes.userInfo)) {\n this.withuserdata = true;\n this.userDataSuffix = Suffixes.userInfo;\n break;\n }\n }\n }\n\n /**\n * Initializes all related events.\n *\n * @private\n * @param {HTMLElement} element - The element to attach the events to.\n */\n initEvents(element) {\n element.addEventListener('click', (event) => {\n const action = event.target.closest(Selectors.action);\n if (!action) {\n return;\n }\n event.preventDefault();\n\n const suffix = (action.dataset?.mdlType == 'userdata') ? this.userDataSuffix : Suffixes.included;\n\n this.changeSelection(\n action.dataset.mdlAction == 'selectall',\n suffix,\n action.dataset?.mdlMod ?? null\n );\n });\n }\n\n /**\n * Changes the selection according to the params.\n *\n * @private\n * @param {boolean} checked - The checked state for the checkboxes.\n * @param {string} suffix - The checkboxes suffix\n * @param {string} [modName] - The module name.\n */\n changeSelection(checked, suffix, modName) {\n const prefix = modName ? Selectors.modCheckboxes(modName) : null;\n\n let formId;\n\n const checkboxes = document.querySelectorAll(Selectors.checkboxes);\n for (const checkbox of checkboxes) {\n formId = formId ?? checkbox.closest('form').getAttribute('id');\n\n if (prefix && !checkbox.name.startsWith(prefix)) {\n continue;\n }\n if (checkbox.name.endsWith(suffix)) {\n checkbox.checked = checked;\n }\n }\n\n // At this point, we really need to persuade the form we are part of to\n // update all of its disabledIf rules. However, as far as I can see,\n // given the way that lib/form/form.js is written, that is impossible.\n if (formId && M.form) {\n M.form.updateFormState(formId);\n }\n }\n\n /**\n * Generates the full selectors element to add to the page.\n *\n * @private\n * @returns {HTMLElement} The selectors element.\n */\n generateSelectorsElement() {\n const links = document.createElement('div');\n links.id = 'backup_selectors';\n this.initEvents(links);\n this.renderSelectorsTemplate(links);\n return links;\n }\n\n /**\n * Load the select all template.\n *\n * @private\n * @param {HTMLElement} element the container\n */\n renderSelectorsTemplate(element) {\n const data = {\n modules: this.getModulesTemplateData(),\n withuserdata: (this.withuserdata) ? true : undefined,\n };\n Templates.renderForPromise(\n 'core_backup/formselectall',\n data\n ).then(({html, js}) => {\n return Templates.replaceNodeContents(element, html, js);\n }).catch(Notification.exception);\n }\n\n /**\n * Generate the modules template data.\n *\n * @private\n * @returns {Array} of modules data.\n */\n getModulesTemplateData() {\n const modules = [];\n for (const modName in this.modNames) {\n if (!this.modNames.hasOwnProperty(modName)) {\n continue;\n }\n modules.push({\n modname: modName,\n heading: this.modNames[modName],\n });\n }\n return modules;\n }\n\n /**\n * Adds select all/none functionality to the backup form.\n *\n * @private\n */\n addSelectorsToPage() {\n const firstSection = document.querySelector(Selectors.firstSection);\n if (!firstSection) {\n // This is not a relevant page.\n return;\n }\n if (!firstSection.querySelector(Selectors.checkboxes)) {\n // No checkboxes.\n return;\n }\n\n // Add global select all/none options.\n const selector = this.generateSelectorsElement();\n firstSection.parentNode.insertBefore(selector, firstSection);\n }\n}\n"],"names":["Selectors","modName","Suffixes","BackupFormController","modNames","constructor","scanFormUserData","addSelectorsToPage","withuserdata","userDataSuffix","checkboxes","document","querySelectorAll","checkbox","name","endsWith","initEvents","element","addEventListener","event","action","target","closest","preventDefault","suffix","dataset","mdlType","this","changeSelection","mdlAction","_action$dataset2","mdlMod","checked","prefix","formId","getAttribute","startsWith","M","form","updateFormState","generateSelectorsElement","links","createElement","id","renderSelectorsTemplate","data","modules","getModulesTemplateData","undefined","Templates","renderForPromise","then","_ref","html","js","replaceNodeContents","catch","Notification","exception","hasOwnProperty","push","modname","heading","firstSection","querySelector","selector","parentNode","insertBefore"],"mappings":";;;;;;;;;;;olCA8BMA,iBACM,oBADNA,qBAEU,4CAFVA,uBAGY,yEAHZA,wBAIcC,oCAAgCA,aAG9CC,kBACQ,YADRA,kBAEQ,YAFRA,kBAGQ,kBAMOC,iCAOLC,iBACD,IAAID,qBAAqBC,UAOpCC,YAAYD,eACHA,SAAWA,cACXE,wBACAC,qBAQTD,wBACSE,cAAe,OACfC,eAAiBP,wBAEhBQ,WAAaC,SAASC,iBAAiBZ,yBACxCU,eAIA,MAAMG,YAAYH,WAAY,OACzBI,KAAOD,SAASC,QAClBA,KAAKC,SAASb,mBAAoB,MAC7BM,cAAe,QAEjB,GAAIM,KAAKC,SAASb,mBAAoB,MACpCM,cAAe,OACfC,eAAiBP,0BAYlCc,WAAWC,SACPA,QAAQC,iBAAiB,SAAUC,yEACzBC,OAASD,MAAME,OAAOC,QAAQtB,sBAC/BoB,cAGLD,MAAMI,uBAEAC,OAAqC,qCAA3BJ,OAAOK,0DAASC,SAAyBC,KAAKlB,eAAiBP,uBAE1E0B,gBAC2B,aAA5BR,OAAOK,QAAQI,UACfL,8DACAJ,OAAOK,2CAAPK,iBAAgBC,8DAAU,SAatCH,gBAAgBI,QAASR,OAAQvB,eACvBgC,OAAShC,QAAUD,wBAAwBC,SAAW,SAExDiC,aAEExB,WAAaC,SAASC,iBAAiBZ,0BACxC,MAAMa,YAAYH,WAAY,aAC/BwB,uBAASA,kCAAUrB,SAASS,QAAQ,QAAQa,aAAa,MAErDF,SAAWpB,SAASC,KAAKsB,WAAWH,SAGpCpB,SAASC,KAAKC,SAASS,UACvBX,SAASmB,QAAUA,SAOvBE,QAAUG,EAAEC,MACZD,EAAEC,KAAKC,gBAAgBL,QAU/BM,iCACUC,MAAQ9B,SAAS+B,cAAc,cACrCD,MAAME,GAAK,wBACN3B,WAAWyB,YACXG,wBAAwBH,OACtBA,MASXG,wBAAwB3B,eACd4B,KAAO,CACTC,QAASnB,KAAKoB,yBACdvC,eAAemB,KAAKnB,mBAAuBwC,GAE/CC,UAAUC,iBACN,4BACAL,MACFM,MAAKC,WAACC,KAACA,KAADC,GAAOA,gBACJL,UAAUM,oBAAoBtC,QAASoC,KAAMC,OACrDE,MAAMC,sBAAaC,WAS1BX,+BACUD,QAAU,OACX,MAAM7C,WAAW0B,KAAKvB,SAClBuB,KAAKvB,SAASuD,eAAe1D,UAGlC6C,QAAQc,KAAK,CACTC,QAAS5D,QACT6D,QAASnC,KAAKvB,SAASH,kBAGxB6C,QAQXvC,2BACUwD,aAAepD,SAASqD,cAAchE,4BACvC+D,wBAIAA,aAAaC,cAAchE,mCAM1BiE,SAAWtC,KAAKa,2BACtBuB,aAAaG,WAAWC,aAAaF,SAAUF"} \ No newline at end of file +{"version":3,"file":"schema_backup_form.min.js","sources":["../src/schema_backup_form.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 * Schema selector javascript controls.\n *\n * This module controls:\n * - The select all feature.\n * - Disabling activities checkboxes when the section is not selected.\n *\n * @module core_backup/schema_backup_form\n * @copyright 2024 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport * as Templates from 'core/templates';\n\nconst Selectors = {\n action: '[data-mdl-action]',\n checkboxes: '#id_coursesettings input[type=\"checkbox\"]',\n firstSection: 'fieldset#id_coursesettings .fcontainer .grouped_settings.section_level',\n modCheckboxes: (modName) => `setting_activity_${modName}_`,\n};\n\nconst Suffixes = {\n userData: '_userdata',\n userInfo: '_userinfo',\n included: '_included',\n};\n\n/**\n * Adds select all/none links to the top of the backup/restore/import schema page.\n */\nexport default class BackupFormController {\n\n /**\n * Static module init method.\n * @param {Array} modNames - The names of the modules.\n * @returns {BackupFormController}\n */\n static init(modNames) {\n return new BackupFormController(modNames);\n }\n\n /**\n * Creates a new instance of the SchemaBackupForm class.\n * @param {Array} modNames - The names of the modules.\n */\n constructor(modNames) {\n this.modNames = modNames;\n this.scanFormUserData();\n this.addSelectorsToPage();\n }\n\n /**\n * Detect the user data attribute from the form.\n *\n * @private\n */\n scanFormUserData() {\n this.withuserdata = false;\n this.userDataSuffix = Suffixes.userData;\n\n const checkboxes = document.querySelectorAll(Selectors.checkboxes);\n if (!checkboxes) {\n return;\n }\n // Depending on the form, user data inclusion is called userinfo or userdata.\n for (const checkbox of checkboxes) {\n const name = checkbox.name;\n if (name.endsWith(Suffixes.userData)) {\n this.withuserdata = true;\n break;\n } else if (name.endsWith(Suffixes.userInfo)) {\n this.withuserdata = true;\n this.userDataSuffix = Suffixes.userInfo;\n break;\n }\n }\n }\n\n /**\n * Initializes all related events.\n *\n * @private\n * @param {HTMLElement} element - The element to attach the events to.\n */\n initEvents(element) {\n element.addEventListener('click', (event) => {\n const action = event.target.closest(Selectors.action);\n if (!action) {\n return;\n }\n event.preventDefault();\n\n const suffix = (action.dataset?.mdlType == 'userdata') ? this.userDataSuffix : Suffixes.included;\n\n this.changeSelection(\n action.dataset.mdlAction == 'selectall',\n suffix,\n action.dataset?.mdlMod ?? null\n );\n });\n }\n\n /**\n * Changes the selection according to the params.\n *\n * @private\n * @param {boolean} checked - The checked state for the checkboxes.\n * @param {string} suffix - The checkboxes suffix\n * @param {string} [modName] - The module name.\n */\n changeSelection(checked, suffix, modName) {\n const prefix = modName ? Selectors.modCheckboxes(modName) : null;\n\n let formId;\n\n const checkboxes = document.querySelectorAll(Selectors.checkboxes);\n for (const checkbox of checkboxes) {\n formId = formId ?? checkbox.closest('form').getAttribute('id');\n\n if (prefix && !checkbox.name.startsWith(prefix)) {\n continue;\n }\n if (checkbox.name.endsWith(suffix)) {\n checkbox.checked = checked;\n }\n }\n\n // At this point, we really need to persuade the form we are part of to\n // update all of its disabledIf rules.\n if (formId && M.form2) {\n M.form2.get(formId).formUpdatedExternally();\n }\n }\n\n /**\n * Generates the full selectors element to add to the page.\n *\n * @private\n * @returns {HTMLElement} The selectors element.\n */\n generateSelectorsElement() {\n const links = document.createElement('div');\n links.id = 'backup_selectors';\n this.initEvents(links);\n this.renderSelectorsTemplate(links);\n return links;\n }\n\n /**\n * Load the select all template.\n *\n * @private\n * @param {HTMLElement} element the container\n */\n renderSelectorsTemplate(element) {\n const data = {\n modules: this.getModulesTemplateData(),\n withuserdata: (this.withuserdata) ? true : undefined,\n };\n Templates.renderForPromise(\n 'core_backup/formselectall',\n data\n ).then(({html, js}) => {\n return Templates.replaceNodeContents(element, html, js);\n }).catch(Notification.exception);\n }\n\n /**\n * Generate the modules template data.\n *\n * @private\n * @returns {Array} of modules data.\n */\n getModulesTemplateData() {\n const modules = [];\n for (const modName in this.modNames) {\n if (!this.modNames.hasOwnProperty(modName)) {\n continue;\n }\n modules.push({\n modname: modName,\n heading: this.modNames[modName],\n });\n }\n return modules;\n }\n\n /**\n * Adds select all/none functionality to the backup form.\n *\n * @private\n */\n addSelectorsToPage() {\n const firstSection = document.querySelector(Selectors.firstSection);\n if (!firstSection) {\n // This is not a relevant page.\n return;\n }\n if (!firstSection.querySelector(Selectors.checkboxes)) {\n // No checkboxes.\n return;\n }\n\n // Add global select all/none options.\n const selector = this.generateSelectorsElement();\n firstSection.parentNode.insertBefore(selector, firstSection);\n }\n}\n"],"names":["Selectors","modName","Suffixes","BackupFormController","modNames","constructor","scanFormUserData","addSelectorsToPage","withuserdata","userDataSuffix","checkboxes","document","querySelectorAll","checkbox","name","endsWith","initEvents","element","addEventListener","event","action","target","closest","preventDefault","suffix","dataset","mdlType","this","changeSelection","mdlAction","_action$dataset2","mdlMod","checked","prefix","formId","getAttribute","startsWith","M","form2","get","formUpdatedExternally","generateSelectorsElement","links","createElement","id","renderSelectorsTemplate","data","modules","getModulesTemplateData","undefined","Templates","renderForPromise","then","_ref","html","js","replaceNodeContents","catch","Notification","exception","hasOwnProperty","push","modname","heading","firstSection","querySelector","selector","parentNode","insertBefore"],"mappings":";;;;;;;;;;;olCA8BMA,iBACM,oBADNA,qBAEU,4CAFVA,uBAGY,yEAHZA,wBAIcC,oCAAgCA,aAG9CC,kBACQ,YADRA,kBAEQ,YAFRA,kBAGQ,kBAMOC,iCAOLC,iBACD,IAAID,qBAAqBC,UAOpCC,YAAYD,eACHA,SAAWA,cACXE,wBACAC,qBAQTD,wBACSE,cAAe,OACfC,eAAiBP,wBAEhBQ,WAAaC,SAASC,iBAAiBZ,yBACxCU,eAIA,MAAMG,YAAYH,WAAY,OACzBI,KAAOD,SAASC,QAClBA,KAAKC,SAASb,mBAAoB,MAC7BM,cAAe,QAEjB,GAAIM,KAAKC,SAASb,mBAAoB,MACpCM,cAAe,OACfC,eAAiBP,0BAYlCc,WAAWC,SACPA,QAAQC,iBAAiB,SAAUC,yEACzBC,OAASD,MAAME,OAAOC,QAAQtB,sBAC/BoB,cAGLD,MAAMI,uBAEAC,OAAqC,qCAA3BJ,OAAOK,0DAASC,SAAyBC,KAAKlB,eAAiBP,uBAE1E0B,gBAC2B,aAA5BR,OAAOK,QAAQI,UACfL,8DACAJ,OAAOK,2CAAPK,iBAAgBC,8DAAU,SAatCH,gBAAgBI,QAASR,OAAQvB,eACvBgC,OAAShC,QAAUD,wBAAwBC,SAAW,SAExDiC,aAEExB,WAAaC,SAASC,iBAAiBZ,0BACxC,MAAMa,YAAYH,WAAY,aAC/BwB,uBAASA,kCAAUrB,SAASS,QAAQ,QAAQa,aAAa,MAErDF,SAAWpB,SAASC,KAAKsB,WAAWH,SAGpCpB,SAASC,KAAKC,SAASS,UACvBX,SAASmB,QAAUA,SAMvBE,QAAUG,EAAEC,OACZD,EAAEC,MAAMC,IAAIL,QAAQM,wBAU5BC,iCACUC,MAAQ/B,SAASgC,cAAc,cACrCD,MAAME,GAAK,wBACN5B,WAAW0B,YACXG,wBAAwBH,OACtBA,MASXG,wBAAwB5B,eACd6B,KAAO,CACTC,QAASpB,KAAKqB,yBACdxC,eAAemB,KAAKnB,mBAAuByC,GAE/CC,UAAUC,iBACN,4BACAL,MACFM,MAAKC,WAACC,KAACA,KAADC,GAAOA,gBACJL,UAAUM,oBAAoBvC,QAASqC,KAAMC,OACrDE,MAAMC,sBAAaC,WAS1BX,+BACUD,QAAU,OACX,MAAM9C,WAAW0B,KAAKvB,SAClBuB,KAAKvB,SAASwD,eAAe3D,UAGlC8C,QAAQc,KAAK,CACTC,QAAS7D,QACT8D,QAASpC,KAAKvB,SAASH,kBAGxB8C,QAQXxC,2BACUyD,aAAerD,SAASsD,cAAcjE,4BACvCgE,wBAIAA,aAAaC,cAAcjE,mCAM1BkE,SAAWvC,KAAKc,2BACtBuB,aAAaG,WAAWC,aAAaF,SAAUF"} \ No newline at end of file diff --git a/backup/util/ui/amd/src/schema_backup_form.js b/backup/util/ui/amd/src/schema_backup_form.js index cc4dcecbef229..669afe90f1c6b 100644 --- a/backup/util/ui/amd/src/schema_backup_form.js +++ b/backup/util/ui/amd/src/schema_backup_form.js @@ -142,10 +142,9 @@ export default class BackupFormController { } // At this point, we really need to persuade the form we are part of to - // update all of its disabledIf rules. However, as far as I can see, - // given the way that lib/form/form.js is written, that is impossible. - if (formId && M.form) { - M.form.updateFormState(formId); + // update all of its disabledIf rules. + if (formId && M.form2) { + M.form2.get(formId).formUpdatedExternally(); } } diff --git a/backup/util/ui/tests/behat/schema_select_all.feature b/backup/util/ui/tests/behat/schema_select_all.feature index 90e491c00867e..dfa2c7073fb38 100644 --- a/backup/util/ui/tests/behat/schema_select_all.feature +++ b/backup/util/ui/tests/behat/schema_select_all.feature @@ -63,7 +63,7 @@ Feature: Schema form selectors And the "Include Test data 2 user data" "checkbox" should be enabled @javascript - Scenario: The type options panell allow to select all and none of one activity type + Scenario: The type options panel allow to select all and none of one activity type Given the field "Section 1" matches value "1" And the field "Test assign 1" matches value "1" And the field "Test data 1" matches value "1" diff --git a/badges/tests/behat/backpack.feature b/badges/tests/behat/backpack.feature index edebf072857cc..be7d0571be9c9 100644 --- a/badges/tests/behat/backpack.feature +++ b/badges/tests/behat/backpack.feature @@ -138,7 +138,7 @@ Feature: Backpack badges And "Move up" "icon" should exist in the "https://dc.imsglobal.org" "table_row" And "Move down" "icon" should not exist in the "https://dc.imsglobal.org" "table_row" - @javascript + @javascript @MDL-82294 Scenario: Add a new site backpack with authentication details checkbox Given I am on homepage And I log in as "admin" @@ -154,7 +154,7 @@ Feature: Backpack badges And "Include authentication details with the backpack" "checkbox" should be visible And I click on "includeauthdetails" "checkbox" And I should see "Badge issuer email address" - And I should not see "Badge issuer password" +# And I should not see "Badge issuer password" And I set the field "apiversion" to "2" And "Include authentication details with the backpack" "checkbox" should be visible And I should see "Badge issuer email address" diff --git a/grade/tests/behat/grade_item_pass_data_to_advanced_page.feature b/grade/tests/behat/grade_item_pass_data_to_advanced_page.feature index d473031272367..86da84f847ba8 100644 --- a/grade/tests/behat/grade_item_pass_data_to_advanced_page.feature +++ b/grade/tests/behat/grade_item_pass_data_to_advanced_page.feature @@ -1,4 +1,4 @@ -@core @core_grades @javascript @testtt +@core @core_grades @javascript Feature: We carry over data from modal to advanced grade item settings In order to setup grade items quickly As an teacher diff --git a/lib/classes/output/requirements/page_requirements_manager.php b/lib/classes/output/requirements/page_requirements_manager.php index a417d4e9ac6c2..e9dd2025baa9b 100644 --- a/lib/classes/output/requirements/page_requirements_manager.php +++ b/lib/classes/output/requirements/page_requirements_manager.php @@ -1099,7 +1099,7 @@ public function js_call_amd($fullmodule, $func = null, $params = []) { $jsonparams[] = json_encode($param); } $strparams = implode(', ', $jsonparams); - if ($CFG->debugdeveloper) { + if ($CFG->debugdeveloper && $fullmodule !== 'core_form/form') { $toomanyparamslimit = 1024; if (strlen($strparams) > $toomanyparamslimit) { debugging('Too much data passed as arguments to js_call_amd("' . $fullmodule . '", "' . $func . diff --git a/lib/form/amd/build/form.min.js b/lib/form/amd/build/form.min.js new file mode 100644 index 0000000000000..3ede5c18b36b2 --- /dev/null +++ b/lib/form/amd/build/form.min.js @@ -0,0 +1,3 @@ +define("core_form/form",["exports","./changechecker","./submit","./form/rules","./form/display","core/pending"],(function(_exports,FormChangeChecker,Submit,_rules,MutateDom,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,FormChangeChecker=_interopRequireWildcard(FormChangeChecker),Submit=_interopRequireWildcard(Submit),_rules=_interopRequireDefault(_rules),MutateDom=_interopRequireWildcard(MutateDom),_pending=_interopRequireDefault(_pending),M.form2=M.form2||new Map;class Form{constructor(formID,dependencies){_defineProperty(this,"form",void 0),_defineProperty(this,"dependencies",void 0),_defineProperty(this,"editors",void 0),_defineProperty(this,"initialDisabledHidden",[]);const pendingPromise=new _pending.default("construction");this.form=document.querySelector("#".concat(formID)),this.dependencies=this.getDependencyMapper(dependencies),this.editors=this.findEditors(),this.rules=new _rules.default(this),this.applyInitialState(),this.registerEventListeners(),FormChangeChecker.watchForm(this.form),pendingPromise.resolve()}applyInitialState(){[...this.form.elements].forEach((element=>{(element.disabled||element.hidden)&&""!==element.name&&this.initialDisabledHidden.push(element.name)}));const map=this.generateDisplayMap();this.domDispatch(map,!0)}formUpdatedExternally(){const map=this.generateDisplayMap();if(this.domDispatch(map),void 0!==map.get("show")){this.elementNamesToDomNodes(map.get("show")).filter((node=>null!==node)).forEach((node=>{if(this.dependencies.has(node.name)){const secondLvlResult=this.displayMapPrune(this.dispatchDependencyRules(node));this.domDispatch(secondLvlResult)}}))}}generateDisplayMap(){const map=(0,MutateDom.mapTemplate)();return[...this.form.elements].forEach((element=>{if(this.dependencies.has(element.name)){const elDisplayMap=this.displayMapPrune(this.dispatchDependencyRules(element));for(const[key,value]of elDisplayMap)map.get(key).push(...value)}})),map}registerEventListeners(){this.form.addEventListener("change",(async e=>{if("submit"===e.target.type&&(FormChangeChecker.resetFormDirtyState(this.form),Submit.init(e.target.id)),"reset"===e.target.type&&(FormChangeChecker.resetFormDirtyState(this.form),this.form.reset()),this.dependencies.has(e.target.name)){FormChangeChecker.markFormChangedFromNode(e.target);const pendingPromise=new _pending.default("update"),results=this.displayMapPrune(this.dispatchDependencyRules(e.target));if(await this.domDispatch(results),void 0!==results.get("show")){this.elementNamesToDomNodes(results.get("show")).filter((node=>null!==node)).forEach((node=>{const pendingPromise=new _pending.default("updatesecond");if(node instanceof RadioNodeList&&node.forEach((n=>{if(this.dependencies.has(n.name)){const secondLvlResult=this.displayMapPrune(this.dispatchDependencyRules(n));this.domDispatch(secondLvlResult)}})),this.dependencies.has(node.name)){const secondLvlResult=this.displayMapPrune(this.dispatchDependencyRules(node));this.domDispatch(secondLvlResult)}pendingPromise.resolve()}))}pendingPromise.resolve()}}))}dispatchDependencyRules(target){const displayMap=(0,MutateDom.mapTemplate)();return this.dependencies.get(target.name).forEach(((dependants,ruleName)=>{(this.rules[ruleName]?this.rules[ruleName](target):this.rules.neq(target)).forEach(((nodeNames,displayOption)=>{displayMap.set(displayOption,[...displayMap.get(displayOption),...nodeNames.values()].flat())}))})),displayMap}displayMapPrune(displayMap){if(void 0!==displayMap.get("unlock")){if(void 0!==displayMap.get("hide")){const hideEvenIfUnlocked=displayMap.get("unlock").filter((x=>-1===displayMap.get("hide").indexOf(x.toString())));displayMap.set("unlock",hideEvenIfUnlocked)}if(void 0!==displayMap.get("lock")){const lockEvenIfUnlocked=displayMap.get("unlock").filter((x=>-1===displayMap.get("lock").indexOf(x.toString())));displayMap.set("unlock",lockEvenIfUnlocked)}}if(void 0!==displayMap.get("show")&&void 0!==displayMap.get("hide")){const hideEvenIfShown=displayMap.get("show").filter((x=>-1===displayMap.get("hide").indexOf(x.toString())));displayMap.set("show",hideEvenIfShown)}for(const[key,value]of displayMap)0===value.length&&displayMap.delete(key);return displayMap}getDependantsOfType(elementName,ruleName){var _this$dependencies$ge;return"undefined"!==this.dependencies.get(elementName)&&null!==(_this$dependencies$ge=this.dependencies.get(elementName).get(ruleName))&&void 0!==_this$dependencies$ge?_this$dependencies$ge:[]}domDispatch(elNamesMap){let firstRun=arguments.length>1&&void 0!==arguments[1]&&arguments[1];(elNamesMap=this.displayMapPrune(elNamesMap)).forEach(((elements,domUpdateOpt)=>{MutateDom[domUpdateOpt]&&(firstRun&&(elements=elements.filter((el=>!this.initialDisabledHidden.includes(el)))),this.elementNamesToDomNodes(elements).forEach((node=>{null!==node&&(node instanceof RadioNodeList?node.forEach((el=>{MutateDom[domUpdateOpt](el)})):MutateDom[domUpdateOpt](node))})))}))}elementNamesToDomNodes(elementNames){return elementNames.map((element=>this.form.querySelector('[data-groupname="'.concat(element,'"]'))?this.form.querySelector('[data-groupname="'.concat(element,'"]')):this.editors.get("".concat(element,"[text]"))?this.form.elements.namedItem("".concat(element,"[text]")):this.form.elements.namedItem(element)?this.form.elements.namedItem(element):this.form.elements.namedItem("id_".concat(element))))}findEditors(){let found=new Map;const fEditors=this.form.querySelectorAll('[data-fieldtype="editor"] textarea');return Array.from(fEditors).forEach((node=>{found.set(node.name,!0)})),found}getDependencyMapper(dependencies){const elementMap=new Map(Object.entries(dependencies));return elementMap.forEach(((elementrules,key)=>{const ruleMap=new Map(Object.entries(elementrules));ruleMap.forEach(((ruleComparisons,key)=>{const hideDefine=new Map(Object.entries(ruleComparisons));hideDefine.forEach(((action,compVal)=>{Array.isArray(action)&&(action={...action}),hideDefine.set(compVal,action)})),ruleMap.set(key,hideDefine)})),elementMap.set(key,ruleMap)})),elementMap}static init(formID,dependencies){const instance=new Form(formID,dependencies);return M.form2.set(formID,instance),instance}}return _exports.default=Form,_exports.default})); + +//# sourceMappingURL=form.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/form.min.js.map b/lib/form/amd/build/form.min.js.map new file mode 100644 index 0000000000000..9a82c3737383f --- /dev/null +++ b/lib/form/amd/build/form.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"form.min.js","sources":["../src/form.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 * This file contains JS functionality required by mforms and is included automatically\n * when required.\n *\n * @see /lib/formslib.php#L2548 Candidate for removal, depends on grouped rules.\n * @see /lib/amd/src/showhidesettings.js Candidate for removal.\n *\n * @module core_form/form\n * @copyright 2024 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport * as FormChangeChecker from './changechecker';\nimport * as Submit from './submit';\nimport Rules from './form/rules';\nimport {mapTemplate} from './form/display';\nimport * as MutateDom from './form/display';\nimport Pending from 'core/pending';\n\n/**\n * @var {Map} M.form2 Global map of forms currently on the page.\n */\nM.form2 = M.form2 || new Map();\n\nexport default class Form {\n /**\n * @var {HTMLFormElement} form Our very own form to work on.\n */\n form;\n\n /**\n * @var {Map} dependencies Our map of form dependencies.\n * @See this.getDependencyMapper() for structure info.\n */\n dependencies;\n\n /**\n * @var {Map} editors Our map of form editors used get the right selector.\n */\n editors;\n\n /**\n * @var {Array} initialDisabledHidden An array of element names that were hidden or disabled by default.\n */\n initialDisabledHidden = [];\n\n /**\n * Create a new Form instance.\n *\n * @param {String} formID The ID of the form to be managed.\n * @param {Object} dependencies The passed object of form dependencies.\n */\n constructor(formID, dependencies) {\n // Handle constructing the dependency map, finding editors and init the rules.\n const pendingPromise = new Pending('construction');\n this.form = document.querySelector(`#${formID}`);\n this.dependencies = this.getDependencyMapper(dependencies);\n this.editors = this.findEditors();\n this.rules = new Rules(this);\n\n // Apply the initial state of the form.\n this.applyInitialState();\n\n // Handle mutations within the form.\n this.registerEventListeners();\n FormChangeChecker.watchForm(this.form);\n pendingPromise.resolve();\n }\n\n /**\n * On page load, apply the initial state of the form by checking the shown items and running their rules.\n * We also want to confirm if anything has been hidden or disabled by a PHP callback on load and respect\n * their wishes on page load.\n */\n applyInitialState() {\n // Find any elements that are hidden or disabled by a PHP callback rule.\n [...this.form.elements].forEach((element) => {\n if ((element.disabled || element.hidden) && element.name !== '') {\n this.initialDisabledHidden.push(element.name);\n }\n });\n // Run through the form elements looking for anything to run rules against on load.\n const map = this.generateDisplayMap();\n this.domDispatch(map, true);\n }\n\n /**\n * Somewhere out there, we have a form that wants to ensure the state of the form reflects their changes.\n */\n formUpdatedExternally() {\n const map = this.generateDisplayMap();\n this.domDispatch(map);\n\n if (map.get('show') !== undefined) {\n const opened = this.elementNamesToDomNodes(map.get('show'));\n const filterNullNodes = opened.filter((node) => node !== null);\n\n filterNullNodes.forEach((node) => {\n if (this.dependencies.has(node.name)) {\n const secondLvlResult = this.displayMapPrune(this.dispatchDependencyRules(node));\n this.domDispatch(secondLvlResult);\n }\n });\n }\n }\n\n /**\n * Helper to iterate all the form elements that have a rule associated with them and generate a display map.\n *\n * @returns {Map}\n */\n generateDisplayMap() {\n const map = mapTemplate();\n [...this.form.elements].forEach((element) => {\n if (this.dependencies.has(element.name)) {\n const elDisplayMap = this.displayMapPrune(this.dispatchDependencyRules(element));\n for (const [key, value] of elDisplayMap) {\n map.get(key).push(...value);\n }\n }\n });\n return map;\n }\n\n /**\n * Add event listeners to the form.\n */\n registerEventListeners() {\n // TODO: Choice dropdown does not trigger here.\n this.form.addEventListener('change', async(e) => {\n if (e.target.type === 'submit') {\n FormChangeChecker.resetFormDirtyState(this.form);\n Submit.init(e.target.id);\n }\n if (e.target.type === 'reset') {\n FormChangeChecker.resetFormDirtyState(this.form);\n this.form.reset();\n }\n // Something changes based on this element.\n if (this.dependencies.has(e.target.name)) {\n FormChangeChecker.markFormChangedFromNode(e.target);\n const pendingPromise = new Pending('update');\n const results = this.displayMapPrune(this.dispatchDependencyRules(e.target));\n await this.domDispatch(results);\n\n // Given that we are showing something,\n // we'll do a second order check to see if we need to show more based on the new state of the form.\n if (results.get('show') !== undefined) {\n const opened = this.elementNamesToDomNodes(results.get('show'));\n const filterNullNodes = opened.filter((node) => node !== null);\n\n filterNullNodes.forEach((node) => {\n const pendingPromise = new Pending('updatesecond');\n // RadioNodeList needs to be iterated through as it is not a simple element.\n if (node instanceof RadioNodeList) {\n node.forEach((n) => {\n if (this.dependencies.has(n.name)) {\n const secondLvlResult = this.displayMapPrune(this.dispatchDependencyRules(n));\n this.domDispatch(secondLvlResult);\n }\n });\n }\n if (this.dependencies.has(node.name)) {\n const secondLvlResult = this.displayMapPrune(this.dispatchDependencyRules(node));\n this.domDispatch(secondLvlResult);\n }\n pendingPromise.resolve();\n });\n }\n pendingPromise.resolve();\n }\n });\n }\n\n /**\n * Dispatch the dependency rules to the appropriate rule handler and get back a map of display options.\n *\n * @param {HTMLFormElement} target The name associated to the element that has changed.\n * @returns {Map>} Actions to be taken along with element names that should be affected.\n */\n dispatchDependencyRules(target) {\n const displayMap = mapTemplate();\n this.dependencies.get(target.name).forEach((dependants, ruleName) => {\n // If the rule exists, use it, otherwise fallback to 'neq' which seems to be the \"default\" rule originally.\n const elNamesMap = this.rules[ruleName] ? this.rules[ruleName](target) : this.rules.neq(target);\n // Merge the current rule map with the final display map.\n elNamesMap.forEach((nodeNames, displayOption) => {\n // We want to merge in the new array values into the existing array otherwise,\n // we would get an array of arrays which is needless complexity.\n displayMap.set(displayOption, [...displayMap.get(displayOption), ...nodeNames.values()].flat());\n });\n });\n return displayMap;\n }\n\n /**\n * By default, the full display map contains empty entries and potential duplicated DOM node names.\n *\n * First: We review the unlock array for node names that have to be hidden. If a match is found,\n * the node name will be removed from being unlocked.\n * Then: We review the show array for node names that have to be hidden. If a match is found,\n * the node name will be removed from being shown as a rule has specified this should actually be hidden.\n * Finally: We get rid of any empty entries within the display map to prevent running pointless display updates.\n *\n * @param {Map} displayMap Map of elements and their associated rules to prune.\n * @returns {Map|Map<>} The pruned map or map even a fully pruned map if noting has to change.\n */\n displayMapPrune(displayMap) {\n // Filter any unlocked items that pegged to be hidden as they must be locked if they are hidden.\n // Using something like !displayMap.get('hide').toString().includes(x.toString()) did not work as\n // it could result in false positives such as contentfoobar includes content when doing the eval.\n if (displayMap.get('unlock') !== undefined) {\n if (displayMap.get('hide') !== undefined) {\n const hideEvenIfUnlocked = displayMap.get('unlock').filter(x => {\n return displayMap.get('hide').indexOf(x.toString()) === -1;\n });\n displayMap.set('unlock', hideEvenIfUnlocked);\n }\n if (displayMap.get('lock') !== undefined) {\n const lockEvenIfUnlocked = displayMap.get('unlock').filter(x => {\n return displayMap.get('lock').indexOf(x.toString()) === -1;\n });\n displayMap.set('unlock', lockEvenIfUnlocked);\n }\n }\n\n // Filter any shown items that pegged to be hidden.\n if (displayMap.get('show') !== undefined && displayMap.get('hide') !== undefined) {\n const hideEvenIfShown = displayMap.get('show').filter(x => {\n return displayMap.get('hide').indexOf(x.toString()) === -1;\n });\n displayMap.set('show', hideEvenIfShown);\n }\n\n // Remove any empty entries.\n for (const [key, value] of displayMap) {\n if (value.length === 0) {\n displayMap.delete(key);\n }\n }\n return displayMap;\n }\n\n /**\n * For a given element, get the names of DOM nodes that can change based on the given rule type name.\n *\n * @param {String} elementName The name of the element to get the dependants for.\n * @param {String} ruleName The rule type to get the dependants for.\n * @returns {Map|[]} Either the rule comparison value with associated node names to update or an empty array.\n */\n getDependantsOfType(elementName, ruleName) {\n return this.dependencies.get(elementName) !== 'undefined' ? this.dependencies.get(elementName).get(ruleName) ?? [] : [];\n }\n\n /**\n * Dispatch the DOM manipulation to the appropriate function.\n *\n * @param {Map} elNamesMap What needs to change.\n * @param {Boolean|Null} firstRun Whether this is the first run of the form if so,\n * filter some elements based on PHP rule callback values.\n */\n domDispatch(elNamesMap, firstRun = false) {\n elNamesMap = this.displayMapPrune(elNamesMap);\n // Go through the pruned display map and perform the requested display action.\n elNamesMap.forEach((elements, domUpdateOpt) => {\n // The requested display function somehow does not exist.\n if (!MutateDom[domUpdateOpt]) {\n return;\n }\n // If something was hidden or disabled by default via PHP rule callback, we don't want to touch it.\n if (firstRun) {\n elements = elements.filter((el) => !this.initialDisabledHidden.includes(el));\n }\n\n // Given the node names to update for a given display action, grab their associated HTMLFormElement and update them.\n this.elementNamesToDomNodes(elements).forEach((node) => {\n // Ensure we only update form items, instanceof check is a bit too much here as we have RadioNodeList items.\n if (node === null) {\n return;\n }\n if (node instanceof RadioNodeList) {\n node.forEach((el) => {\n MutateDom[domUpdateOpt](el);\n });\n } else {\n // Given a HTMLFormElement, perform the requested display action.\n MutateDom[domUpdateOpt](node);\n }\n });\n });\n }\n\n /**\n * Convert element names into DOM nodes based on the element name or a compound selector based on the given name.\n *\n * @param {Array} elementNames The name of dependent elements to get associated DOM nodes.\n * @returns {Array} DOM items to perform display actions on.\n */\n elementNamesToDomNodes(elementNames) {\n return elementNames.map((element) => {\n if (this.form.querySelector(`[data-groupname=\"${element}\"]`)) {\n return this.form.querySelector(`[data-groupname=\"${element}\"]`);\n }\n if (this.editors.get(`${element}[text]`)) {\n // Text editors are stupid.\n return this.form.elements.namedItem(`${element}[text]`);\n } else if (!this.form.elements.namedItem(element)) {\n // Grouped items are stupid.\n return this.form.elements.namedItem(`id_${element}`);\n }\n // Regular happy plain form item or RadioNodeList.\n return this.form.elements.namedItem(element);\n });\n }\n\n /**\n * During init, look through the form and identify which elements are editors.\n *\n * @returns {Map} Map of found editors.\n */\n findEditors() {\n let found = new Map();\n const fEditors = this.form.querySelectorAll('[data-fieldtype=\"editor\"] textarea');\n Array.from(fEditors).forEach((node) => {\n found.set(node.name, true);\n });\n return found;\n }\n\n /**\n * Convert the dependencies object into a map of elements and their associated rules.\n *\n * @example\n * Note: This is a simplified example of the returned map showing the rules for the grade type element in assign.\n *\n * \"grade[modgrade_type]\" => Map {\n * \"eq\" => Map {\n * \"none\" => Object {\n * 1 => Array [\n * \"advancedgradingmethod_submissions\",\n * \"gradecat\",\n * \"gradepass\",\n * \"completionusegrade\",\n * \"completionusegrade\",\n * ]\n * }\n * },\n * \"neq\" => Map {\n * \"point\" => Object {\n * 1 => Array [\n * \"grade[modgrade_point]\",\n * \"grade[modgrade_rescalegrades]\"\n * ]\n * },\n * \"scale\" => Object {\n * 1 => Array [\n * \"grade[modgrade_scale]\"\n * ]\n * }\n * }\n * }\n *\n * Note: If the value of grade[modgrade_type] === \"none\" then the array of elements defined should be hidden.\n * Note: If the value of grade[modgrade_type] !== \"point\" then the array of elements defined within the following:\n * \"eq\" => \"none\" && \"neq\" => \"scale\" should be hidden.\n *\n * Note: The object within the \"rule\" map can contain either 0 or 1 this helps determine if the element should be:\n * hidden or locked if the rule is met.\n * @See /lib/formslib.php DEP_DISABLE & DEP_HIDE.\n *\n * @param {Object} dependencies The supplied object of form dependencies to migrate into a map.\n * @returns {Map} A map of elements and their associated rules.\n */\n getDependencyMapper(dependencies) {\n /**\n * Convert the object into a first level map. i.e. elementName => ruleType.\n *\n * @type {Map} The map of rules associated to the given element.\n * @example \"grade[modgrade_type]\" => Map<\"eq\", \"neq\">\n */\n const elementMap = new Map(Object.entries(dependencies));\n elementMap.forEach((elementrules, key) => {\n /**\n * Convert the element rules object into a map.\n *\n * @type {Map} The map of rules associated to the given element.\n * @example \"eq\" => Map<\"none\" => Object>\n * @example \"neq\" => Map<\"point\" => Object, \"scale\" => Object>\n */\n const ruleMap = new Map(Object.entries(elementrules));\n ruleMap.forEach((ruleComparisons, key) => {\n /**\n * Convert any disabledIf rules into objects, so we can manage them the same as hideIf items.\n *\n * @type {Map} The map of comparison values.\n * @example \"none\" => \"none\" => Object\n * @example \"neq\" => \"point\" => Object\n */\n const hideDefine = new Map(Object.entries(ruleComparisons));\n hideDefine.forEach((action, compVal) => {\n if (Array.isArray(action)) {\n action = {...action};\n }\n hideDefine.set(compVal, action);\n });\n ruleMap.set(key, hideDefine);\n });\n elementMap.set(key, ruleMap);\n });\n return elementMap;\n }\n\n /**\n * Initialize the form and its dependencies. Also add the Form instance to the global state.\n *\n * @param {String} formID The ID of the form to be managed.\n * @param {Object} dependencies The passed object of form dependencies.\n * @returns {Form} An instance associated to a specific form on a given page.\n */\n static init(formID, dependencies) {\n const instance = new Form(formID, dependencies);\n // Add the Form instance to the global state so other modules can access this instance.\n M.form2.set(formID, instance);\n return instance;\n }\n}\n"],"names":["M","form2","Map","Form","constructor","formID","dependencies","pendingPromise","Pending","form","document","querySelector","this","getDependencyMapper","editors","findEditors","rules","Rules","applyInitialState","registerEventListeners","FormChangeChecker","watchForm","resolve","elements","forEach","element","disabled","hidden","name","initialDisabledHidden","push","map","generateDisplayMap","domDispatch","formUpdatedExternally","undefined","get","elementNamesToDomNodes","filter","node","has","secondLvlResult","displayMapPrune","dispatchDependencyRules","elDisplayMap","key","value","addEventListener","async","e","target","type","resetFormDirtyState","Submit","init","id","reset","markFormChangedFromNode","results","RadioNodeList","n","displayMap","dependants","ruleName","neq","nodeNames","displayOption","set","values","flat","hideEvenIfUnlocked","x","indexOf","toString","lockEvenIfUnlocked","hideEvenIfShown","length","delete","getDependantsOfType","elementName","elNamesMap","firstRun","domUpdateOpt","MutateDom","el","includes","elementNames","namedItem","found","fEditors","querySelectorAll","Array","from","elementMap","Object","entries","elementrules","ruleMap","ruleComparisons","hideDefine","action","compVal","isArray","instance"],"mappings":"qpDAuCAA,EAAEC,MAAQD,EAAEC,OAAS,IAAIC,UAEJC,KA4BjBC,YAAYC,OAAQC,kLARI,UAUdC,eAAiB,IAAIC,iBAAQ,qBAC9BC,KAAOC,SAASC,yBAAkBN,cAClCC,aAAeM,KAAKC,oBAAoBP,mBACxCQ,QAAUF,KAAKG,mBACfC,MAAQ,IAAIC,eAAML,WAGlBM,yBAGAC,yBACLC,kBAAkBC,UAAUT,KAAKH,MACjCF,eAAee,UAQnBJ,wBAEQN,KAAKH,KAAKc,UAAUC,SAASC,WACxBA,QAAQC,UAAYD,QAAQE,SAA4B,KAAjBF,QAAQG,WAC3CC,sBAAsBC,KAAKL,QAAQG,eAI1CG,IAAMnB,KAAKoB,0BACZC,YAAYF,KAAK,GAM1BG,8BACUH,IAAMnB,KAAKoB,6BACZC,YAAYF,UAEOI,IAApBJ,IAAIK,IAAI,QAAuB,CAChBxB,KAAKyB,uBAAuBN,IAAIK,IAAI,SACpBE,QAAQC,MAAkB,OAATA,OAEhCf,SAASe,UACjB3B,KAAKN,aAAakC,IAAID,KAAKX,MAAO,OAC5Ba,gBAAkB7B,KAAK8B,gBAAgB9B,KAAK+B,wBAAwBJ,YACrEN,YAAYQ,sBAWjCT,2BACUD,KAAM,qCACRnB,KAAKH,KAAKc,UAAUC,SAASC,aACzBb,KAAKN,aAAakC,IAAIf,QAAQG,MAAO,OAC/BgB,aAAehC,KAAK8B,gBAAgB9B,KAAK+B,wBAAwBlB,cAClE,MAAOoB,IAAKC,SAAUF,aACvBb,IAAIK,IAAIS,KAAKf,QAAQgB,WAI1Bf,IAMXZ,8BAESV,KAAKsC,iBAAiB,UAAUC,MAAAA,OACX,WAAlBC,EAAEC,OAAOC,OACT/B,kBAAkBgC,oBAAoBxC,KAAKH,MAC3C4C,OAAOC,KAAKL,EAAEC,OAAOK,KAEH,UAAlBN,EAAEC,OAAOC,OACT/B,kBAAkBgC,oBAAoBxC,KAAKH,WACtCA,KAAK+C,SAGV5C,KAAKN,aAAakC,IAAIS,EAAEC,OAAOtB,MAAO,CACtCR,kBAAkBqC,wBAAwBR,EAAEC,cACtC3C,eAAiB,IAAIC,iBAAQ,UAC7BkD,QAAU9C,KAAK8B,gBAAgB9B,KAAK+B,wBAAwBM,EAAEC,kBAC9DtC,KAAKqB,YAAYyB,cAIKvB,IAAxBuB,QAAQtB,IAAI,QAAuB,CACpBxB,KAAKyB,uBAAuBqB,QAAQtB,IAAI,SACxBE,QAAQC,MAAkB,OAATA,OAEhCf,SAASe,aACfhC,eAAiB,IAAIC,iBAAQ,mBAE/B+B,gBAAgBoB,eAChBpB,KAAKf,SAASoC,OACNhD,KAAKN,aAAakC,IAAIoB,EAAEhC,MAAO,OACzBa,gBAAkB7B,KAAK8B,gBAAgB9B,KAAK+B,wBAAwBiB,SACrE3B,YAAYQ,qBAIzB7B,KAAKN,aAAakC,IAAID,KAAKX,MAAO,OAC5Ba,gBAAkB7B,KAAK8B,gBAAgB9B,KAAK+B,wBAAwBJ,YACrEN,YAAYQ,iBAErBlC,eAAee,aAGvBf,eAAee,cAW3BqB,wBAAwBO,cACdW,YAAa,uCACdvD,aAAa8B,IAAIc,OAAOtB,MAAMJ,SAAQ,CAACsC,WAAYC,aAEjCnD,KAAKI,MAAM+C,UAAYnD,KAAKI,MAAM+C,UAAUb,QAAUtC,KAAKI,MAAMgD,IAAId,SAE7E1B,SAAQ,CAACyC,UAAWC,iBAG3BL,WAAWM,IAAID,cAAe,IAAIL,WAAWzB,IAAI8B,kBAAmBD,UAAUG,UAAUC,cAGzFR,WAeXnB,gBAAgBmB,oBAIqB1B,IAA7B0B,WAAWzB,IAAI,UAAyB,SACTD,IAA3B0B,WAAWzB,IAAI,QAAuB,OAChCkC,mBAAqBT,WAAWzB,IAAI,UAAUE,QAAOiC,IACE,IAAlDV,WAAWzB,IAAI,QAAQoC,QAAQD,EAAEE,cAE5CZ,WAAWM,IAAI,SAAUG,4BAEEnC,IAA3B0B,WAAWzB,IAAI,QAAuB,OAChCsC,mBAAqBb,WAAWzB,IAAI,UAAUE,QAAOiC,IACE,IAAlDV,WAAWzB,IAAI,QAAQoC,QAAQD,EAAEE,cAE5CZ,WAAWM,IAAI,SAAUO,6BAKFvC,IAA3B0B,WAAWzB,IAAI,cAAoDD,IAA3B0B,WAAWzB,IAAI,QAAuB,OACxEuC,gBAAkBd,WAAWzB,IAAI,QAAQE,QAAOiC,IACO,IAAlDV,WAAWzB,IAAI,QAAQoC,QAAQD,EAAEE,cAE5CZ,WAAWM,IAAI,OAAQQ,qBAItB,MAAO9B,IAAKC,SAAUe,WACF,IAAjBf,MAAM8B,QACNf,WAAWgB,OAAOhC,YAGnBgB,WAUXiB,oBAAoBC,YAAahB,0CACiB,cAAvCnD,KAAKN,aAAa8B,IAAI2C,4CAA+BnE,KAAKN,aAAa8B,IAAI2C,aAAa3C,IAAI2B,iEAAkB,GAUzH9B,YAAY+C,gBAAYC,kEACpBD,WAAapE,KAAK8B,gBAAgBsC,aAEvBxD,SAAQ,CAACD,SAAU2D,gBAErBC,UAAUD,gBAIXD,WACA1D,SAAWA,SAASe,QAAQ8C,KAAQxE,KAAKiB,sBAAsBwD,SAASD,YAIvE/C,uBAAuBd,UAAUC,SAASe,OAE9B,OAATA,OAGAA,gBAAgBoB,cAChBpB,KAAKf,SAAS4D,KACVD,UAAUD,cAAcE,OAI5BD,UAAUD,cAAc3C,cAYxCF,uBAAuBiD,qBACZA,aAAavD,KAAKN,SACjBb,KAAKH,KAAKE,yCAAkCc,eACrCb,KAAKH,KAAKE,yCAAkCc,eAEnDb,KAAKE,QAAQsB,cAAOX,mBAEbb,KAAKH,KAAKc,SAASgE,oBAAa9D,mBAC/Bb,KAAKH,KAAKc,SAASgE,UAAU9D,SAKlCb,KAAKH,KAAKc,SAASgE,UAAU9D,SAHzBb,KAAKH,KAAKc,SAASgE,uBAAgB9D,YAYtDV,kBACQyE,MAAQ,IAAItF,UACVuF,SAAW7E,KAAKH,KAAKiF,iBAAiB,6CAC5CC,MAAMC,KAAKH,UAAUjE,SAASe,OAC1BiD,MAAMrB,IAAI5B,KAAKX,MAAM,MAElB4D,MA+CX3E,oBAAoBP,oBAOVuF,WAAa,IAAI3F,IAAI4F,OAAOC,QAAQzF,sBAC1CuF,WAAWrE,SAAQ,CAACwE,aAAcnD,aAQxBoD,QAAU,IAAI/F,IAAI4F,OAAOC,QAAQC,eACvCC,QAAQzE,SAAQ,CAAC0E,gBAAiBrD,aAQxBsD,WAAa,IAAIjG,IAAI4F,OAAOC,QAAQG,kBAC1CC,WAAW3E,SAAQ,CAAC4E,OAAQC,WACpBV,MAAMW,QAAQF,UACdA,OAAS,IAAIA,SAEjBD,WAAWhC,IAAIkC,QAASD,WAE5BH,QAAQ9B,IAAItB,IAAKsD,eAErBN,WAAW1B,IAAItB,IAAKoD,YAEjBJ,uBAUCxF,OAAQC,oBACViG,SAAW,IAAIpG,KAAKE,OAAQC,qBAElCN,EAAEC,MAAMkE,IAAI9D,OAAQkG,UACbA"} \ No newline at end of file diff --git a/lib/form/amd/build/form/display.min.js b/lib/form/amd/build/form/display.min.js new file mode 100644 index 0000000000000..f2346d69e3749 --- /dev/null +++ b/lib/form/amd/build/form/display.min.js @@ -0,0 +1,3 @@ +define("core_form/form/display",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unlock=_exports.show=_exports.mapTemplate=_exports.lock=_exports.hide=_exports.determineDisplayMap=void 0;const dependencyBehaviour_disable=0,dependencyBehaviour_hide=1;_exports.mapTemplate=()=>new Map([["hide",[]],["show",[]],["lock",[]],["unlock",[]]]);_exports.determineDisplayMap=(dependant,displayMap,lock)=>{const hide=!!dependant.hasOwnProperty(dependencyBehaviour_hide)&&lock;dependant.hasOwnProperty(dependencyBehaviour_disable)&&(displayMap.get("show").push(dependant[dependencyBehaviour_disable]),lock?displayMap.get("lock").push(dependant[dependencyBehaviour_disable]):displayMap.get("unlock").push(dependant[dependencyBehaviour_disable])),dependant.hasOwnProperty(dependencyBehaviour_hide)&&(hide||displayMap.get("hide").toString().includes(dependant[dependencyBehaviour_hide].toString())?displayMap.get("hide").push(dependant[dependencyBehaviour_hide]):displayMap.get("show").push(dependant[dependencyBehaviour_hide]))};_exports.lock=element=>{element.setAttribute("disabled","disabled"),("editor"===element.dataset.fieldtype||element.closest('[data-fieldtype="editor"]'))&&(element.setAttribute("readonly","readonly"),element.dispatchEvent(new Event("form:editorUpdated")))};_exports.unlock=element=>{element.removeAttribute("disabled"),("editor"===element.dataset.fieldtype||element.closest('[data-fieldtype="editor"]'))&&(element.removeAttribute("readonly"),element.dispatchEvent(new Event("form:editorUpdated")))};_exports.hide=element=>{element.setAttribute("disabled","disabled");const parent=element.closest(".fitem");if(parent){parent.setAttribute("hidden","hidden"),parent.classList.add("d-none");const label=document.querySelector('label[for="'+element.id+'"]');label&&(label.setAttribute("hidden","hidden"),label.classList.add("d-none"))}};_exports.show=element=>{element.removeAttribute("disabled");const parent=element.closest(".fitem");if(parent){parent.removeAttribute("hidden"),parent.classList.remove("d-none");const label=document.querySelector('label[for="'+element.id+'"]');label&&(label.removeAttribute("hidden"),label.classList.remove("d-none"))}}})); + +//# sourceMappingURL=display.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/form/display.min.js.map b/lib/form/amd/build/form/display.min.js.map new file mode 100644 index 0000000000000..9371efdbd3a19 --- /dev/null +++ b/lib/form/amd/build/form/display.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"display.min.js","sources":["../../src/form/display.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 * This file contains some helper functions to change the visual state of the form elements.\n *\n * @module core_form/form/dom\n * @copyright 2024 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\n/**\n * A small object that defines the behaviour of the dependency rules for readability.\n *\n * @type {{hide: number, disable: number}}\n */\nconst dependencyBehaviour = {\n disable: 0,\n hide: 1,\n};\n\n/**\n * The archetypal display map that we'll be using to figure out what has to change and how.\n *\n * @returns {Map}\n */\nexport const mapTemplate = () => {\n return new Map([\n ['hide', []],\n ['show', []],\n ['lock', []],\n ['unlock', []],\n ]);\n};\n\n/**\n * Considering the dependant object and if we need to lock it, assign the elements to the correct displayMap key.\n *\n * @param {Object} dependant The dependant object that contains the rules for hiding and locking.\n * @param {Map} displayMap The aggregation of elements that should be shown, hidden, locked, or unlocked.\n * @param {Boolean} lock According to the rules, should the element be locked or unlocked.\n */\nexport const determineDisplayMap = (dependant, displayMap, lock) => {\n // Determine via a combination of the lock state & whether the rule is a disable or hide rule, what to do.\n const hide = dependant.hasOwnProperty(dependencyBehaviour.hide) ? lock : false;\n // This is a disable rule, so we want to always show it and optionally disable it.\n if (dependant.hasOwnProperty(dependencyBehaviour.disable)) {\n displayMap.get('show').push(dependant[dependencyBehaviour.disable]);\n if (lock) {\n displayMap.get('lock').push(dependant[dependencyBehaviour.disable]);\n } else {\n displayMap.get('unlock').push(dependant[dependencyBehaviour.disable]);\n }\n }\n // Conditionally hide the element based on the lock status.\n if (dependant.hasOwnProperty(dependencyBehaviour.hide)) {\n // Prevent showing an element if it has already been defined hidden.\n if (!hide && !displayMap.get('hide').toString().includes(dependant[dependencyBehaviour.hide].toString())) {\n displayMap.get('show').push(dependant[dependencyBehaviour.hide]);\n } else {\n displayMap.get('hide').push(dependant[dependencyBehaviour.hide]);\n }\n }\n};\n\n/**\n * Disable an element.\n *\n * @param {HTMLElement} element The element to be disabled.\n */\nexport const lock = (element) => {\n element.setAttribute('disabled', 'disabled');\n if (element.dataset.fieldtype === 'editor' || element.closest('[data-fieldtype=\"editor\"]')) {\n element.setAttribute('readonly', 'readonly');\n element.dispatchEvent(new Event('form:editorUpdated'));\n }\n};\n\n/**\n * Enable an element.\n *\n * @param {HTMLElement} element The element to be enabled.\n */\nexport const unlock = (element) => {\n element.removeAttribute('disabled');\n if (element.dataset.fieldtype === 'editor' || element.closest('[data-fieldtype=\"editor\"]')) {\n element.removeAttribute('readonly');\n element.dispatchEvent(new Event('form:editorUpdated'));\n }\n};\n\n/**\n * Hide an element.\n *\n * @param {HTMLElement} element The element to be hidden.\n */\nexport const hide = (element) => {\n element.setAttribute('disabled', 'disabled');\n const parent = element.closest('.fitem');\n if (parent) {\n parent.setAttribute('hidden', 'hidden');\n parent.classList.add('d-none');\n // Hide the label as well.\n const label = document.querySelector('label[for=\"' + element.id + '\"]');\n if (label) {\n label.setAttribute('hidden', 'hidden');\n label.classList.add('d-none');\n }\n }\n};\n\n/**\n * Show an element.\n *\n * @param {HTMLElement} element The elements to be shown.\n */\nexport const show = (element) => {\n element.removeAttribute('disabled');\n const parent = element.closest('.fitem');\n if (parent) {\n parent.removeAttribute('hidden');\n parent.classList.remove('d-none');\n // Show the label as well.\n const label = document.querySelector('label[for=\"' + element.id + '\"]');\n if (label) {\n label.removeAttribute('hidden');\n label.classList.remove('d-none');\n }\n }\n};\n"],"names":["dependencyBehaviour","Map","dependant","displayMap","lock","hide","hasOwnProperty","get","push","toString","includes","element","setAttribute","dataset","fieldtype","closest","dispatchEvent","Event","removeAttribute","parent","classList","add","label","document","querySelector","id","remove"],"mappings":"iPA8BMA,4BACO,EADPA,yBAEI,uBAQiB,IAChB,IAAIC,IAAI,CACX,CAAC,OAAQ,IACT,CAAC,OAAQ,IACT,CAAC,OAAQ,IACT,CAAC,SAAU,mCAWgB,CAACC,UAAWC,WAAYC,cAEjDC,OAAOH,UAAUI,eAAeN,2BAA4BI,KAE9DF,UAAUI,eAAeN,+BACzBG,WAAWI,IAAI,QAAQC,KAAKN,UAAUF,8BAClCI,KACAD,WAAWI,IAAI,QAAQC,KAAKN,UAAUF,8BAEtCG,WAAWI,IAAI,UAAUC,KAAKN,UAAUF,+BAI5CE,UAAUI,eAAeN,4BAEpBK,MAASF,WAAWI,IAAI,QAAQE,WAAWC,SAASR,UAAUF,0BAA0BS,YAGzFN,WAAWI,IAAI,QAAQC,KAAKN,UAAUF,2BAFtCG,WAAWI,IAAI,QAAQC,KAAKN,UAAUF,2CAY7BW,UACjBA,QAAQC,aAAa,WAAY,aACC,WAA9BD,QAAQE,QAAQC,WAA0BH,QAAQI,QAAQ,gCAC1DJ,QAAQC,aAAa,WAAY,YACjCD,QAAQK,cAAc,IAAIC,MAAM,yCASjBN,UACnBA,QAAQO,gBAAgB,aACU,WAA9BP,QAAQE,QAAQC,WAA0BH,QAAQI,QAAQ,gCAC1DJ,QAAQO,gBAAgB,YACxBP,QAAQK,cAAc,IAAIC,MAAM,uCASnBN,UACjBA,QAAQC,aAAa,WAAY,kBAC3BO,OAASR,QAAQI,QAAQ,aAC3BI,OAAQ,CACRA,OAAOP,aAAa,SAAU,UAC9BO,OAAOC,UAAUC,IAAI,gBAEfC,MAAQC,SAASC,cAAc,cAAgBb,QAAQc,GAAK,MAC9DH,QACAA,MAAMV,aAAa,SAAU,UAC7BU,MAAMF,UAAUC,IAAI,2BAUXV,UACjBA,QAAQO,gBAAgB,kBAClBC,OAASR,QAAQI,QAAQ,aAC3BI,OAAQ,CACRA,OAAOD,gBAAgB,UACvBC,OAAOC,UAAUM,OAAO,gBAElBJ,MAAQC,SAASC,cAAc,cAAgBb,QAAQc,GAAK,MAC9DH,QACAA,MAAMJ,gBAAgB,UACtBI,MAAMF,UAAUM,OAAO"} \ No newline at end of file diff --git a/lib/form/amd/build/form/rules.min.js b/lib/form/amd/build/form/rules.min.js new file mode 100644 index 0000000000000..506373b6938df --- /dev/null +++ b/lib/form/amd/build/form/rules.min.js @@ -0,0 +1,3 @@ +define("core_form/form/rules",["exports","./display"],(function(_exports,_display){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default=class{common(){return{displayMap:(0,_display.mapTemplate)(),lock:!1}}notchecked(target){let{displayMap:displayMap,lock:lock}=this.common();return this.formInstance.getDependantsOfType(target.name,"notchecked").forEach(((dependant,key)=>{lock=Boolean(key)!==target.checked,(0,_display.determineDisplayMap)(dependant,displayMap,lock)})),displayMap}checked(target){let{displayMap:displayMap,lock:lock}=this.common();return this.formInstance.getDependantsOfType(target.name,"checked").forEach(((dependant,key)=>{lock=Boolean(key)===target.checked,(0,_display.determineDisplayMap)(dependant,displayMap,lock)})),displayMap}eq(target){let{displayMap:displayMap,lock:lock}=this.common();return this.formInstance.getDependantsOfType(target.name,"eq").forEach(((dependant,key)=>{lock="radio"===target.type?String(key)===String(this.getRadioFieldVal(target)):"hidden"===target.type&&this.getHiddenCkbs(target)?target.checked===Boolean(key):"checkbox"!==target.type||target.checked?target.classList.contains("filepickerhidden")?!M.form_filepicker.instances[target.id].fileadded:target.value===key:target.checked===Boolean(key),(0,_display.determineDisplayMap)(dependant,displayMap,lock)})),displayMap}neq(target){var _this$formInstance$ge,_this$formInstance$ge2,_this$formInstance$ge3,_this$formInstance$ge4,_this$formInstance$ge5,_this$formInstance$ge6;let{displayMap:displayMap,lock:lock}=this.common();return new Map([...null!==(_this$formInstance$ge=null===(_this$formInstance$ge2=this.formInstance.getDependantsOfType(target.name,"neq"))||void 0===_this$formInstance$ge2?void 0:_this$formInstance$ge2.entries())&&void 0!==_this$formInstance$ge?_this$formInstance$ge:[],...null!==(_this$formInstance$ge3=null===(_this$formInstance$ge4=this.formInstance.getDependantsOfType(target.name,"ne"))||void 0===_this$formInstance$ge4?void 0:_this$formInstance$ge4.entries())&&void 0!==_this$formInstance$ge3?_this$formInstance$ge3:[],...null!==(_this$formInstance$ge5=null===(_this$formInstance$ge6=this.formInstance.getDependantsOfType(target.name,"noteq"))||void 0===_this$formInstance$ge6?void 0:_this$formInstance$ge6.entries())&&void 0!==_this$formInstance$ge5?_this$formInstance$ge5:[]]).forEach(((dependant,key)=>{lock="radio"===target.type?String(key)!==String(this.getRadioFieldVal(target)):"hidden"===target.type&&this.getHiddenCkbs(target)?target.checked!==Boolean(key):"checkbox"!==target.type||target.checked?target.classList.contains("filepickerhidden")?!!M.form_filepicker.instances[target.id].fileadded:target.value!==key:target.checked===Boolean(key),(0,_display.determineDisplayMap)(dependant,displayMap,lock)})),displayMap}in(target){let{displayMap:displayMap,lock:lock}=this.common();return this.formInstance.getDependantsOfType(target.name,"in").forEach(((dependant,key)=>{lock=key.split("|").includes(target.value),(0,_display.determineDisplayMap)(dependant,displayMap,lock)})),displayMap}getRadioFieldVal(target){return"radio"===target.type?this.formInstance.form.elements.namedItem(target.name).value:target.value}getHiddenCkbs(target){return 0!==this.formInstance.form.querySelectorAll('input[type=checkbox][name="'+target.name+'"]').length}constructor(form){var obj,key,value;value=void 0,(key="formInstance")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.formInstance=form}},_exports.default})); + +//# sourceMappingURL=rules.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/form/rules.min.js.map b/lib/form/amd/build/form/rules.min.js.map new file mode 100644 index 0000000000000..be49a9c046d5a --- /dev/null +++ b/lib/form/amd/build/form/rules.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"rules.min.js","sources":["../../src/form/rules.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 * This file contains a set of rules that elements can be compared against to determine if they should be shown, hidden, etc...\n *\n * @See /lib/pear/HTML/QuickForm/Rule/Compare.php\n * @See https://pear.php.net/manual/en/package.html.html-quickform2.rules.list.php for a list of available rules.\n *\n * @module core_form/form/rules\n * @copyright 2024 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n\"use strict\";\n\nimport {determineDisplayMap, mapTemplate} from './display';\n\nexport default class Rules {\n /**\n * @var {Form} formInstance The instance of the form class that has a DOM node & references matched.\n */\n formInstance;\n\n /**\n * Boilerplate for the common variables.\n * @returns {{displayMap: Map, lock: boolean}}\n */\n common() {\n const displayMap = mapTemplate();\n let lock = false;\n return {displayMap, lock};\n }\n\n /**\n * Compare the value of the checkbox vs if it is not checked.\n *\n * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule.\n * @returns {Map} Actions to be taken along with elements that should be affected.\n */\n notchecked(target) {\n let {displayMap, lock} = this.common();\n this.formInstance.getDependantsOfType(target.name, 'notchecked').forEach((dependant, key) => {\n // if (target.disabled) {\n // lock = true;\n // } else {\n // lock = Boolean(key) !== target.checked;\n // }\n lock = Boolean(key) !== target.checked;\n determineDisplayMap(dependant, displayMap, lock);\n });\n return displayMap;\n }\n\n /**\n * Compare the value of the checkbox and if it is checked.\n *\n * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule.\n * @returns {Map} Actions to be taken along with elements that should be affected.\n */\n checked(target) {\n let {displayMap, lock} = this.common();\n this.formInstance.getDependantsOfType(target.name, 'checked').forEach((dependant, key) => {\n // if (target.disabled) {\n // lock = true;\n // } else {\n // lock = Boolean(key) === target.checked;\n // }\n lock = Boolean(key) === target.checked;\n determineDisplayMap(dependant, displayMap, lock);\n });\n return displayMap;\n }\n\n /**\n * Compare the value of the changed DOM node equals the rule value.\n *\n * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule.\n * @returns {Map} Actions to be taken along with elements that should be affected.\n */\n eq(target) {\n let {displayMap, lock} = this.common();\n this.formInstance.getDependantsOfType(target.name, 'eq').forEach((dependant, key) => {\n if (target.type === 'radio') {\n lock = String(key) === String(this.getRadioFieldVal(target));\n } else if (target.type === 'hidden' && this.getHiddenCkbs(target)) {\n // This is the hidden input that is part of an advcheckbox.\n lock = target.checked === Boolean(key);\n } else if (target.type === 'checkbox' && !target.checked) {\n lock = target.checked === Boolean(key);\n } else if (target.classList.contains('filepickerhidden')) {\n lock = !M.form_filepicker.instances[target.id].fileadded;\n } else {\n lock = target.value === key;\n }\n determineDisplayMap(dependant, displayMap, lock);\n });\n return displayMap;\n }\n\n /**\n * Compare the value of the changed DOM node to the requested rule value.\n * @See Moodle has some interesting aliasing ne && noteq, this is also the old \"default\" rule.\n *\n * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule.\n * @returns {Map} Actions to be taken along with elements that should be affected.\n */\n neq(target) {\n let {displayMap, lock} = this.common();\n // Get all the aliases of neq and check them all at once.\n const maps = new Map([\n ...this.formInstance.getDependantsOfType(target.name, 'neq')?.entries() ?? [],\n ...this.formInstance.getDependantsOfType(target.name, 'ne')?.entries() ?? [],\n ...this.formInstance.getDependantsOfType(target.name, 'noteq')?.entries() ?? [],\n ]);\n maps.forEach((dependant, key) => {\n if (target.type === 'radio') {\n lock = String(key) !== String(this.getRadioFieldVal(target));\n } else if (target.type === 'hidden' && this.getHiddenCkbs(target)) {\n // This is the hidden input that is part of an advcheckbox.\n lock = target.checked !== Boolean(key);\n } else if (target.type === 'checkbox' && !target.checked) {\n lock = target.checked === Boolean(key);\n } else if (target.classList.contains('filepickerhidden')) {\n lock = !!M.form_filepicker.instances[target.id].fileadded;\n } else {\n lock = target.value !== key;\n }\n determineDisplayMap(dependant, displayMap, lock);\n });\n return displayMap;\n }\n\n /**\n * Compare the value of the changed DOM node vs if it is in the defined values passed as a rule.\n *\n * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule.\n * @returns {Map} Actions to be taken along with elements that should be affected.\n */\n in(target) {\n let {displayMap, lock} = this.common();\n this.formInstance.getDependantsOfType(target.name, 'in').forEach((dependant, key) => {\n lock = key.split('|').includes(target.value);\n determineDisplayMap(dependant, displayMap, lock);\n });\n return displayMap;\n }\n\n /**\n * Radio fields are a bit different, they need to be handled differently.\n *\n * @param {HTMLFormElement} target The changed DOM node to find a potential radio field for.\n * @returns {String} The value of the radio field.\n */\n getRadioFieldVal(target) {\n return target.type === 'radio' ? this.formInstance.form.elements.namedItem(target.name).value : target.value;\n }\n\n /**\n * A small helper to determine if the advcheckboxes are being used.\n *\n * @param {HTMLElement} target The target element to get the hidden checkboxes for.\n * @returns {boolean} Is this a hidden checkbox?\n */\n getHiddenCkbs(target) {\n return this.formInstance.form.querySelectorAll('input[type=checkbox][name=\"' + target.name + '\"]').length !== 0;\n }\n\n /**\n * Constructor for the Rules class.\n *\n * @param {Form} form The form object that the rules are being applied to.\n */\n constructor(form) {\n this.formInstance = form;\n }\n}\n"],"names":["common","displayMap","lock","notchecked","target","this","formInstance","getDependantsOfType","name","forEach","dependant","key","Boolean","checked","eq","type","String","getRadioFieldVal","getHiddenCkbs","classList","contains","M","form_filepicker","instances","id","fileadded","value","neq","Map","_this$formInstance$ge2","entries","_this$formInstance$ge4","_this$formInstance$ge6","in","split","includes","form","elements","namedItem","querySelectorAll","length","constructor"],"mappings":"iMAwCIA,eAGW,CAACC,YAFW,0BAECC,MADT,GAUfC,WAAWC,YACHH,WAACA,WAADC,KAAaA,MAAQG,KAAKL,qBACzBM,aAAaC,oBAAoBH,OAAOI,KAAM,cAAcC,SAAQ,CAACC,UAAWC,OAMjFT,KAAOU,QAAQD,OAASP,OAAOS,yCACXH,UAAWT,WAAYC,SAExCD,WASXY,QAAQT,YACAH,WAACA,WAADC,KAAaA,MAAQG,KAAKL,qBACzBM,aAAaC,oBAAoBH,OAAOI,KAAM,WAAWC,SAAQ,CAACC,UAAWC,OAM9ET,KAAOU,QAAQD,OAASP,OAAOS,yCACXH,UAAWT,WAAYC,SAExCD,WASXa,GAAGV,YACKH,WAACA,WAADC,KAAaA,MAAQG,KAAKL,qBACzBM,aAAaC,oBAAoBH,OAAOI,KAAM,MAAMC,SAAQ,CAACC,UAAWC,OAErET,KADgB,UAAhBE,OAAOW,KACAC,OAAOL,OAASK,OAAOX,KAAKY,iBAAiBb,SAC7B,WAAhBA,OAAOW,MAAqBV,KAAKa,cAAcd,QAE/CA,OAAOS,UAAYD,QAAQD,KACX,aAAhBP,OAAOW,MAAwBX,OAAOS,QAEtCT,OAAOe,UAAUC,SAAS,qBACzBC,EAAEC,gBAAgBC,UAAUnB,OAAOoB,IAAIC,UAExCrB,OAAOsB,QAAUf,IAJjBP,OAAOS,UAAYD,QAAQD,sCAMlBD,UAAWT,WAAYC,SAExCD,WAUX0B,IAAIvB,yJACIH,WAACA,WAADC,KAAaA,MAAQG,KAAKL,gBAEjB,IAAI4B,IAAI,iEACdvB,KAAKC,aAAaC,oBAAoBH,OAAOI,KAAM,gDAAnDqB,uBAA2DC,iEAAa,oEACxEzB,KAAKC,aAAaC,oBAAoBH,OAAOI,KAAM,+CAAnDuB,uBAA0DD,mEAAa,oEACvEzB,KAAKC,aAAaC,oBAAoBH,OAAOI,KAAM,kDAAnDwB,uBAA6DF,mEAAa,KAE5ErB,SAAQ,CAACC,UAAWC,OAEjBT,KADgB,UAAhBE,OAAOW,KACAC,OAAOL,OAASK,OAAOX,KAAKY,iBAAiBb,SAC7B,WAAhBA,OAAOW,MAAqBV,KAAKa,cAAcd,QAE/CA,OAAOS,UAAYD,QAAQD,KACX,aAAhBP,OAAOW,MAAwBX,OAAOS,QAEtCT,OAAOe,UAAUC,SAAS,sBACxBC,EAAEC,gBAAgBC,UAAUnB,OAAOoB,IAAIC,UAEzCrB,OAAOsB,QAAUf,IAJjBP,OAAOS,UAAYD,QAAQD,sCAMlBD,UAAWT,WAAYC,SAExCD,WASXgC,GAAG7B,YACKH,WAACA,WAADC,KAAaA,MAAQG,KAAKL,qBACzBM,aAAaC,oBAAoBH,OAAOI,KAAM,MAAMC,SAAQ,CAACC,UAAWC,OACzET,KAAOS,IAAIuB,MAAM,KAAKC,SAAS/B,OAAOsB,wCAClBhB,UAAWT,WAAYC,SAExCD,WASXgB,iBAAiBb,cACU,UAAhBA,OAAOW,KAAmBV,KAAKC,aAAa8B,KAAKC,SAASC,UAAUlC,OAAOI,MAAMkB,MAAQtB,OAAOsB,MAS3GR,cAAcd,eACoG,IAAvGC,KAAKC,aAAa8B,KAAKG,iBAAiB,8BAAgCnC,OAAOI,KAAO,MAAMgC,OAQvGC,YAAYL,iLACH9B,aAAe8B"} \ No newline at end of file diff --git a/lib/form/amd/src/form.js b/lib/form/amd/src/form.js new file mode 100644 index 0000000000000..084c803ebfd63 --- /dev/null +++ b/lib/form/amd/src/form.js @@ -0,0 +1,443 @@ +// 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 . + +/** + * This file contains JS functionality required by mforms and is included automatically + * when required. + * + * @see /lib/formslib.php#L2548 Candidate for removal, depends on grouped rules. + * @see /lib/amd/src/showhidesettings.js Candidate for removal. + * + * @module core_form/form + * @copyright 2024 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import * as FormChangeChecker from './changechecker'; +import * as Submit from './submit'; +import Rules from './form/rules'; +import {mapTemplate} from './form/display'; +import * as MutateDom from './form/display'; +import Pending from 'core/pending'; + +/** + * @var {Map} M.form2 Global map of forms currently on the page. + */ +M.form2 = M.form2 || new Map(); + +export default class Form { + /** + * @var {HTMLFormElement} form Our very own form to work on. + */ + form; + + /** + * @var {Map} dependencies Our map of form dependencies. + * @See this.getDependencyMapper() for structure info. + */ + dependencies; + + /** + * @var {Map} editors Our map of form editors used get the right selector. + */ + editors; + + /** + * @var {Array} initialDisabledHidden An array of element names that were hidden or disabled by default. + */ + initialDisabledHidden = []; + + /** + * Create a new Form instance. + * + * @param {String} formID The ID of the form to be managed. + * @param {Object} dependencies The passed object of form dependencies. + */ + constructor(formID, dependencies) { + // Handle constructing the dependency map, finding editors and init the rules. + const pendingPromise = new Pending('construction'); + this.form = document.querySelector(`#${formID}`); + this.dependencies = this.getDependencyMapper(dependencies); + this.editors = this.findEditors(); + this.rules = new Rules(this); + + // Apply the initial state of the form. + this.applyInitialState(); + + // Handle mutations within the form. + this.registerEventListeners(); + FormChangeChecker.watchForm(this.form); + pendingPromise.resolve(); + } + + /** + * On page load, apply the initial state of the form by checking the shown items and running their rules. + * We also want to confirm if anything has been hidden or disabled by a PHP callback on load and respect + * their wishes on page load. + */ + applyInitialState() { + // Find any elements that are hidden or disabled by a PHP callback rule. + [...this.form.elements].forEach((element) => { + if ((element.disabled || element.hidden) && element.name !== '') { + this.initialDisabledHidden.push(element.name); + } + }); + // Run through the form elements looking for anything to run rules against on load. + const map = this.generateDisplayMap(); + this.domDispatch(map, true); + } + + /** + * Somewhere out there, we have a form that wants to ensure the state of the form reflects their changes. + */ + formUpdatedExternally() { + const map = this.generateDisplayMap(); + this.domDispatch(map); + + if (map.get('show') !== undefined) { + const opened = this.elementNamesToDomNodes(map.get('show')); + const filterNullNodes = opened.filter((node) => node !== null); + + filterNullNodes.forEach((node) => { + if (this.dependencies.has(node.name)) { + const secondLvlResult = this.displayMapPrune(this.dispatchDependencyRules(node)); + this.domDispatch(secondLvlResult); + } + }); + } + } + + /** + * Helper to iterate all the form elements that have a rule associated with them and generate a display map. + * + * @returns {Map} + */ + generateDisplayMap() { + const map = mapTemplate(); + [...this.form.elements].forEach((element) => { + if (this.dependencies.has(element.name)) { + const elDisplayMap = this.displayMapPrune(this.dispatchDependencyRules(element)); + for (const [key, value] of elDisplayMap) { + map.get(key).push(...value); + } + } + }); + return map; + } + + /** + * Add event listeners to the form. + */ + registerEventListeners() { + // TODO: Choice dropdown does not trigger here. + this.form.addEventListener('change', async(e) => { + if (e.target.type === 'submit') { + FormChangeChecker.resetFormDirtyState(this.form); + Submit.init(e.target.id); + } + if (e.target.type === 'reset') { + FormChangeChecker.resetFormDirtyState(this.form); + this.form.reset(); + } + // Something changes based on this element. + if (this.dependencies.has(e.target.name)) { + FormChangeChecker.markFormChangedFromNode(e.target); + const pendingPromise = new Pending('update'); + const results = this.displayMapPrune(this.dispatchDependencyRules(e.target)); + await this.domDispatch(results); + + // Given that we are showing something, + // we'll do a second order check to see if we need to show more based on the new state of the form. + if (results.get('show') !== undefined) { + const opened = this.elementNamesToDomNodes(results.get('show')); + const filterNullNodes = opened.filter((node) => node !== null); + + filterNullNodes.forEach((node) => { + const pendingPromise = new Pending('updatesecond'); + // RadioNodeList needs to be iterated through as it is not a simple element. + if (node instanceof RadioNodeList) { + node.forEach((n) => { + if (this.dependencies.has(n.name)) { + const secondLvlResult = this.displayMapPrune(this.dispatchDependencyRules(n)); + this.domDispatch(secondLvlResult); + } + }); + } + if (this.dependencies.has(node.name)) { + const secondLvlResult = this.displayMapPrune(this.dispatchDependencyRules(node)); + this.domDispatch(secondLvlResult); + } + pendingPromise.resolve(); + }); + } + pendingPromise.resolve(); + } + }); + } + + /** + * Dispatch the dependency rules to the appropriate rule handler and get back a map of display options. + * + * @param {HTMLFormElement} target The name associated to the element that has changed. + * @returns {Map>} Actions to be taken along with element names that should be affected. + */ + dispatchDependencyRules(target) { + const displayMap = mapTemplate(); + this.dependencies.get(target.name).forEach((dependants, ruleName) => { + // If the rule exists, use it, otherwise fallback to 'neq' which seems to be the "default" rule originally. + const elNamesMap = this.rules[ruleName] ? this.rules[ruleName](target) : this.rules.neq(target); + // Merge the current rule map with the final display map. + elNamesMap.forEach((nodeNames, displayOption) => { + // We want to merge in the new array values into the existing array otherwise, + // we would get an array of arrays which is needless complexity. + displayMap.set(displayOption, [...displayMap.get(displayOption), ...nodeNames.values()].flat()); + }); + }); + return displayMap; + } + + /** + * By default, the full display map contains empty entries and potential duplicated DOM node names. + * + * First: We review the unlock array for node names that have to be hidden. If a match is found, + * the node name will be removed from being unlocked. + * Then: We review the show array for node names that have to be hidden. If a match is found, + * the node name will be removed from being shown as a rule has specified this should actually be hidden. + * Finally: We get rid of any empty entries within the display map to prevent running pointless display updates. + * + * @param {Map} displayMap Map of elements and their associated rules to prune. + * @returns {Map|Map<>} The pruned map or map even a fully pruned map if noting has to change. + */ + displayMapPrune(displayMap) { + // Filter any unlocked items that pegged to be hidden as they must be locked if they are hidden. + // Using something like !displayMap.get('hide').toString().includes(x.toString()) did not work as + // it could result in false positives such as contentfoobar includes content when doing the eval. + if (displayMap.get('unlock') !== undefined) { + if (displayMap.get('hide') !== undefined) { + const hideEvenIfUnlocked = displayMap.get('unlock').filter(x => { + return displayMap.get('hide').indexOf(x.toString()) === -1; + }); + displayMap.set('unlock', hideEvenIfUnlocked); + } + if (displayMap.get('lock') !== undefined) { + const lockEvenIfUnlocked = displayMap.get('unlock').filter(x => { + return displayMap.get('lock').indexOf(x.toString()) === -1; + }); + displayMap.set('unlock', lockEvenIfUnlocked); + } + } + + // Filter any shown items that pegged to be hidden. + if (displayMap.get('show') !== undefined && displayMap.get('hide') !== undefined) { + const hideEvenIfShown = displayMap.get('show').filter(x => { + return displayMap.get('hide').indexOf(x.toString()) === -1; + }); + displayMap.set('show', hideEvenIfShown); + } + + // Remove any empty entries. + for (const [key, value] of displayMap) { + if (value.length === 0) { + displayMap.delete(key); + } + } + return displayMap; + } + + /** + * For a given element, get the names of DOM nodes that can change based on the given rule type name. + * + * @param {String} elementName The name of the element to get the dependants for. + * @param {String} ruleName The rule type to get the dependants for. + * @returns {Map|[]} Either the rule comparison value with associated node names to update or an empty array. + */ + getDependantsOfType(elementName, ruleName) { + return this.dependencies.get(elementName) !== 'undefined' ? this.dependencies.get(elementName).get(ruleName) ?? [] : []; + } + + /** + * Dispatch the DOM manipulation to the appropriate function. + * + * @param {Map} elNamesMap What needs to change. + * @param {Boolean|Null} firstRun Whether this is the first run of the form if so, + * filter some elements based on PHP rule callback values. + */ + domDispatch(elNamesMap, firstRun = false) { + elNamesMap = this.displayMapPrune(elNamesMap); + // Go through the pruned display map and perform the requested display action. + elNamesMap.forEach((elements, domUpdateOpt) => { + // The requested display function somehow does not exist. + if (!MutateDom[domUpdateOpt]) { + return; + } + // If something was hidden or disabled by default via PHP rule callback, we don't want to touch it. + if (firstRun) { + elements = elements.filter((el) => !this.initialDisabledHidden.includes(el)); + } + + // Given the node names to update for a given display action, grab their associated HTMLFormElement and update them. + this.elementNamesToDomNodes(elements).forEach((node) => { + // Ensure we only update form items, instanceof check is a bit too much here as we have RadioNodeList items. + if (node === null) { + return; + } + if (node instanceof RadioNodeList) { + node.forEach((el) => { + MutateDom[domUpdateOpt](el); + }); + } else { + // Given a HTMLFormElement, perform the requested display action. + MutateDom[domUpdateOpt](node); + } + }); + }); + } + + /** + * Convert element names into DOM nodes based on the element name or a compound selector based on the given name. + * + * @param {Array} elementNames The name of dependent elements to get associated DOM nodes. + * @returns {Array} DOM items to perform display actions on. + */ + elementNamesToDomNodes(elementNames) { + return elementNames.map((element) => { + if (this.form.querySelector(`[data-groupname="${element}"]`)) { + return this.form.querySelector(`[data-groupname="${element}"]`); + } + if (this.editors.get(`${element}[text]`)) { + // Text editors are stupid. + return this.form.elements.namedItem(`${element}[text]`); + } else if (!this.form.elements.namedItem(element)) { + // Grouped items are stupid. + return this.form.elements.namedItem(`id_${element}`); + } + // Regular happy plain form item or RadioNodeList. + return this.form.elements.namedItem(element); + }); + } + + /** + * During init, look through the form and identify which elements are editors. + * + * @returns {Map} Map of found editors. + */ + findEditors() { + let found = new Map(); + const fEditors = this.form.querySelectorAll('[data-fieldtype="editor"] textarea'); + Array.from(fEditors).forEach((node) => { + found.set(node.name, true); + }); + return found; + } + + /** + * Convert the dependencies object into a map of elements and their associated rules. + * + * @example + * Note: This is a simplified example of the returned map showing the rules for the grade type element in assign. + * + * "grade[modgrade_type]" => Map { + * "eq" => Map { + * "none" => Object { + * 1 => Array [ + * "advancedgradingmethod_submissions", + * "gradecat", + * "gradepass", + * "completionusegrade", + * "completionusegrade", + * ] + * } + * }, + * "neq" => Map { + * "point" => Object { + * 1 => Array [ + * "grade[modgrade_point]", + * "grade[modgrade_rescalegrades]" + * ] + * }, + * "scale" => Object { + * 1 => Array [ + * "grade[modgrade_scale]" + * ] + * } + * } + * } + * + * Note: If the value of grade[modgrade_type] === "none" then the array of elements defined should be hidden. + * Note: If the value of grade[modgrade_type] !== "point" then the array of elements defined within the following: + * "eq" => "none" && "neq" => "scale" should be hidden. + * + * Note: The object within the "rule" map can contain either 0 or 1 this helps determine if the element should be: + * hidden or locked if the rule is met. + * @See /lib/formslib.php DEP_DISABLE & DEP_HIDE. + * + * @param {Object} dependencies The supplied object of form dependencies to migrate into a map. + * @returns {Map} A map of elements and their associated rules. + */ + getDependencyMapper(dependencies) { + /** + * Convert the object into a first level map. i.e. elementName => ruleType. + * + * @type {Map} The map of rules associated to the given element. + * @example "grade[modgrade_type]" => Map<"eq", "neq"> + */ + const elementMap = new Map(Object.entries(dependencies)); + elementMap.forEach((elementrules, key) => { + /** + * Convert the element rules object into a map. + * + * @type {Map} The map of rules associated to the given element. + * @example "eq" => Map<"none" => Object> + * @example "neq" => Map<"point" => Object, "scale" => Object> + */ + const ruleMap = new Map(Object.entries(elementrules)); + ruleMap.forEach((ruleComparisons, key) => { + /** + * Convert any disabledIf rules into objects, so we can manage them the same as hideIf items. + * + * @type {Map} The map of comparison values. + * @example "none" => "none" => Object + * @example "neq" => "point" => Object + */ + const hideDefine = new Map(Object.entries(ruleComparisons)); + hideDefine.forEach((action, compVal) => { + if (Array.isArray(action)) { + action = {...action}; + } + hideDefine.set(compVal, action); + }); + ruleMap.set(key, hideDefine); + }); + elementMap.set(key, ruleMap); + }); + return elementMap; + } + + /** + * Initialize the form and its dependencies. Also add the Form instance to the global state. + * + * @param {String} formID The ID of the form to be managed. + * @param {Object} dependencies The passed object of form dependencies. + * @returns {Form} An instance associated to a specific form on a given page. + */ + static init(formID, dependencies) { + const instance = new Form(formID, dependencies); + // Add the Form instance to the global state so other modules can access this instance. + M.form2.set(formID, instance); + return instance; + } +} diff --git a/lib/form/amd/src/form/display.js b/lib/form/amd/src/form/display.js new file mode 100644 index 0000000000000..544b3bfc71d13 --- /dev/null +++ b/lib/form/amd/src/form/display.js @@ -0,0 +1,144 @@ +// 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 . + +/** + * This file contains some helper functions to change the visual state of the form elements. + * + * @module core_form/form/dom + * @copyright 2024 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +/** + * A small object that defines the behaviour of the dependency rules for readability. + * + * @type {{hide: number, disable: number}} + */ +const dependencyBehaviour = { + disable: 0, + hide: 1, +}; + +/** + * The archetypal display map that we'll be using to figure out what has to change and how. + * + * @returns {Map} + */ +export const mapTemplate = () => { + return new Map([ + ['hide', []], + ['show', []], + ['lock', []], + ['unlock', []], + ]); +}; + +/** + * Considering the dependant object and if we need to lock it, assign the elements to the correct displayMap key. + * + * @param {Object} dependant The dependant object that contains the rules for hiding and locking. + * @param {Map} displayMap The aggregation of elements that should be shown, hidden, locked, or unlocked. + * @param {Boolean} lock According to the rules, should the element be locked or unlocked. + */ +export const determineDisplayMap = (dependant, displayMap, lock) => { + // Determine via a combination of the lock state & whether the rule is a disable or hide rule, what to do. + const hide = dependant.hasOwnProperty(dependencyBehaviour.hide) ? lock : false; + // This is a disable rule, so we want to always show it and optionally disable it. + if (dependant.hasOwnProperty(dependencyBehaviour.disable)) { + displayMap.get('show').push(dependant[dependencyBehaviour.disable]); + if (lock) { + displayMap.get('lock').push(dependant[dependencyBehaviour.disable]); + } else { + displayMap.get('unlock').push(dependant[dependencyBehaviour.disable]); + } + } + // Conditionally hide the element based on the lock status. + if (dependant.hasOwnProperty(dependencyBehaviour.hide)) { + // Prevent showing an element if it has already been defined hidden. + if (!hide && !displayMap.get('hide').toString().includes(dependant[dependencyBehaviour.hide].toString())) { + displayMap.get('show').push(dependant[dependencyBehaviour.hide]); + } else { + displayMap.get('hide').push(dependant[dependencyBehaviour.hide]); + } + } +}; + +/** + * Disable an element. + * + * @param {HTMLElement} element The element to be disabled. + */ +export const lock = (element) => { + element.setAttribute('disabled', 'disabled'); + if (element.dataset.fieldtype === 'editor' || element.closest('[data-fieldtype="editor"]')) { + element.setAttribute('readonly', 'readonly'); + element.dispatchEvent(new Event('form:editorUpdated')); + } +}; + +/** + * Enable an element. + * + * @param {HTMLElement} element The element to be enabled. + */ +export const unlock = (element) => { + element.removeAttribute('disabled'); + if (element.dataset.fieldtype === 'editor' || element.closest('[data-fieldtype="editor"]')) { + element.removeAttribute('readonly'); + element.dispatchEvent(new Event('form:editorUpdated')); + } +}; + +/** + * Hide an element. + * + * @param {HTMLElement} element The element to be hidden. + */ +export const hide = (element) => { + element.setAttribute('disabled', 'disabled'); + const parent = element.closest('.fitem'); + if (parent) { + parent.setAttribute('hidden', 'hidden'); + parent.classList.add('d-none'); + // Hide the label as well. + const label = document.querySelector('label[for="' + element.id + '"]'); + if (label) { + label.setAttribute('hidden', 'hidden'); + label.classList.add('d-none'); + } + } +}; + +/** + * Show an element. + * + * @param {HTMLElement} element The elements to be shown. + */ +export const show = (element) => { + element.removeAttribute('disabled'); + const parent = element.closest('.fitem'); + if (parent) { + parent.removeAttribute('hidden'); + parent.classList.remove('d-none'); + // Show the label as well. + const label = document.querySelector('label[for="' + element.id + '"]'); + if (label) { + label.removeAttribute('hidden'); + label.classList.remove('d-none'); + } + } +}; diff --git a/lib/form/amd/src/form/rules.js b/lib/form/amd/src/form/rules.js new file mode 100644 index 0000000000000..f387676702e6c --- /dev/null +++ b/lib/form/amd/src/form/rules.js @@ -0,0 +1,189 @@ +// 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 . + +/** + * This file contains a set of rules that elements can be compared against to determine if they should be shown, hidden, etc... + * + * @See /lib/pear/HTML/QuickForm/Rule/Compare.php + * @See https://pear.php.net/manual/en/package.html.html-quickform2.rules.list.php for a list of available rules. + * + * @module core_form/form/rules + * @copyright 2024 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +"use strict"; + +import {determineDisplayMap, mapTemplate} from './display'; + +export default class Rules { + /** + * @var {Form} formInstance The instance of the form class that has a DOM node & references matched. + */ + formInstance; + + /** + * Boilerplate for the common variables. + * @returns {{displayMap: Map, lock: boolean}} + */ + common() { + const displayMap = mapTemplate(); + let lock = false; + return {displayMap, lock}; + } + + /** + * Compare the value of the checkbox vs if it is not checked. + * + * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule. + * @returns {Map} Actions to be taken along with elements that should be affected. + */ + notchecked(target) { + let {displayMap, lock} = this.common(); + this.formInstance.getDependantsOfType(target.name, 'notchecked').forEach((dependant, key) => { + // if (target.disabled) { + // lock = true; + // } else { + // lock = Boolean(key) !== target.checked; + // } + lock = Boolean(key) !== target.checked; + determineDisplayMap(dependant, displayMap, lock); + }); + return displayMap; + } + + /** + * Compare the value of the checkbox and if it is checked. + * + * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule. + * @returns {Map} Actions to be taken along with elements that should be affected. + */ + checked(target) { + let {displayMap, lock} = this.common(); + this.formInstance.getDependantsOfType(target.name, 'checked').forEach((dependant, key) => { + // if (target.disabled) { + // lock = true; + // } else { + // lock = Boolean(key) === target.checked; + // } + lock = Boolean(key) === target.checked; + determineDisplayMap(dependant, displayMap, lock); + }); + return displayMap; + } + + /** + * Compare the value of the changed DOM node equals the rule value. + * + * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule. + * @returns {Map} Actions to be taken along with elements that should be affected. + */ + eq(target) { + let {displayMap, lock} = this.common(); + this.formInstance.getDependantsOfType(target.name, 'eq').forEach((dependant, key) => { + if (target.type === 'radio') { + lock = String(key) === String(this.getRadioFieldVal(target)); + } else if (target.type === 'hidden' && this.getHiddenCkbs(target)) { + // This is the hidden input that is part of an advcheckbox. + lock = target.checked === Boolean(key); + } else if (target.type === 'checkbox' && !target.checked) { + lock = target.checked === Boolean(key); + } else if (target.classList.contains('filepickerhidden')) { + lock = !M.form_filepicker.instances[target.id].fileadded; + } else { + lock = target.value === key; + } + determineDisplayMap(dependant, displayMap, lock); + }); + return displayMap; + } + + /** + * Compare the value of the changed DOM node to the requested rule value. + * @See Moodle has some interesting aliasing ne && noteq, this is also the old "default" rule. + * + * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule. + * @returns {Map} Actions to be taken along with elements that should be affected. + */ + neq(target) { + let {displayMap, lock} = this.common(); + // Get all the aliases of neq and check them all at once. + const maps = new Map([ + ...this.formInstance.getDependantsOfType(target.name, 'neq')?.entries() ?? [], + ...this.formInstance.getDependantsOfType(target.name, 'ne')?.entries() ?? [], + ...this.formInstance.getDependantsOfType(target.name, 'noteq')?.entries() ?? [], + ]); + maps.forEach((dependant, key) => { + if (target.type === 'radio') { + lock = String(key) !== String(this.getRadioFieldVal(target)); + } else if (target.type === 'hidden' && this.getHiddenCkbs(target)) { + // This is the hidden input that is part of an advcheckbox. + lock = target.checked !== Boolean(key); + } else if (target.type === 'checkbox' && !target.checked) { + lock = target.checked === Boolean(key); + } else if (target.classList.contains('filepickerhidden')) { + lock = !!M.form_filepicker.instances[target.id].fileadded; + } else { + lock = target.value !== key; + } + determineDisplayMap(dependant, displayMap, lock); + }); + return displayMap; + } + + /** + * Compare the value of the changed DOM node vs if it is in the defined values passed as a rule. + * + * @param {HTMLFormElement} target The changed DOM node to be compared against the requested rule. + * @returns {Map} Actions to be taken along with elements that should be affected. + */ + in(target) { + let {displayMap, lock} = this.common(); + this.formInstance.getDependantsOfType(target.name, 'in').forEach((dependant, key) => { + lock = key.split('|').includes(target.value); + determineDisplayMap(dependant, displayMap, lock); + }); + return displayMap; + } + + /** + * Radio fields are a bit different, they need to be handled differently. + * + * @param {HTMLFormElement} target The changed DOM node to find a potential radio field for. + * @returns {String} The value of the radio field. + */ + getRadioFieldVal(target) { + return target.type === 'radio' ? this.formInstance.form.elements.namedItem(target.name).value : target.value; + } + + /** + * A small helper to determine if the advcheckboxes are being used. + * + * @param {HTMLElement} target The target element to get the hidden checkboxes for. + * @returns {boolean} Is this a hidden checkbox? + */ + getHiddenCkbs(target) { + return this.formInstance.form.querySelectorAll('input[type=checkbox][name="' + target.name + '"]').length !== 0; + } + + /** + * Constructor for the Rules class. + * + * @param {Form} form The form object that the rules are being applied to. + */ + constructor(form) { + this.formInstance = form; + } +} diff --git a/lib/form/tests/behat/fixtures/form_rules.php b/lib/form/tests/behat/fixtures/form_rules.php new file mode 100644 index 0000000000000..72eb4b0f92adf --- /dev/null +++ b/lib/form/tests/behat/fixtures/form_rules.php @@ -0,0 +1,214 @@ +. + +/** + * Test page for the different form rules. + * + * @copyright 2024 Mathew May + * @package core_form + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../../../config.php'); + +// defined('BEHAT_SITE_RUNNING') || die(); + +global $CFG, $PAGE, $OUTPUT; +require_once($CFG->libdir . '/formslib.php'); +$PAGE->set_context(context_system::instance()); +$PAGE->set_url('/lib/form/tests/behat/fixtures/form_rules.php'); +$PAGE->set_heading('Form rules behaviour test'); +$PAGE->add_body_class('limitedwidth'); + +require_admin(); + +/** + * Class test_form_rules + * @package core_form + */ +class test_form_rules extends moodleform { + /** + * Define the form. + */ + public function definition() { + $mform = $this->_form; + + // Radio rule test. + $mform->addElement('header', 'radioheader', 'Radio: eq/neq'); + $radiogroup = [ + $mform->createElement('radio', 'rgt', '', 'Enable', '1'), + $mform->createElement('radio', 'rgt', '', 'Disable', '2'), + $mform->createElement('radio', 'rgt', '', 'Hide', '3'), + ]; + $mform->addGroup($radiogroup, 'rgt_group', 'Enable/Disable/Hide', ' ', false); + $mform->setDefault('rgt', 1); + + $mform->addElement('button', 'r_eq_btn', 'Radio EQ'); + $mform->disabledIf('r_eq_btn', 'rgt', 'eq', '2'); + $mform->hideIf('r_eq_btn', 'rgt', 'eq', '3'); + + $mform->addElement('button', 'r_neq_btn', 'Radio NEQ'); + $mform->disabledIf('r_neq_btn', 'rgt', 'neq', '2'); + $mform->hideIf('r_neq_btn', 'rgt', 'neq', '3'); + + // Checkboxes checked/notchecked rule test. + $mform->addElement('header', 'checkboxheader', 'Checkboxes: checked/notchecked/eq/neq'); + $mform->setExpanded('checkboxheader'); + $ckb = [ + $mform->createElement('advcheckbox', 'hidden_ckb', 'Checked hide', ''), + $mform->createElement('advcheckbox', 'disabled_ckb', 'Checked disable', ''), + $mform->createElement('advcheckbox', 'hidden_uncheck_ckb', 'Not checked hide', ''), + $mform->createElement('advcheckbox', 'disabled_uncheck_ckb', 'Not checked disable', ''), + $mform->createElement('advcheckbox', 'eq_ckb', 'EQ Checked', ''), + $mform->createElement('advcheckbox', 'neq_ckb', 'NEQ Checked', ''), + ]; + $mform->addGroup($ckb, 'checkbox_test_group', 'Checked/Not checked/EQ/NEQ', ' ', false); + + $mform->addElement('button', 'checked_hidden_btn', 'Checked hidden'); + $mform->hideIf('checked_hidden_btn', 'hidden_ckb', 'checked'); + $mform->addElement('text', 'checked_hidden_txt', 'Label test'); + $mform->setType('checked_hidden_txt', PARAM_TEXT); + $mform->hideIf('checked_hidden_txt', 'hidden_ckb', 'checked'); + + $mform->addElement('button', 'checked_disabled_btn', 'Checked disabled'); + $mform->disabledIf('checked_disabled_btn', 'disabled_ckb', 'checked'); + + $mform->addElement('button', 'unchecked_hidden_btn', 'Not checked hidden'); + $mform->hideIf('unchecked_hidden_btn', 'hidden_uncheck_ckb', 'notchecked'); + + $mform->addElement('button', 'unchecked_disabled_btn', 'Not checked disabled'); + $mform->disabledIf('unchecked_disabled_btn', 'disabled_uncheck_ckb', 'notchecked'); + + $mform->addElement('button', 'eq_ckb_btn', 'EQ ckb 1 disabled'); + $mform->disabledIf('eq_ckb_btn', 'eq_ckb', 'eq' , '1'); + + $mform->addElement('button', 'neq_ckb_btn', 'NEQ ckb 0 hidden'); + $mform->setDefault('neq_ckb', 1); + $mform->hideIf('neq_ckb_btn', 'neq_ckb', 'neq' , '0'); + + // Select test. + $mform->addElement('header', 'selectheader', 'Select: eq/neq'); + $mform->setExpanded('selectheader'); + $mform->addElement('select', 'sct_int', 'Select', + [0 => 'Enable', 1 => 'Disable', 2 => 'Hide'], + /*['multiple' => true]*/ + ); + + $mform->addElement('button', 'sct_eq_btn', 'Select EQ'); + $mform->disabledIf('sct_eq_btn', 'sct_int', 'eq', 1); + $mform->hideIf('sct_eq_btn', 'sct_int', 'eq', 2); + + $mform->addElement('button', 'sct_neq_btn', 'Select NEQ'); + $mform->disabledIf('sct_neq_btn', 'sct_int', 'neq', 1); + $mform->hideIf('sct_neq_btn', 'sct_int', 'neq', 0); + + // Text alpha input rule test. + $mform->addElement('header', 'textalphaheader', 'Text alpha: eq/neq/in'); + $mform->setExpanded('textalphaheader'); + $mform->addElement('text', 'alpha_btn', 'Text input'); + $mform->setType('alpha_btn', PARAM_TEXT); + + $mform->addElement('button', 'tia_eq_btn', 'Alpha EQ'); + $mform->disabledIf('tia_eq_btn', 'alpha_btn', 'eq', 'Disable eq'); + $mform->hideIf('tia_eq_btn', 'alpha_btn', 'eq', 'Hidden eq'); + + $mform->addElement('button', 'tia_neq_btn', 'Alpha NEQ'); + $mform->disabledIf('tia_neq_btn', 'alpha_btn', 'neq', 'Disable neq'); + $mform->hideIf('tia_neq_btn', 'alpha_btn', 'neq', 'Hidden neq'); + + $mform->addElement('button', 'tia_in_btn', 'Alpha IN'); + $mform->disabledIf('tia_in_btn', 'alpha_btn', 'in', ['Tool', 'Rage Against The Machine']); + $mform->hideIf('tia_in_btn', 'alpha_btn', 'in', ['MF Doom', 'USAO / Camellia / No Mana']); + + // Text int input rule test. + $mform->addElement('header', 'textintheader', 'Text int: eq/neq/in'); + $mform->setExpanded('textintheader'); + $mform->addElement('text', 'tii_btn', 'Number input'); + $mform->setType('tii_btn', PARAM_INT); + + $mform->addElement('button', 'tii_eq_btn', 'Int EQ'); + $mform->disabledIf('tii_eq_btn', 'tii_btn', 'eq', 1); + $mform->hideIf('tii_eq_btn', 'tii_btn', 'eq', 2); + + $mform->addElement('button', 'tii_neq_btn', 'Int NEQ'); + $mform->disabledIf('tii_neq_btn', 'tii_btn', 'neq', 3); + $mform->hideIf('tii_neq_btn', 'tii_btn', 'neq', 4); + + $mform->addElement('button', 'tii_in_btn', 'Int IN'); + $mform->disabledIf('tii_in_btn', 'tii_btn', 'in', [9, 10]); + $mform->hideIf('tii_in_btn', 'tii_btn', 'in', [11, 12]); + + // Date selector rule test. + $mform->addElement('header', 'dateselectorheader', 'Date selector: ~'); + $mform->setExpanded('dateselectorheader'); + $mform->addElement('checkbox', 'ds_enb', get_string('enable')); + $mform->setDefault('ds_enb', 1); + $mform->addElement('checkbox', 'ds_dis', get_string('show')); + $mform->setDefault('ds_dis', 1); + $mform->addElement('date_selector', 'ds', 'Date selector for testing'); + $mform->disabledIf("ds", 'ds_enb'); + $mform->hideIf("ds", 'ds_dis'); + + // Editor rule test. + $mform->addElement('header', 'editorheader', 'Editor: ~'); + $mform->setExpanded('editorheader'); + $mform->addElement('checkbox', 'edt_enb', get_string('enable')); + $mform->setDefault('edt_enb', 1); + $mform->addElement('checkbox', 'edt_dis', get_string('show')); + $mform->setDefault('edt_dis', 1); + $editoroptions = [ + 'subdirs' => 0, + 'maxbytes' => 0, + 'maxfiles' => 0, + 'changeformat' => 0, + 'context' => context_system::instance(), + 'noclean' => 0, + 'trusttext' => 0 + ]; + $mform->addElement('editor', 'edt', 'Editor for testing', $editoroptions); + $mform->setDefault('edt', ['text' => 'Hello world!', 'format' => FORMAT_HTML]); + $mform->disabledIf("edt", 'edt_enb'); + $mform->hideIf("edt", 'edt_dis'); + + // Filepicker rule test. + $mform->addElement('header', 'filepickerheader', 'Filepicker: ~'); + $mform->setExpanded('filepickerheader'); + $mform->addElement('checkbox', 'fp_dis', get_string('show')); + $mform->setDefault('fp_dis', 1); + $mform->addElement('filepicker', 'fp', 'Filepicker for testing', null, ['accepted_types' => '*']); + $mform->hideIf("fp", 'fp_dis'); + + $this->add_action_buttons(false, 'Send form'); + } +} + +echo $OUTPUT->header(); + +$form = new test_form_rules(); + +$data = $form->get_data(); +if ($data) { + echo "

Submitted data

"; + echo '
    '; + $data = (array) $data; + foreach ($data as $field => $value) { + echo "
  • $field: $value
  • "; + } + echo '
'; +} +$form->display(); + +echo $OUTPUT->footer(); diff --git a/lib/form/tests/behat/form_rules.feature b/lib/form/tests/behat/form_rules.feature new file mode 100644 index 0000000000000..aee971f8d19f7 --- /dev/null +++ b/lib/form/tests/behat/form_rules.feature @@ -0,0 +1,89 @@ +@core @core_form @core_form-rules +Feature: Test the form rules + For forms that make use of different rules + So that users can use form elements + I need to test the ensure elements are controlled correctly + + Background: + Given I log in as "admin" + And I am on fixture page "/lib/form/tests/behat/fixtures/form_rules.php" + And I wait until the page is ready + + @javascript + Scenario Outline: Checkbox, Date selector, Editor, Filepicker rules rules + Given I should "