From f2e4b56cfa75913cabb5277383238c5025cf1edb Mon Sep 17 00:00:00 2001 From: Mathew May Date: Wed, 17 Jul 2024 12:33:32 +0800 Subject: [PATCH] Initial state/DOM handling + refining rules --- .../page_requirements_manager.php | 2 +- lib/form/amd/build/form.min.js | 2 +- lib/form/amd/build/form.min.js.map | 2 +- lib/form/amd/build/form/dom.min.js | 2 +- lib/form/amd/build/form/dom.min.js.map | 2 +- lib/form/amd/build/form/rules.min.js | 2 +- lib/form/amd/build/form/rules.min.js.map | 2 +- lib/form/amd/src/form.js | 60 ++++++--- lib/form/amd/src/form/dom.js | 97 ++++++------- lib/form/amd/src/form/rules.js | 127 +++++++++++------- lib/form/tests/behat/fixtures/form_rules.php | 37 ++++- lib/formslib.php | 2 +- 12 files changed, 212 insertions(+), 125 deletions(-) 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 index b4270eaae1f52..82a21746e8547 100644 --- a/lib/form/amd/build/form.min.js +++ b/lib/form/amd/build/form.min.js @@ -1,3 +1,3 @@ -define("core_form/form",["exports","./changechecker","./events","./submit","./form/rules","./form/dom"],(function(_exports,FormChangeChecker,FormEvents,Submit,_rules,MutateDom){var 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),FormEvents=_interopRequireWildcard(FormEvents),Submit=_interopRequireWildcard(Submit),_rules=(obj=_rules)&&obj.__esModule?obj:{default:obj},MutateDom=_interopRequireWildcard(MutateDom);class Form{constructor(formID,dependencies){_defineProperty(this,"form",void 0),_defineProperty(this,"dependencies",void 0),this.form=document.querySelector("#".concat(formID)),this.dependencies=this.getDependencyMapper(dependencies),this.rules=new _rules.default(this),this.registerEventListeners(),FormChangeChecker.watchForm(this.form)}registerEventListeners(){this.form.addEventListener("change",(e=>{if("submit"===e.target.type&&(FormEvents.notifyFormSubmittedByJavascript(this.form),FormChangeChecker.resetFormDirtyState(this.form),Submit.init(e.target.id)),"reset"===e.target.type&&this.form.reset(),this.dependencies.has(e.target.name)){const displayMap=this.dispatchDependencyRules(e.target),reducedDisplayMap=this.displayMapPrune(displayMap);this.domDispatch(reducedDisplayMap)}}))}dispatchDependencyRules(target){const displayMap=this.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.get(displayOption).push(...nodeNames)}))})),displayMap}displayMapPrune(displayMap){window.console.log("preprune:",displayMap);const removelockifhidden=displayMap.get("lock").filter((x=>displayMap.get("hide").includes(x)));window.console.log("removelockifhidden:",removelockifhidden),displayMap.set("lock",removelockifhidden);for(const[key,value]of displayMap)0===value.length&&displayMap.delete(key);return displayMap}getDependantsOfType(element,type){var _this$dependencies$ge;return"undefined"!==this.dependencies.get(element)&&null!==(_this$dependencies$ge=this.dependencies.get(element).get(type))&&void 0!==_this$dependencies$ge?_this$dependencies$ge:[]}domDispatch(elNamesMap){window.console.log("elNamesMap",elNamesMap),elNamesMap.forEach(((elements,domUpdateOpt)=>{if(!MutateDom[domUpdateOpt])return;this.elementNamesToDomNodes(elements).forEach((node=>{null!==node&&MutateDom[domUpdateOpt](node)}))}))}elementNamesToDomNodes(elementNames){return elementNames.map((element=>this.form.elements.namedItem(element)))}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)=>{ruleMap.set(key,new Map(Object.entries(ruleComparisons)))})),elementMap.set(key,ruleMap)})),elementMap}mapTemplate(){return new Map([["hide",[]],["show",[]],["lock",[]],["unlock",[]]])}static init(formID,dependencies){return new Form(formID,dependencies)}}return _exports.default=Form,_exports.default})); +define("core_form/form",["exports","./changechecker","./submit","./form/rules","./form/dom"],(function(_exports,FormChangeChecker,Submit,_rules,MutateDom){var 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=(obj=_rules)&&obj.__esModule?obj:{default:obj},MutateDom=_interopRequireWildcard(MutateDom);class Form{constructor(formID,dependencies){_defineProperty(this,"form",void 0),_defineProperty(this,"dependencies",void 0),this.form=document.querySelector("#".concat(formID)),this.dependencies=this.getDependencyMapper(dependencies),this.rules=new _rules.default(this),this.applyInitialState(),this.registerEventListeners(),FormChangeChecker.watchForm(this.form)}applyInitialState(){[...this.form.elements].forEach((element=>{this.dependencies.has(element.name)&&this.domDispatch(this.displayMapPrune(this.dispatchDependencyRules(element)))}))}registerEventListeners(){this.form.addEventListener("change",(e=>{"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),this.domDispatch(this.displayMapPrune(this.dispatchDependencyRules(e.target))))}))}dispatchDependencyRules(target){const displayMap=this.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.get(displayOption).push(...nodeNames)}))})),displayMap}displayMapPrune(displayMap){const removeunlockifhidden=displayMap.get("unlock").filter((x=>!displayMap.get("hide").toString().includes(x.toString())));displayMap.set("unlock",removeunlockifhidden);for(const[key,value]of displayMap)0===value.length&&displayMap.delete(key);return displayMap}getDependantsOfType(element,type){var _this$dependencies$ge;return"undefined"!==this.dependencies.get(element)&&null!==(_this$dependencies$ge=this.dependencies.get(element).get(type))&&void 0!==_this$dependencies$ge?_this$dependencies$ge:[]}domDispatch(elNamesMap){elNamesMap.forEach(((elements,domUpdateOpt)=>{if(!MutateDom[domUpdateOpt])return;this.elementNamesToDomNodes(elements).forEach((node=>{null!==node&&MutateDom[domUpdateOpt](node)}))}))}elementNamesToDomNodes(elementNames){return elementNames.map((element=>element.map((element2=>this.form.elements.namedItem(element2)))))}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}mapTemplate(){return new Map([["hide",[]],["show",[]],["lock",[]],["unlock",[]]])}static init(formID,dependencies){return new Form(formID,dependencies)}}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 index 3cbd0752e884e..a0226b31f88f0 100644 --- a/lib/form/amd/build/form.min.js.map +++ b/lib/form/amd/build/form.min.js.map @@ -1 +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\n// Pure form functionality.\n// import {serialize} from './util';\nimport * as FormChangeChecker from './changechecker';\nimport * as FormEvents from './events';\nimport * as Submit from './submit';\nimport Rules from './form/rules';\nimport * as MutateDom from './form/dom';\n\n// Maybe not needed but noted just in case.\n// import * as CollapseSections from './collapsesections'; // This is included via ../collapsesections.mustache\n// import * as EncryptedPassword from './encryptedpassword'; // This is included via ../setting_encryptedpassword.mustache\n// import * as FileTypes from './filetypes'; // This is included via /lib/form/filetypes.php#L152\n// import * as PasswordUnmask from './passwordunmask'; // This is included via ../element-passwordunmask.mustache\n// import * as ShowAdvanced from './showadvanced'; // This is included via /lib/formslib.php#L3349\n\n// Custom element types, Likely not needed.\n// import * as ChoiceDropdown from 'core_form/choicedropdown'; // This is included via ../choicedropdown.mustache\n// import * as ConfigText_maxlength from 'core_form/configtext_maxlength'; // This is included via lib/adminlib.php#L2635\n// import * as DefaultCustom from 'core_form/defaultcustom'; // This is included via lib/form/defaultcustom.php#L253\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 */\n dependencies;\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 // Set class properties.\n this.form = document.querySelector(`#${formID}`);\n this.dependencies = this.getDependencyMapper(dependencies);\n this.rules = new Rules(this);\n\n // Handle mutations within the form.\n this.registerEventListeners();\n FormChangeChecker.watchForm(this.form);\n // TODO: Apply the rules to the form on load.\n }\n\n /**\n * Add event listeners to the form.\n */\n registerEventListeners() {\n this.form.addEventListener('change', (e) => {\n if (e.target.type === 'submit') {\n // TODO: Dummy calls so import list looks better...\n // Notify listeners that the form is about to be submitted.\n FormEvents.notifyFormSubmittedByJavascript(this.form);\n FormChangeChecker.resetFormDirtyState(this.form);\n Submit.init(e.target.id);\n }\n if (e.target.type === 'reset') {\n this.form.reset();\n }\n // Something changes based on this element.\n if (this.dependencies.has(e.target.name)) {\n const displayMap = this.dispatchDependencyRules(e.target);\n const reducedDisplayMap = this.displayMapPrune(displayMap);\n this.domDispatch(reducedDisplayMap);\n }\n });\n }\n\n /**\n * Dispatch the dependency rules to the appropriate rule handler.\n *\n * @param {HTMLFormElement} target The name associated to the element that has changed.\n * @returns {Map} Actions to be taken along with elements that should be affected.\n */\n dispatchDependencyRules(target) {\n const displayMap = this.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 displayMap.get(displayOption).push(...nodeNames);\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 * Here we will get rid of any empty entries and review the locked array. If a node name exists in both\n * the locked and hidden array, it should be removed from the locked array as nodes are locked / disabled when hidden.\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 window.console.log('preprune:', displayMap);\n // Filter any locked items that are to be hidden anyway.\n const removelockifhidden = displayMap.get('lock').filter(x => displayMap.get('hide').includes(x));\n window.console.log('removelockifhidden:', removelockifhidden);\n displayMap.set('lock', removelockifhidden);\n // Filter any unlocked items that pegged to be hidden as they must be locked if they are hidden.\n //const removeunlockifhidden = displayMap.get('unlock').filter(x => displayMap.get('hide').includes(x));\n //displayMap.set('unlock', removeunlockifhidden);\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} element The name of the element to get the dependants for.\n * @param {String} type The rule type to get the dependants for.\n * @returns {Array|[]}\n */\n getDependantsOfType(element, type) {\n return this.dependencies.get(element) !== 'undefined' ? this.dependencies.get(element).get(type) ?? [] : [];\n }\n\n /**\n * Dispatch the DOM manipulation to the appropriate function.\n *\n * @param {Map} elNamesMap What needs to change.\n */\n domDispatch(elNamesMap) {\n window.console.log('elNamesMap', elNamesMap);\n elNamesMap.forEach((elements, domUpdateOpt) => {\n if (!MutateDom[domUpdateOpt]) {\n return;\n }\n const nodes = this.elementNamesToDomNodes(elements);\n nodes.forEach((node) => {\n if (node === null) {\n return;\n }\n MutateDom[domUpdateOpt](node);\n });\n });\n }\n\n /**\n * Convert the element names into DOM nodes based on the element names.\n *\n * @param {Array} elementNames The name of dependent elements to get associated DOM nodes.\n * @returns {Array}\n */\n elementNamesToDomNodes(elementNames) {\n return elementNames.map((element) => {\n return this.form.elements.namedItem(element);\n });\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 ruleMap.set(key, new Map(Object.entries(ruleComparisons)));\n });\n elementMap.set(key, ruleMap);\n });\n return elementMap;\n }\n\n /**\n * A standard map that we'll be using to figure out what has to change and how.\n *\n * @returns {Map}\n */\n mapTemplate() {\n return new Map([\n ['hide', []],\n ['show', []],\n ['lock', []],\n ['unlock', []],\n ]);\n }\n\n /**\n * Initialize the form and its dependencies.\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 return new Form(formID, dependencies);\n }\n}\n"],"names":["Form","constructor","formID","dependencies","form","document","querySelector","this","getDependencyMapper","rules","Rules","registerEventListeners","FormChangeChecker","watchForm","addEventListener","e","target","type","FormEvents","notifyFormSubmittedByJavascript","resetFormDirtyState","Submit","init","id","reset","has","name","displayMap","dispatchDependencyRules","reducedDisplayMap","displayMapPrune","domDispatch","mapTemplate","get","forEach","dependants","ruleName","neq","nodeNames","displayOption","push","window","console","log","removelockifhidden","filter","x","includes","set","key","value","length","delete","getDependantsOfType","element","elNamesMap","elements","domUpdateOpt","MutateDom","elementNamesToDomNodes","node","elementNames","map","namedItem","elementMap","Map","Object","entries","elementrules","ruleMap","ruleComparisons"],"mappings":"gmDAiDqBA,KAiBjBC,YAAYC,OAAQC,mGAEXC,KAAOC,SAASC,yBAAkBJ,cAClCC,aAAeI,KAAKC,oBAAoBL,mBACxCM,MAAQ,IAAIC,eAAMH,WAGlBI,yBACLC,kBAAkBC,UAAUN,KAAKH,MAOrCO,8BACSP,KAAKU,iBAAiB,UAAWC,OACZ,WAAlBA,EAAEC,OAAOC,OAGTC,WAAWC,gCAAgCZ,KAAKH,MAChDQ,kBAAkBQ,oBAAoBb,KAAKH,MAC3CiB,OAAOC,KAAKP,EAAEC,OAAOO,KAEH,UAAlBR,EAAEC,OAAOC,WACJb,KAAKoB,QAGVjB,KAAKJ,aAAasB,IAAIV,EAAEC,OAAOU,MAAO,OAChCC,WAAapB,KAAKqB,wBAAwBb,EAAEC,QAC5Ca,kBAAoBtB,KAAKuB,gBAAgBH,iBAC1CI,YAAYF,uBAW7BD,wBAAwBZ,cACdW,WAAapB,KAAKyB,0BACnB7B,aAAa8B,IAAIjB,OAAOU,MAAMQ,SAAQ,CAACC,WAAYC,aAEjC7B,KAAKE,MAAM2B,UAAY7B,KAAKE,MAAM2B,UAAUpB,QAAUT,KAAKE,MAAM4B,IAAIrB,SAE7EkB,SAAQ,CAACI,UAAWC,iBAC3BZ,WAAWM,IAAIM,eAAeC,QAAQF,iBAGvCX,WAWXG,gBAAgBH,YACZc,OAAOC,QAAQC,IAAI,YAAahB,kBAE1BiB,mBAAqBjB,WAAWM,IAAI,QAAQY,QAAOC,GAAKnB,WAAWM,IAAI,QAAQc,SAASD,KAC9FL,OAAOC,QAAQC,IAAI,sBAAuBC,oBAC1CjB,WAAWqB,IAAI,OAAQJ,wBAMlB,MAAOK,IAAKC,SAAUvB,WACF,IAAjBuB,MAAMC,QACNxB,WAAWyB,OAAOH,YAGnBtB,WAUX0B,oBAAoBC,QAASrC,sCACiB,cAAnCV,KAAKJ,aAAa8B,IAAIqB,wCAA2B/C,KAAKJ,aAAa8B,IAAIqB,SAASrB,IAAIhB,6DAAc,GAQ7Gc,YAAYwB,YACRd,OAAOC,QAAQC,IAAI,aAAcY,YACjCA,WAAWrB,SAAQ,CAACsB,SAAUC,oBACrBC,UAAUD,qBAGDlD,KAAKoD,uBAAuBH,UACpCtB,SAAS0B,OACE,OAATA,MAGJF,UAAUD,cAAcG,YAWpCD,uBAAuBE,qBACZA,aAAaC,KAAKR,SACd/C,KAAKH,KAAKoD,SAASO,UAAUT,WAgD5C9C,oBAAoBL,oBAOV6D,WAAa,IAAIC,IAAIC,OAAOC,QAAQhE,sBAC1C6D,WAAW9B,SAAQ,CAACkC,aAAcnB,aAQxBoB,QAAU,IAAIJ,IAAIC,OAAOC,QAAQC,eACvCC,QAAQnC,SAAQ,CAACoC,gBAAiBrB,OAC9BoB,QAAQrB,IAAIC,IAAK,IAAIgB,IAAIC,OAAOC,QAAQG,sBAE5CN,WAAWhB,IAAIC,IAAKoB,YAEjBL,WAQXhC,qBACW,IAAIiC,IAAI,CACX,CAAC,OAAQ,IACT,CAAC,OAAQ,IACT,CAAC,OAAQ,IACT,CAAC,SAAU,kBAWP/D,OAAQC,qBACT,IAAIH,KAAKE,OAAQC"} \ No newline at end of file +{"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\n// Pure form functionality.\n// import {serialize} from './util';\nimport * as FormChangeChecker from './changechecker';\nimport * as Submit from './submit';\nimport Rules from './form/rules';\nimport * as MutateDom from './form/dom';\n\n// Maybe not needed but noted just in case.\n// import * as CollapseSections from './collapsesections'; // This is included via ../collapsesections.mustache\n// import * as EncryptedPassword from './encryptedpassword'; // This is included via ../setting_encryptedpassword.mustache\n// import * as FileTypes from './filetypes'; // This is included via /lib/form/filetypes.php#L152\n// import * as PasswordUnmask from './passwordunmask'; // This is included via ../element-passwordunmask.mustache\n// import * as ShowAdvanced from './showadvanced'; // This is included via /lib/formslib.php#L3349\n\n// Custom element types, Likely not needed.\n// import * as ChoiceDropdown from 'core_form/choicedropdown'; // This is included via ../choicedropdown.mustache\n// import * as ConfigText_maxlength from 'core_form/configtext_maxlength'; // This is included via lib/adminlib.php#L2635\n// import * as DefaultCustom from 'core_form/defaultcustom'; // This is included via lib/form/defaultcustom.php#L253\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 */\n dependencies;\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 // Set class properties.\n this.form = document.querySelector(`#${formID}`);\n this.dependencies = this.getDependencyMapper(dependencies);\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 }\n\n /**\n * Given the page has loaded, apply the initial state of the form.\n */\n applyInitialState() {\n [...this.form.elements].forEach((element) => {\n if (this.dependencies.has(element.name)) {\n this.domDispatch(this.displayMapPrune(this.dispatchDependencyRules(element)));\n }\n });\n }\n\n /**\n * Add event listeners to the form.\n */\n registerEventListeners() {\n this.form.addEventListener('change', (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 this.domDispatch(this.displayMapPrune(this.dispatchDependencyRules(e.target)));\n }\n });\n }\n\n /**\n * Dispatch the dependency rules to the appropriate rule handler.\n *\n * @param {HTMLFormElement} target The name associated to the element that has changed.\n * @returns {Map} Actions to be taken along with elements that should be affected.\n */\n dispatchDependencyRules(target) {\n const displayMap = this.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 displayMap.get(displayOption).push(...nodeNames);\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 * Here we will get rid of any empty entries and review the locked array. If a node name exists in both\n * the locked and hidden array, it should be removed from the locked array as nodes are locked / disabled when hidden.\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 const removeunlockifhidden = displayMap.get('unlock').filter(x => {\n return !displayMap.get('hide').toString().includes(x.toString());\n });\n displayMap.set('unlock', removeunlockifhidden);\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} element The name of the element to get the dependants for.\n * @param {String} type The rule type to get the dependants for.\n * @returns {Array|[]}\n */\n getDependantsOfType(element, type) {\n return this.dependencies.get(element) !== 'undefined' ? this.dependencies.get(element).get(type) ?? [] : [];\n }\n\n /**\n * Dispatch the DOM manipulation to the appropriate function.\n *\n * @param {Map} elNamesMap What needs to change.\n */\n domDispatch(elNamesMap) {\n elNamesMap.forEach((elements, domUpdateOpt) => {\n if (!MutateDom[domUpdateOpt]) {\n return;\n }\n const nodes = this.elementNamesToDomNodes(elements);\n nodes.forEach((node) => {\n if (node === null) {\n return;\n }\n MutateDom[domUpdateOpt](node);\n });\n });\n }\n\n /**\n * Convert the element names into DOM nodes based on the element names.\n *\n * @param {Array} elementNames The name of dependent elements to get associated DOM nodes.\n * @returns {Array}\n */\n elementNamesToDomNodes(elementNames) {\n return elementNames.map((element) => {\n // TODO: Rename and document why we need to deep look if there are multiple nodes effected by the rule.\n return element.map((element2) => {\n return this.form.elements.namedItem(element2);\n });\n });\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 t.\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 * A standard map that we'll be using to figure out what has to change and how.\n *\n * @returns {Map}\n */\n mapTemplate() {\n return new Map([\n ['hide', []],\n ['show', []],\n ['lock', []],\n ['unlock', []],\n ]);\n }\n\n /**\n * Initialize the form and its dependencies.\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 return new Form(formID, dependencies);\n }\n}\n"],"names":["Form","constructor","formID","dependencies","form","document","querySelector","this","getDependencyMapper","rules","Rules","applyInitialState","registerEventListeners","FormChangeChecker","watchForm","elements","forEach","element","has","name","domDispatch","displayMapPrune","dispatchDependencyRules","addEventListener","e","target","type","resetFormDirtyState","Submit","init","id","reset","markFormChangedFromNode","displayMap","mapTemplate","get","dependants","ruleName","neq","nodeNames","displayOption","push","removeunlockifhidden","filter","x","toString","includes","set","key","value","length","delete","getDependantsOfType","elNamesMap","domUpdateOpt","MutateDom","elementNamesToDomNodes","node","elementNames","map","element2","namedItem","elementMap","Map","Object","entries","elementrules","ruleMap","ruleComparisons","hideDefine","action","compVal","Array","isArray"],"mappings":"2hDAgDqBA,KAiBjBC,YAAYC,OAAQC,mGAEXC,KAAOC,SAASC,yBAAkBJ,cAClCC,aAAeI,KAAKC,oBAAoBL,mBACxCM,MAAQ,IAAIC,eAAMH,WAGlBI,yBAGAC,yBACLC,kBAAkBC,UAAUP,KAAKH,MAMrCO,wBACQJ,KAAKH,KAAKW,UAAUC,SAASC,UACzBV,KAAKJ,aAAae,IAAID,QAAQE,YACzBC,YAAYb,KAAKc,gBAAgBd,KAAKe,wBAAwBL,cAQ/EL,8BACSR,KAAKmB,iBAAiB,UAAWC,IACZ,WAAlBA,EAAEC,OAAOC,OACTb,kBAAkBc,oBAAoBpB,KAAKH,MAC3CwB,OAAOC,KAAKL,EAAEC,OAAOK,KAEH,UAAlBN,EAAEC,OAAOC,OACTb,kBAAkBc,oBAAoBpB,KAAKH,WACtCA,KAAK2B,SAGVxB,KAAKJ,aAAae,IAAIM,EAAEC,OAAON,QAC/BN,kBAAkBmB,wBAAwBR,EAAEC,aACvCL,YAAYb,KAAKc,gBAAgBd,KAAKe,wBAAwBE,EAAEC,cAWjFH,wBAAwBG,cACdQ,WAAa1B,KAAK2B,0BACnB/B,aAAagC,IAAIV,OAAON,MAAMH,SAAQ,CAACoB,WAAYC,aAEjC9B,KAAKE,MAAM4B,UAAY9B,KAAKE,MAAM4B,UAAUZ,QAAUlB,KAAKE,MAAM6B,IAAIb,SAE7ET,SAAQ,CAACuB,UAAWC,iBAC3BP,WAAWE,IAAIK,eAAeC,QAAQF,iBAGvCN,WAWXZ,gBAAgBY,kBAENS,qBAAuBT,WAAWE,IAAI,UAAUQ,QAAOC,IACjDX,WAAWE,IAAI,QAAQU,WAAWC,SAASF,EAAEC,cAEzDZ,WAAWc,IAAI,SAAUL,0BAGpB,MAAOM,IAAKC,SAAUhB,WACF,IAAjBgB,MAAMC,QACNjB,WAAWkB,OAAOH,YAGnBf,WAUXmB,oBAAoBnC,QAASS,sCACiB,cAAnCnB,KAAKJ,aAAagC,IAAIlB,wCAA2BV,KAAKJ,aAAagC,IAAIlB,SAASkB,IAAIT,6DAAc,GAQ7GN,YAAYiC,YACRA,WAAWrC,SAAQ,CAACD,SAAUuC,oBACrBC,UAAUD,qBAGD/C,KAAKiD,uBAAuBzC,UACpCC,SAASyC,OACE,OAATA,MAGJF,UAAUD,cAAcG,YAWpCD,uBAAuBE,qBACZA,aAAaC,KAAK1C,SAEdA,QAAQ0C,KAAKC,UACTrD,KAAKH,KAAKW,SAAS8C,UAAUD,cAiDhDpD,oBAAoBL,oBAOV2D,WAAa,IAAIC,IAAIC,OAAOC,QAAQ9D,sBAC1C2D,WAAW9C,SAAQ,CAACkD,aAAclB,aAQxBmB,QAAU,IAAIJ,IAAIC,OAAOC,QAAQC,eACvCC,QAAQnD,SAAQ,CAACoD,gBAAiBpB,aAQxBqB,WAAa,IAAIN,IAAIC,OAAOC,QAAQG,kBAC1CC,WAAWrD,SAAQ,CAACsD,OAAQC,WACpBC,MAAMC,QAAQH,UACdA,OAAS,IAAIA,SAEjBD,WAAWtB,IAAIwB,QAASD,WAE5BH,QAAQpB,IAAIC,IAAKqB,eAErBP,WAAWf,IAAIC,IAAKmB,YAEjBL,WAQX5B,qBACW,IAAI6B,IAAI,CACX,CAAC,OAAQ,IACT,CAAC,OAAQ,IACT,CAAC,OAAQ,IACT,CAAC,SAAU,kBAWP7D,OAAQC,qBACT,IAAIH,KAAKE,OAAQC"} \ No newline at end of file diff --git a/lib/form/amd/build/form/dom.min.js b/lib/form/amd/build/form/dom.min.js index 431f307508586..7c22a1db52025 100644 --- a/lib/form/amd/build/form/dom.min.js +++ b/lib/form/amd/build/form/dom.min.js @@ -1,3 +1,3 @@ -define("core_form/form/dom",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unlock=_exports.show=_exports.lock=_exports.hide=void 0;const lock=element=>{};_exports.lock=lock;_exports.unlock=element=>{show(element)};_exports.hide=element=>{};const show=element=>{};_exports.show=show})); +define("core_form/form/dom",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.unlock=_exports.show=_exports.lock=_exports.hide=void 0;_exports.lock=elements=>{elements.forEach((element=>{null!==element&&element.setAttribute("disabled","disabled")}))};_exports.unlock=elements=>{elements.forEach((element=>{null!==element&&element.removeAttribute("disabled")}))};_exports.hide=elements=>{elements.forEach((element=>{if(null!==element){const parent=element.closest("[data-groupname]")?element.closest("[data-groupname]"):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=elements=>{elements.forEach((element=>{if(null!==element){const parent=element.closest("[data-groupname]")?element.closest("[data-groupname]"):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=dom.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/form/dom.min.js.map b/lib/form/amd/build/form/dom.min.js.map index 65fa46cd79c61..a1e3793d292c1 100644 --- a/lib/form/amd/build/form/dom.min.js.map +++ b/lib/form/amd/build/form/dom.min.js.map @@ -1 +1 @@ -{"version":3,"file":"dom.min.js","sources":["../../src/form/dom.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 * TODO: Considerations requiring action:\n *\n * Things old YUI looked for after grabbing the element DOM ancestor (Label management):\n * Classes: fitem fitem_fgroup\n * Data attributes: label[for=\"' + id + '\"]\n * Selectors from old YUI:\n * .fitem [data-fieldtype=\"filepicker\"] input,\n * .fitem [data-fieldtype=\"filemanager\"] input,\n * .fitem [data-fieldtype=\"group\"] input[id*=\"filemanager\"]\n *\n * Guessed selectors:\n * [data-groupname=\"${element}\"]\n *\n * Other things to consider:\n * Special handling for editors & filepickers i.e. Name suffix on editors -> See old YUI?\n * element.setAttribute('aria-disabled', >);\n * element.setAttribute('readonly', 'readonly');\n */\n\n/**\n * Disable an element.\n *\n * @param {HTMLElement} element The element to be disabled.\n */\n// eslint-disable-next-line no-unused-vars\nexport const lock = (element) => {\n // window.console.log('Lock node:', element);\n // element.disabled = true;\n // element.classList.add('disabled');\n};\n\n/**\n * Enable an element.\n *\n * @param {HTMLElement} element The element to be enabled.\n */\nexport const unlock = (element) => {\n show(element);\n // window.console.log('Unlock node:', element);\n // element.disabled = false;\n // element.classList.remove('disabled');\n};\n\n/**\n * Hide an element.\n *\n * @param {HTMLElement} element The element to be hidden.\n */\nexport const hide = (element) => {\n // Lock the element to prevent interaction.\n lock(element);\n // window.console.log('Hide node:', element);\n // element.hidden = true;\n // element.classList.add('hidden');\n};\n\n/**\n * Show an element.\n *\n * @param {HTMLElement} element The element to be shown.\n */\n// eslint-disable-next-line no-unused-vars\nexport const show = (element) => {\n //window.console.log('Show node:', element);\n // element.hidden = false;\n // element.classList.remove('hidden');\n};\n"],"names":["lock","element","show"],"mappings":"2LAmDaA,KAAQC,+CAWEA,UACnBC,KAAKD,wBAWYA,kBAcRC,KAAQD"} \ No newline at end of file +{"version":3,"file":"dom.min.js","sources":["../../src/form/dom.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 * Disable an element.\n *\n * @param {Array} elements The element to be disabled.\n */\nexport const lock = (elements) => {\n elements.forEach(element => {\n if (element !== null) {\n element.setAttribute('disabled', 'disabled');\n }\n });\n};\n\n/**\n * Enable an element.\n *\n * @param {Array} elements The element to be enabled.\n */\nexport const unlock = (elements) => {\n elements.forEach(element => {\n if (element !== null) {\n element.removeAttribute('disabled');\n }\n });\n};\n\n/**\n * Hide an element.\n *\n * @param {Array} elements The elements to be hidden.\n */\nexport const hide = (elements) => {\n elements.forEach(element => {\n if (element !== null) {\n // Check if the element is part of a group otherwise, just grab the nearest wrapping element.\n const parent = element.closest('[data-groupname]') ? element.closest('[data-groupname]') : element.closest('.fitem');\n if (parent) {\n parent.setAttribute('hidden', 'hidden');\n parent.classList.add('d-none');\n\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\n/**\n * Show an element.\n *\n * @param {Array} elements The elements to be shown.\n */\nexport const show = (elements) => {\n elements.forEach(element => {\n if (element !== null) {\n // Check if the element is part of a group otherwise, just grab the nearest wrapping element.\n const parent = element.closest('[data-groupname]') ? element.closest('[data-groupname]') : element.closest('.fitem');\n if (parent) {\n parent.removeAttribute('hidden');\n parent.classList.remove('d-none');\n\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 });\n};\n"],"names":["elements","forEach","element","setAttribute","removeAttribute","parent","closest","classList","add","label","document","querySelector","id","remove"],"mappings":"mMA8BqBA,WACjBA,SAASC,SAAQC,UACG,OAAZA,SACAA,QAAQC,aAAa,WAAY,gCAUtBH,WACnBA,SAASC,SAAQC,UACG,OAAZA,SACAA,QAAQE,gBAAgB,8BAUfJ,WACjBA,SAASC,SAAQC,aACG,OAAZA,QAAkB,OAEZG,OAASH,QAAQI,QAAQ,oBAAsBJ,QAAQI,QAAQ,oBAAsBJ,QAAQI,QAAQ,aACvGD,OAAQ,CACRA,OAAOF,aAAa,SAAU,UAC9BE,OAAOE,UAAUC,IAAI,gBAGfC,MAAQC,SAASC,cAAc,cAAgBT,QAAQU,GAAK,MAC9DH,QACAA,MAAMN,aAAa,SAAU,UAC7BM,MAAMF,UAAUC,IAAI,+BAYnBR,WACjBA,SAASC,SAAQC,aACG,OAAZA,QAAkB,OAEZG,OAASH,QAAQI,QAAQ,oBAAsBJ,QAAQI,QAAQ,oBAAsBJ,QAAQI,QAAQ,aACvGD,OAAQ,CACRA,OAAOD,gBAAgB,UACvBC,OAAOE,UAAUM,OAAO,gBAGlBJ,MAAQC,SAASC,cAAc,cAAgBT,QAAQU,GAAK,MAC9DH,QACAA,MAAML,gBAAgB,UACtBK,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 index 8d87674d5d8a4..209b928930205 100644 --- a/lib/form/amd/build/form/rules.min.js +++ b/lib/form/amd/build/form/rules.min.js @@ -1,3 +1,3 @@ -define("core_form/form/rules",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;_exports.default=class{notchecked(target){const displayMap=this.form.mapTemplate();return this.getDependantsOfType(target.name,"notchecked").forEach(((dependant,key)=>{Boolean(key)===target.checked?showUnlock(dependant,displayMap):hideLock(dependant,displayMap)})),this.form.displayMapPrune(displayMap)}checked(target){const displayMap=this.form.mapTemplate();return this.getDependantsOfType(target.name,"checked").forEach(((dependant,key)=>{Boolean(key)!==target.checked?showUnlock(dependant,displayMap):hideLock(dependant,displayMap)})),this.form.displayMapPrune(displayMap)}eq(target){const displayMap=this.form.mapTemplate(),radioFieldVal=this.getRadioFieldVal(target);return this.getDependantsOfType(target.name,"eq").forEach(((dependant,key)=>{key===target.value||String(key)===String(null==radioFieldVal?void 0:radioFieldVal.value)?hideLock(dependant,displayMap):showUnlock(dependant,displayMap)})),this.form.displayMapPrune(displayMap)}neq(target){const displayMap=this.form.mapTemplate(),radioFieldVal=this.getRadioFieldVal(target);return this.getDependantsOfType(target.name,"neq").forEach(((dependant,key)=>{key!==target.value||String(key)!==String(null==radioFieldVal?void 0:radioFieldVal.value)?hideLock(dependant,displayMap):showUnlock(dependant,displayMap)})),this.form.displayMapPrune(displayMap)}in(target){const displayMap=this.form.mapTemplate();return this.getDependantsOfType(target.name,"in").forEach(((dependant,key)=>{key.split("|").includes(target.value)?hideLock(dependant,displayMap):showUnlock(dependant,displayMap)})),this.form.displayMapPrune(displayMap)}getDependantsOfType(element,type){return this.form.getDependantsOfType(element,type)}getRadioFieldVal(target){return"radio"===target.type?this.form.form.elements.namedItem(target.name):null}constructor(form){var obj,key,value;value=void 0,(key="form")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.form=form}};const dependencyBehaviour_disable=0,dependencyBehaviour_hide=1,showUnlock=(dependant,displayMap)=>{dependant.hasOwnProperty(dependencyBehaviour_hide)&&displayMap.get("show").push(dependant[dependencyBehaviour_hide]),dependant.hasOwnProperty(dependencyBehaviour_disable)&&displayMap.get("unlock").push(dependant[dependencyBehaviour_disable])},hideLock=(dependant,displayMap)=>{dependant.hasOwnProperty(dependencyBehaviour_hide)&&displayMap.get("hide").push(dependant[dependencyBehaviour_hide]),dependant.hasOwnProperty(dependencyBehaviour_disable)&&displayMap.get("lock").push(dependant[dependencyBehaviour_disable])};return _exports.default})); +define("core_form/form/rules",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;_exports.default=class{notchecked(target){const displayMap=this.form.mapTemplate();let lock=!1;return this.getDependantsOfType(target.name,"notchecked").forEach(((dependant,key)=>{lock=Boolean(key)!==target.checked,determineDisplayMap(dependant,displayMap,lock)})),this.form.displayMapPrune(displayMap)}checked(target){const displayMap=this.form.mapTemplate();let lock=!1;return this.getDependantsOfType(target.name,"checked").forEach(((dependant,key)=>{lock=Boolean(key)===target.checked,determineDisplayMap(dependant,displayMap,lock)})),this.form.displayMapPrune(displayMap)}eq(target){const displayMap=this.form.mapTemplate(),ctarget=this.getRadioFieldVal(target);let lock=!1;return this.getDependantsOfType(target.name,"eq").forEach(((dependant,key)=>{if(("radio"!==target.type||String(key)===String(ctarget.value))&&"hidden"!==target.type)return"checkbox"!==target.type||target.checksed?void(target.classList.contains("filepickerhidden")?(lock=!M.form_filepicker.instances[target.id].fileadded,determineDisplayMap(dependant,displayMap,lock)):"select"===target.tagName.toLowerCase()&&target.multiple||(lock=target.value===key,determineDisplayMap(dependant,displayMap,lock))):(lock=target.checked===Boolean(key),void determineDisplayMap(dependant,displayMap,lock))})),this.form.displayMapPrune(displayMap)}neq(target){var _this$getDependantsOf,_this$getDependantsOf2,_this$getDependantsOf3,_this$getDependantsOf4,_this$getDependantsOf5,_this$getDependantsOf6;const displayMap=this.form.mapTemplate(),ctarget=this.getRadioFieldVal(target);let lock=!1;const maps=[...null!==(_this$getDependantsOf=null===(_this$getDependantsOf2=this.getDependantsOfType(target.name,"neq"))||void 0===_this$getDependantsOf2?void 0:_this$getDependantsOf2.entries())&&void 0!==_this$getDependantsOf?_this$getDependantsOf:[],...null!==(_this$getDependantsOf3=null===(_this$getDependantsOf4=this.getDependantsOfType(target.name,"ne"))||void 0===_this$getDependantsOf4?void 0:_this$getDependantsOf4.entries())&&void 0!==_this$getDependantsOf3?_this$getDependantsOf3:[],...null!==(_this$getDependantsOf5=null===(_this$getDependantsOf6=this.getDependantsOfType(target.name,"noteq"))||void 0===_this$getDependantsOf6?void 0:_this$getDependantsOf6.entries())&&void 0!==_this$getDependantsOf5?_this$getDependantsOf5:[]];return new Map(maps).forEach(((dependant,key)=>{if(("radio"!==target.type||String(key)===String(ctarget.value))&&"hidden"!==target.type)return"checkbox"!==target.type||target.checked?void(target.classList.contains("filepickerhidden")?(lock=!!M.form_filepicker.instances[target.id].fileadded,determineDisplayMap(dependant,displayMap,lock)):"select"===target.tagName.toLowerCase()&&target.multiple||(lock=target.value!==key,determineDisplayMap(dependant,displayMap,lock))):(lock=target.checked===Boolean(key),void determineDisplayMap(dependant,displayMap,lock))})),this.form.displayMapPrune(displayMap)}in(target){const displayMap=this.form.mapTemplate();let lock=!1;return this.getDependantsOfType(target.name,"in").forEach(((dependant,key)=>{lock=key.split("|").includes(target.value),determineDisplayMap(dependant,displayMap,lock)})),this.form.displayMapPrune(displayMap)}getDependantsOfType(element,type){return this.form.getDependantsOfType(element,type)}getRadioFieldVal(target){return"radio"===target.type?this.form.form.elements.namedItem(target.name):target}constructor(form){var obj,key,value;value=void 0,(key="form")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.form=form}};const dependencyBehaviour_disable=0,dependencyBehaviour_hide=1,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").push(dependant[dependencyBehaviour_hide]):displayMap.get("show").push(dependant[dependencyBehaviour_hide]))};return _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 index 30bd4c50314e5..613bc3aae21fc 100644 --- a/lib/form/amd/build/form/rules.min.js.map +++ b/lib/form/amd/build/form/rules.min.js.map @@ -1 +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\nexport default class Rules {\n /**\n * @var {Form} form The instance of the form class that has a DOM node & references matched.\n */\n form;\n\n // Shorthand helpers rather than requiring devs to export the value and use an eq or neq check.\n\n /**\n * Compare the value of the changed DOM node to the requested 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 notchecked(target) {\n const displayMap = this.form.mapTemplate();\n\n this.getDependantsOfType(target.name, 'notchecked').forEach((dependant, key) => {\n if (Boolean(key) === target.checked) {\n showUnlock(dependant, displayMap);\n } else {\n hideLock(dependant, displayMap);\n }\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n /**\n * Compare the value of the changed DOM node to the requested 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 checked(target) {\n const displayMap = this.form.mapTemplate();\n\n this.getDependantsOfType(target.name, 'checked').forEach((dependant, key) => {\n if (Boolean(key) !== target.checked) {\n showUnlock(dependant, displayMap);\n } else {\n hideLock(dependant, displayMap);\n }\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n // Handlers for default rules defined in the links in the JSDoc for this class.\n\n /**\n * Compare the value of the changed DOM node to the requested 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 eq(target) {\n const displayMap = this.form.mapTemplate();\n const radioFieldVal = this.getRadioFieldVal(target);\n\n this.getDependantsOfType(target.name, 'eq').forEach((dependant, key) => {\n if (key === target.value || String(key) === String(radioFieldVal?.value)) {\n hideLock(dependant, displayMap);\n } else {\n showUnlock(dependant, displayMap);\n }\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n /**\n * Compare the value of the changed DOM node to the requested rule.\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 const displayMap = this.form.mapTemplate();\n const radioFieldVal = this.getRadioFieldVal(target);\n\n // TODO: get ne & noteq as well.\n this.getDependantsOfType(target.name, 'neq').forEach((dependant, key) => {\n if (key !== target.value || String(key) !== String(radioFieldVal?.value)) {\n hideLock(dependant, displayMap);\n } else {\n showUnlock(dependant, displayMap);\n }\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n // Moodle addons to the rules list.\n\n /**\n * Compare the value of the changed DOM node to the requested 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 const displayMap = this.form.mapTemplate();\n\n this.getDependantsOfType(target.name, 'in').forEach((dependant, key) => {\n if (key.split('|').includes(target.value)) {\n hideLock(dependant, displayMap);\n } else {\n showUnlock(dependant, displayMap);\n }\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n /**\n * Call off to the Form() to grab the elements that listen to the passed rule for the given element.\n *\n * @param {String} element The name of the element to get the dependants for.\n * @param {String} type The rule type to get the dependants for.\n * @returns {Array|[]}\n */\n getDependantsOfType(element, type) {\n return this.form.getDependantsOfType(element, type);\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 {RadioNodeList|null}\n */\n getRadioFieldVal(target) {\n return target.type === 'radio' ? this.form.form.elements.namedItem(target.name) : null;\n }\n\n /**\n * Constructor for the Rules class.\n * @param {Form} form The form object that the rules are being applied to.\n */\n constructor(form) {\n this.form = form;\n }\n}\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 * Considering the dependant object, show and unlock the elements that are required.\n *\n * @param {Object} dependant The dependant object that contains the rules for showing and unlocking.\n * @param {Map} displayMap The aggregation of elements that should be shown, hidden, locked, or unlocked.\n */\nconst showUnlock = (dependant, displayMap) => {\n if (dependant.hasOwnProperty(dependencyBehaviour.hide)) {\n displayMap.get('show').push(dependant[dependencyBehaviour.hide]);\n }\n if (dependant.hasOwnProperty(dependencyBehaviour.disable)) {\n displayMap.get('unlock').push(dependant[dependencyBehaviour.disable]);\n }\n};\n\n/**\n * Considering the dependant object, hide and lock the elements that are required.\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 */\nconst hideLock = (dependant, displayMap) => {\n if (dependant.hasOwnProperty(dependencyBehaviour.hide)) {\n displayMap.get('hide').push(dependant[dependencyBehaviour.hide]);\n }\n if (dependant.hasOwnProperty(dependencyBehaviour.disable)) {\n displayMap.get('lock').push(dependant[dependencyBehaviour.disable]);\n }\n};\n"],"names":["notchecked","target","displayMap","this","form","mapTemplate","getDependantsOfType","name","forEach","dependant","key","Boolean","checked","showUnlock","hideLock","displayMapPrune","eq","radioFieldVal","getRadioFieldVal","value","String","neq","in","split","includes","element","type","elements","namedItem","constructor","dependencyBehaviour","hasOwnProperty","get","push"],"mappings":"qKA0CIA,WAAWC,cACDC,WAAaC,KAAKC,KAAKC,0BAExBC,oBAAoBL,OAAOM,KAAM,cAAcC,SAAQ,CAACC,UAAWC,OAChEC,QAAQD,OAAST,OAAOW,QACxBC,WAAWJ,UAAWP,YAEtBY,SAASL,UAAWP,eAIrBC,KAAKC,KAAKW,gBAAgBb,YASrCU,QAAQX,cACEC,WAAaC,KAAKC,KAAKC,0BAExBC,oBAAoBL,OAAOM,KAAM,WAAWC,SAAQ,CAACC,UAAWC,OAC7DC,QAAQD,OAAST,OAAOW,QACxBC,WAAWJ,UAAWP,YAEtBY,SAASL,UAAWP,eAIrBC,KAAKC,KAAKW,gBAAgBb,YAWrCc,GAAGf,cACOC,WAAaC,KAAKC,KAAKC,cACvBY,cAAgBd,KAAKe,iBAAiBjB,oBAEvCK,oBAAoBL,OAAOM,KAAM,MAAMC,SAAQ,CAACC,UAAWC,OACxDA,MAAQT,OAAOkB,OAASC,OAAOV,OAASU,OAAOH,MAAAA,qBAAAA,cAAeE,OAC9DL,SAASL,UAAWP,YAEpBW,WAAWJ,UAAWP,eAIvBC,KAAKC,KAAKW,gBAAgBb,YAUrCmB,IAAIpB,cACMC,WAAaC,KAAKC,KAAKC,cACvBY,cAAgBd,KAAKe,iBAAiBjB,oBAGvCK,oBAAoBL,OAAOM,KAAM,OAAOC,SAAQ,CAACC,UAAWC,OACzDA,MAAQT,OAAOkB,OAASC,OAAOV,OAASU,OAAOH,MAAAA,qBAAAA,cAAeE,OAC9DL,SAASL,UAAWP,YAEpBW,WAAWJ,UAAWP,eAIvBC,KAAKC,KAAKW,gBAAgBb,YAWrCoB,GAAGrB,cACOC,WAAaC,KAAKC,KAAKC,0BAExBC,oBAAoBL,OAAOM,KAAM,MAAMC,SAAQ,CAACC,UAAWC,OACxDA,IAAIa,MAAM,KAAKC,SAASvB,OAAOkB,OAC/BL,SAASL,UAAWP,YAEpBW,WAAWJ,UAAWP,eAIvBC,KAAKC,KAAKW,gBAAgBb,YAUrCI,oBAAoBmB,QAASC,aAClBvB,KAAKC,KAAKE,oBAAoBmB,QAASC,MASlDR,iBAAiBjB,cACU,UAAhBA,OAAOyB,KAAmBvB,KAAKC,KAAKA,KAAKuB,SAASC,UAAU3B,OAAOM,MAAQ,KAOtFsB,YAAYzB,yKACHA,KAAOA,aASd0B,4BACO,EADPA,yBAEI,EASJjB,WAAa,CAACJ,UAAWP,cACvBO,UAAUsB,eAAeD,2BACzB5B,WAAW8B,IAAI,QAAQC,KAAKxB,UAAUqB,2BAEtCrB,UAAUsB,eAAeD,8BACzB5B,WAAW8B,IAAI,UAAUC,KAAKxB,UAAUqB,+BAU1ChB,SAAW,CAACL,UAAWP,cACrBO,UAAUsB,eAAeD,2BACzB5B,WAAW8B,IAAI,QAAQC,KAAKxB,UAAUqB,2BAEtCrB,UAAUsB,eAAeD,8BACzB5B,WAAW8B,IAAI,QAAQC,KAAKxB,UAAUqB"} \ No newline at end of file +{"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\nexport default class Rules {\n /**\n * @var {Form} form The instance of the form class that has a DOM node & references matched.\n */\n form;\n\n // Shorthand helpers rather than requiring devs to export the value and use an eq or neq check.\n\n /**\n * Compare the value of the changed DOM node to the requested 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 notchecked(target) {\n const displayMap = this.form.mapTemplate();\n let lock = false;\n\n this.getDependantsOfType(target.name, 'notchecked').forEach((dependant, key) => {\n lock = Boolean(key) !== target.checked;\n determineDisplayMap(dependant, displayMap, lock);\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n /**\n * Compare the value of the changed DOM node to the requested 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 checked(target) {\n const displayMap = this.form.mapTemplate();\n let lock = false;\n\n this.getDependantsOfType(target.name, 'checked').forEach((dependant, key) => {\n lock = Boolean(key) === target.checked;\n determineDisplayMap(dependant, displayMap, lock);\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n // Handlers for default rules defined in the links in the JSDoc for this class.\n\n /**\n * Compare the value of the changed DOM node to the requested 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 eq(target) {\n const displayMap = this.form.mapTemplate();\n const ctarget = this.getRadioFieldVal(target);\n let lock = false;\n\n this.getDependantsOfType(target.name, 'eq').forEach((dependant, key) => {\n if (target.type === 'radio' && String(key) !== String(ctarget.value)) {\n return;\n } else if (target.type === 'hidden') {\n // This is the hidden input that is part of an advcheckbox.\n //hiddenVal = target.value === key;\n return;\n } else if (target.type === 'checkbox' && !target.checksed) {\n lock = target.checked === Boolean(key);\n determineDisplayMap(dependant, displayMap, lock);\n return;\n }\n if (target.classList.contains('filepickerhidden')) {\n lock = !M.form_filepicker.instances[target.id].fileadded;\n determineDisplayMap(dependant, displayMap, lock);\n } else if (target.tagName.toLowerCase() === 'select' && target.multiple) {\n // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator\n // when multiple values have to be selected at the same time.\n } else {\n lock = target.value === key;\n determineDisplayMap(dependant, displayMap, lock);\n }\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n /**\n * Compare the value of the changed DOM node to the requested rule.\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 const displayMap = this.form.mapTemplate();\n const ctarget = this.getRadioFieldVal(target);\n let lock = false;\n\n // Get all the aliases of neq and check them all at once.\n const maps = [\n ...this.getDependantsOfType(target.name, 'neq')?.entries() ?? [],\n ...this.getDependantsOfType(target.name, 'ne')?.entries() ?? [],\n ...this.getDependantsOfType(target.name, 'noteq')?.entries() ?? [],\n ];\n const condensedMaps = new Map(maps);\n condensedMaps.forEach((dependant, key) => {\n if (target.type === 'radio' && String(key) !== String(ctarget.value)) {\n return;\n } else if (target.type === 'hidden') {\n // This is the hidden input that is part of an advcheckbox.\n //hiddenVal = target.value === key;\n return;\n } else if (target.type === 'checkbox' && !target.checked) {\n lock = target.checked === Boolean(key);\n determineDisplayMap(dependant, displayMap, lock);\n return;\n }\n if (target.classList.contains('filepickerhidden')) {\n lock = !!M.form_filepicker.instances[target.id].fileadded;\n determineDisplayMap(dependant, displayMap, lock);\n } else if (target.tagName.toLowerCase() === 'select' && target.multiple) {\n // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator\n // when multiple values have to be selected at the same time.\n } else {\n lock = target.value !== key;\n determineDisplayMap(dependant, displayMap, lock);\n }\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n // Moodle addons to the rules list.\n\n /**\n * Compare the value of the changed DOM node to the requested 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 const displayMap = this.form.mapTemplate();\n let lock = false;\n\n this.getDependantsOfType(target.name, 'in').forEach((dependant, key) => {\n lock = key.split('|').includes(target.value);\n determineDisplayMap(dependant, displayMap, lock);\n });\n\n return this.form.displayMapPrune(displayMap);\n }\n\n /**\n * Call off to the Form() to grab the elements that listen to the passed rule for the given element.\n *\n * @param {String} element The name of the element to get the dependants for.\n * @param {String} type The rule type to get the dependants for.\n * @returns {Array|[]}\n */\n getDependantsOfType(element, type) {\n return this.form.getDependantsOfType(element, type);\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 {RadioNodeList|HTMLFormElement}\n */\n getRadioFieldVal(target) {\n return target.type === 'radio' ? this.form.form.elements.namedItem(target.name) : target;\n }\n\n /**\n * Constructor for the Rules class.\n * @param {Form} form The form object that the rules are being applied to.\n */\n constructor(form) {\n this.form = form;\n }\n}\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 * 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 */\nconst determineDisplayMap = (dependant, displayMap, lock) => {\n const hide = dependant.hasOwnProperty(dependencyBehaviour.hide) ? lock : false;\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 } else if (dependant.hasOwnProperty(dependencyBehaviour.hide)) {\n if (hide) {\n displayMap.get('hide').push(dependant[dependencyBehaviour.hide]);\n } else {\n displayMap.get('show').push(dependant[dependencyBehaviour.hide]);\n }\n }\n};\n"],"names":["notchecked","target","displayMap","this","form","mapTemplate","lock","getDependantsOfType","name","forEach","dependant","key","Boolean","checked","determineDisplayMap","displayMapPrune","eq","ctarget","getRadioFieldVal","type","String","value","checksed","classList","contains","M","form_filepicker","instances","id","fileadded","tagName","toLowerCase","multiple","neq","maps","_this$getDependantsOf2","entries","_this$getDependantsOf4","_this$getDependantsOf6","Map","in","split","includes","element","elements","namedItem","constructor","dependencyBehaviour","hide","hasOwnProperty","get","push"],"mappings":"qKA0CIA,WAAWC,cACDC,WAAaC,KAAKC,KAAKC,kBACzBC,MAAO,cAENC,oBAAoBN,OAAOO,KAAM,cAAcC,SAAQ,CAACC,UAAWC,OACpEL,KAAOM,QAAQD,OAASV,OAAOY,QAC/BC,oBAAoBJ,UAAWR,WAAYI,SAGxCH,KAAKC,KAAKW,gBAAgBb,YASrCW,QAAQZ,cACEC,WAAaC,KAAKC,KAAKC,kBACzBC,MAAO,cAENC,oBAAoBN,OAAOO,KAAM,WAAWC,SAAQ,CAACC,UAAWC,OACjEL,KAAOM,QAAQD,OAASV,OAAOY,QAC/BC,oBAAoBJ,UAAWR,WAAYI,SAGxCH,KAAKC,KAAKW,gBAAgBb,YAWrCc,GAAGf,cACOC,WAAaC,KAAKC,KAAKC,cACvBY,QAAUd,KAAKe,iBAAiBjB,YAClCK,MAAO,cAENC,oBAAoBN,OAAOO,KAAM,MAAMC,SAAQ,CAACC,UAAWC,WACxC,UAAhBV,OAAOkB,MAAoBC,OAAOT,OAASS,OAAOH,QAAQI,SAEnC,WAAhBpB,OAAOkB,KAIX,MAAoB,aAAhBlB,OAAOkB,MAAwBlB,OAAOqB,cAK7CrB,OAAOsB,UAAUC,SAAS,qBAC1BlB,MAAQmB,EAAEC,gBAAgBC,UAAU1B,OAAO2B,IAAIC,UAC/Cf,oBAAoBJ,UAAWR,WAAYI,OACH,WAAjCL,OAAO6B,QAAQC,eAA8B9B,OAAO+B,WAI3D1B,KAAOL,OAAOoB,QAAUV,IACxBG,oBAAoBJ,UAAWR,WAAYI,SAZ3CA,KAAOL,OAAOY,UAAYD,QAAQD,UAClCG,oBAAoBJ,UAAWR,WAAYI,UAe5CH,KAAKC,KAAKW,gBAAgBb,YAUrC+B,IAAIhC,2JACMC,WAAaC,KAAKC,KAAKC,cACvBY,QAAUd,KAAKe,iBAAiBjB,YAClCK,MAAO,QAGL4B,KAAO,iEACN/B,KAAKI,oBAAoBN,OAAOO,KAAM,gDAAtC2B,uBAA8CC,iEAAa,oEAC3DjC,KAAKI,oBAAoBN,OAAOO,KAAM,+CAAtC6B,uBAA6CD,mEAAa,oEAC1DjC,KAAKI,oBAAoBN,OAAOO,KAAM,kDAAtC8B,uBAAgDF,mEAAa,WAE9C,IAAIG,IAAIL,MAChBzB,SAAQ,CAACC,UAAWC,WACV,UAAhBV,OAAOkB,MAAoBC,OAAOT,OAASS,OAAOH,QAAQI,SAEnC,WAAhBpB,OAAOkB,KAIX,MAAoB,aAAhBlB,OAAOkB,MAAwBlB,OAAOY,aAK7CZ,OAAOsB,UAAUC,SAAS,qBAC1BlB,OAASmB,EAAEC,gBAAgBC,UAAU1B,OAAO2B,IAAIC,UAChDf,oBAAoBJ,UAAWR,WAAYI,OACH,WAAjCL,OAAO6B,QAAQC,eAA8B9B,OAAO+B,WAI3D1B,KAAOL,OAAOoB,QAAUV,IACxBG,oBAAoBJ,UAAWR,WAAYI,SAZ3CA,KAAOL,OAAOY,UAAYD,QAAQD,UAClCG,oBAAoBJ,UAAWR,WAAYI,UAe5CH,KAAKC,KAAKW,gBAAgBb,YAWrCsC,GAAGvC,cACOC,WAAaC,KAAKC,KAAKC,kBACzBC,MAAO,cAENC,oBAAoBN,OAAOO,KAAM,MAAMC,SAAQ,CAACC,UAAWC,OAC5DL,KAAOK,IAAI8B,MAAM,KAAKC,SAASzC,OAAOoB,OACtCP,oBAAoBJ,UAAWR,WAAYI,SAGxCH,KAAKC,KAAKW,gBAAgBb,YAUrCK,oBAAoBoC,QAASxB,aAClBhB,KAAKC,KAAKG,oBAAoBoC,QAASxB,MASlDD,iBAAiBjB,cACU,UAAhBA,OAAOkB,KAAmBhB,KAAKC,KAAKA,KAAKwC,SAASC,UAAU5C,OAAOO,MAAQP,OAOtF6C,YAAY1C,yKACHA,KAAOA,aASd2C,4BACO,EADPA,yBAEI,EAUJjC,oBAAsB,CAACJ,UAAWR,WAAYI,cAC1C0C,OAAOtC,UAAUuC,eAAeF,2BAA4BzC,KAC9DI,UAAUuC,eAAeF,8BACzB7C,WAAWgD,IAAI,QAAQC,KAAKzC,UAAUqC,8BAClCzC,KACAJ,WAAWgD,IAAI,QAAQC,KAAKzC,UAAUqC,8BAEtC7C,WAAWgD,IAAI,UAAUC,KAAKzC,UAAUqC,+BAErCrC,UAAUuC,eAAeF,4BAC5BC,KACA9C,WAAWgD,IAAI,QAAQC,KAAKzC,UAAUqC,2BAEtC7C,WAAWgD,IAAI,QAAQC,KAAKzC,UAAUqC"} \ No newline at end of file diff --git a/lib/form/amd/src/form.js b/lib/form/amd/src/form.js index 74d716ac265a8..5d6d7f04344ce 100644 --- a/lib/form/amd/src/form.js +++ b/lib/form/amd/src/form.js @@ -30,7 +30,6 @@ // Pure form functionality. // import {serialize} from './util'; import * as FormChangeChecker from './changechecker'; -import * as FormEvents from './events'; import * as Submit from './submit'; import Rules from './form/rules'; import * as MutateDom from './form/dom'; @@ -70,10 +69,23 @@ export default class Form { this.dependencies = this.getDependencyMapper(dependencies); 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); - // TODO: Apply the rules to the form on load. + } + + /** + * Given the page has loaded, apply the initial state of the form. + */ + applyInitialState() { + [...this.form.elements].forEach((element) => { + if (this.dependencies.has(element.name)) { + this.domDispatch(this.displayMapPrune(this.dispatchDependencyRules(element))); + } + }); } /** @@ -82,20 +94,17 @@ export default class Form { registerEventListeners() { this.form.addEventListener('change', (e) => { if (e.target.type === 'submit') { - // TODO: Dummy calls so import list looks better... - // Notify listeners that the form is about to be submitted. - FormEvents.notifyFormSubmittedByJavascript(this.form); 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)) { - const displayMap = this.dispatchDependencyRules(e.target); - const reducedDisplayMap = this.displayMapPrune(displayMap); - this.domDispatch(reducedDisplayMap); + FormChangeChecker.markFormChangedFromNode(e.target); + this.domDispatch(this.displayMapPrune(this.dispatchDependencyRules(e.target))); } }); } @@ -128,14 +137,11 @@ export default class Form { * @returns {Map|Map<>} The pruned map or map even a fully pruned map if noting has to change. */ displayMapPrune(displayMap) { - window.console.log('preprune:', displayMap); - // Filter any locked items that are to be hidden anyway. - const removelockifhidden = displayMap.get('lock').filter(x => displayMap.get('hide').includes(x)); - window.console.log('removelockifhidden:', removelockifhidden); - displayMap.set('lock', removelockifhidden); // Filter any unlocked items that pegged to be hidden as they must be locked if they are hidden. - //const removeunlockifhidden = displayMap.get('unlock').filter(x => displayMap.get('hide').includes(x)); - //displayMap.set('unlock', removeunlockifhidden); + const removeunlockifhidden = displayMap.get('unlock').filter(x => { + return !displayMap.get('hide').toString().includes(x.toString()); + }); + displayMap.set('unlock', removeunlockifhidden); // Remove any empty entries. for (const [key, value] of displayMap) { @@ -163,7 +169,6 @@ export default class Form { * @param {Map} elNamesMap What needs to change. */ domDispatch(elNamesMap) { - window.console.log('elNamesMap', elNamesMap); elNamesMap.forEach((elements, domUpdateOpt) => { if (!MutateDom[domUpdateOpt]) { return; @@ -181,12 +186,15 @@ export default class Form { /** * Convert the element names into DOM nodes based on the element names. * - * @param {Array} elementNames The name of dependent elements to get associated DOM nodes. + * @param {Array} elementNames The name of dependent elements to get associated DOM nodes. * @returns {Array} */ elementNamesToDomNodes(elementNames) { return elementNames.map((element) => { - return this.form.elements.namedItem(element); + // TODO: Rename and document why we need to deep look if there are multiple nodes effected by the rule. + return element.map((element2) => { + return this.form.elements.namedItem(element2); + }); }); } @@ -252,7 +260,21 @@ export default class Form { */ const ruleMap = new Map(Object.entries(elementrules)); ruleMap.forEach((ruleComparisons, key) => { - ruleMap.set(key, new Map(Object.entries(ruleComparisons))); + /** + * Convert any disabledIf rules into objects, so we can manage them the same as hideIf items. + * + * @type {Map} The map of comparison values t. + * @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); }); diff --git a/lib/form/amd/src/form/dom.js b/lib/form/amd/src/form/dom.js index 8050af73632fe..5606a635d8124 100644 --- a/lib/form/amd/src/form/dom.js +++ b/lib/form/amd/src/form/dom.js @@ -23,71 +23,78 @@ "use strict"; -/** - * TODO: Considerations requiring action: - * - * Things old YUI looked for after grabbing the element DOM ancestor (Label management): - * Classes: fitem fitem_fgroup - * Data attributes: label[for="' + id + '"] - * Selectors from old YUI: - * .fitem [data-fieldtype="filepicker"] input, - * .fitem [data-fieldtype="filemanager"] input, - * .fitem [data-fieldtype="group"] input[id*="filemanager"] - * - * Guessed selectors: - * [data-groupname="${element}"] - * - * Other things to consider: - * Special handling for editors & filepickers i.e. Name suffix on editors -> See old YUI? - * element.setAttribute('aria-disabled', >); - * element.setAttribute('readonly', 'readonly'); - */ - /** * Disable an element. * - * @param {HTMLElement} element The element to be disabled. + * @param {Array} elements The element to be disabled. */ -// eslint-disable-next-line no-unused-vars -export const lock = (element) => { - // window.console.log('Lock node:', element); - // element.disabled = true; - // element.classList.add('disabled'); +export const lock = (elements) => { + elements.forEach(element => { + if (element !== null) { + element.setAttribute('disabled', 'disabled'); + } + }); }; /** * Enable an element. * - * @param {HTMLElement} element The element to be enabled. + * @param {Array} elements The element to be enabled. */ -export const unlock = (element) => { - show(element); - // window.console.log('Unlock node:', element); - // element.disabled = false; - // element.classList.remove('disabled'); +export const unlock = (elements) => { + elements.forEach(element => { + if (element !== null) { + element.removeAttribute('disabled'); + } + }); }; /** * Hide an element. * - * @param {HTMLElement} element The element to be hidden. + * @param {Array} elements The elements to be hidden. */ -export const hide = (element) => { - // Lock the element to prevent interaction. - lock(element); - // window.console.log('Hide node:', element); - // element.hidden = true; - // element.classList.add('hidden'); +export const hide = (elements) => { + elements.forEach(element => { + if (element !== null) { + // Check if the element is part of a group otherwise, just grab the nearest wrapping element. + const parent = element.closest('[data-groupname]') ? element.closest('[data-groupname]') : 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 element to be shown. + * @param {Array} elements The elements to be shown. */ -// eslint-disable-next-line no-unused-vars -export const show = (element) => { - //window.console.log('Show node:', element); - // element.hidden = false; - // element.classList.remove('hidden'); +export const show = (elements) => { + elements.forEach(element => { + if (element !== null) { + // Check if the element is part of a group otherwise, just grab the nearest wrapping element. + const parent = element.closest('[data-groupname]') ? element.closest('[data-groupname]') : 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 index a7d746187bb7e..1317725007959 100644 --- a/lib/form/amd/src/form/rules.js +++ b/lib/form/amd/src/form/rules.js @@ -42,13 +42,11 @@ export default class Rules { */ notchecked(target) { const displayMap = this.form.mapTemplate(); + let lock = false; this.getDependantsOfType(target.name, 'notchecked').forEach((dependant, key) => { - if (Boolean(key) === target.checked) { - showUnlock(dependant, displayMap); - } else { - hideLock(dependant, displayMap); - } + lock = Boolean(key) !== target.checked; + determineDisplayMap(dependant, displayMap, lock); }); return this.form.displayMapPrune(displayMap); @@ -62,13 +60,11 @@ export default class Rules { */ checked(target) { const displayMap = this.form.mapTemplate(); + let lock = false; this.getDependantsOfType(target.name, 'checked').forEach((dependant, key) => { - if (Boolean(key) !== target.checked) { - showUnlock(dependant, displayMap); - } else { - hideLock(dependant, displayMap); - } + lock = Boolean(key) === target.checked; + determineDisplayMap(dependant, displayMap, lock); }); return this.form.displayMapPrune(displayMap); @@ -84,13 +80,30 @@ export default class Rules { */ eq(target) { const displayMap = this.form.mapTemplate(); - const radioFieldVal = this.getRadioFieldVal(target); + const ctarget = this.getRadioFieldVal(target); + let lock = false; this.getDependantsOfType(target.name, 'eq').forEach((dependant, key) => { - if (key === target.value || String(key) === String(radioFieldVal?.value)) { - hideLock(dependant, displayMap); + if (target.type === 'radio' && String(key) !== String(ctarget.value)) { + return; + } else if (target.type === 'hidden') { + // This is the hidden input that is part of an advcheckbox. + //hiddenVal = target.value === key; + return; + } else if (target.type === 'checkbox' && !target.checksed) { + lock = target.checked === Boolean(key); + determineDisplayMap(dependant, displayMap, lock); + return; + } + if (target.classList.contains('filepickerhidden')) { + lock = !M.form_filepicker.instances[target.id].fileadded; + determineDisplayMap(dependant, displayMap, lock); + } else if (target.tagName.toLowerCase() === 'select' && target.multiple) { + // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator + // when multiple values have to be selected at the same time. } else { - showUnlock(dependant, displayMap); + lock = target.value === key; + determineDisplayMap(dependant, displayMap, lock); } }); @@ -106,14 +119,37 @@ export default class Rules { */ neq(target) { const displayMap = this.form.mapTemplate(); - const radioFieldVal = this.getRadioFieldVal(target); - - // TODO: get ne & noteq as well. - this.getDependantsOfType(target.name, 'neq').forEach((dependant, key) => { - if (key !== target.value || String(key) !== String(radioFieldVal?.value)) { - hideLock(dependant, displayMap); + const ctarget = this.getRadioFieldVal(target); + let lock = false; + + // Get all the aliases of neq and check them all at once. + const maps = [ + ...this.getDependantsOfType(target.name, 'neq')?.entries() ?? [], + ...this.getDependantsOfType(target.name, 'ne')?.entries() ?? [], + ...this.getDependantsOfType(target.name, 'noteq')?.entries() ?? [], + ]; + const condensedMaps = new Map(maps); + condensedMaps.forEach((dependant, key) => { + if (target.type === 'radio' && String(key) !== String(ctarget.value)) { + return; + } else if (target.type === 'hidden') { + // This is the hidden input that is part of an advcheckbox. + //hiddenVal = target.value === key; + return; + } else if (target.type === 'checkbox' && !target.checked) { + lock = target.checked === Boolean(key); + determineDisplayMap(dependant, displayMap, lock); + return; + } + if (target.classList.contains('filepickerhidden')) { + lock = !!M.form_filepicker.instances[target.id].fileadded; + determineDisplayMap(dependant, displayMap, lock); + } else if (target.tagName.toLowerCase() === 'select' && target.multiple) { + // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator + // when multiple values have to be selected at the same time. } else { - showUnlock(dependant, displayMap); + lock = target.value !== key; + determineDisplayMap(dependant, displayMap, lock); } }); @@ -130,13 +166,11 @@ export default class Rules { */ in(target) { const displayMap = this.form.mapTemplate(); + let lock = false; this.getDependantsOfType(target.name, 'in').forEach((dependant, key) => { - if (key.split('|').includes(target.value)) { - hideLock(dependant, displayMap); - } else { - showUnlock(dependant, displayMap); - } + lock = key.split('|').includes(target.value); + determineDisplayMap(dependant, displayMap, lock); }); return this.form.displayMapPrune(displayMap); @@ -157,10 +191,10 @@ export default class Rules { * 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 {RadioNodeList|null} + * @returns {RadioNodeList|HTMLFormElement} */ getRadioFieldVal(target) { - return target.type === 'radio' ? this.form.form.elements.namedItem(target.name) : null; + return target.type === 'radio' ? this.form.form.elements.namedItem(target.name) : target; } /** @@ -183,31 +217,26 @@ const dependencyBehaviour = { }; /** - * Considering the dependant object, show and unlock the elements that are required. - * - * @param {Object} dependant The dependant object that contains the rules for showing and unlocking. - * @param {Map} displayMap The aggregation of elements that should be shown, hidden, locked, or unlocked. - */ -const showUnlock = (dependant, displayMap) => { - if (dependant.hasOwnProperty(dependencyBehaviour.hide)) { - displayMap.get('show').push(dependant[dependencyBehaviour.hide]); - } - if (dependant.hasOwnProperty(dependencyBehaviour.disable)) { - displayMap.get('unlock').push(dependant[dependencyBehaviour.disable]); - } -}; - -/** - * Considering the dependant object, hide and lock the elements that are required. + * 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. */ -const hideLock = (dependant, displayMap) => { - if (dependant.hasOwnProperty(dependencyBehaviour.hide)) { - displayMap.get('hide').push(dependant[dependencyBehaviour.hide]); - } +const determineDisplayMap = (dependant, displayMap, lock) => { + const hide = dependant.hasOwnProperty(dependencyBehaviour.hide) ? lock : false; if (dependant.hasOwnProperty(dependencyBehaviour.disable)) { - displayMap.get('lock').push(dependant[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]); + } + } else if (dependant.hasOwnProperty(dependencyBehaviour.hide)) { + if (hide) { + displayMap.get('hide').push(dependant[dependencyBehaviour.hide]); + } else { + displayMap.get('show').push(dependant[dependencyBehaviour.hide]); + } } }; diff --git a/lib/form/tests/behat/fixtures/form_rules.php b/lib/form/tests/behat/fixtures/form_rules.php index 0099cc98ce62f..f672fec85df2b 100644 --- a/lib/form/tests/behat/fixtures/form_rules.php +++ b/lib/form/tests/behat/fixtures/form_rules.php @@ -65,18 +65,23 @@ public function definition() { $mform->hideIf('r_neq_btn', 'rgt', 'neq', '3'); // Checkboxes checked/notchecked rule test. - $mform->addElement('header', 'checkboxheader', 'Checkboxes: checked/notchecked'); + $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', ' ', false); + $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', 'Text input'); + $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'); @@ -87,6 +92,28 @@ public function definition() { $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->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'); @@ -130,8 +157,10 @@ public function definition() { // Date selector rule test. $mform->addElement('header', 'dateselectorheader', 'Date selector: ~'); - //$mform->setExpanded('dateselectorheader'); + $mform->setExpanded('dateselectorheader'); + $mform->addElement('checkbox', 'ds_enb', get_string('enable')); $mform->addElement('date_selector', 'ds', 'Date selector'); + $mform->hideIf("ds", 'ds_enb'); // Editor rule test. $mform->addElement('header', 'editorheader', 'Editor: ~'); @@ -151,7 +180,7 @@ public function definition() { // Filepicker rule test. $mform->addElement('header', 'filepickerheader', 'Filepicker: ~'); //$mform->setExpanded('filepickerheader'); - $mform->addElement('filepicker', 'modelfile', get_string('file'), null, ['accepted_types' => '.zip']); + $mform->addElement('filepicker', 'modelfile', get_string('file'), null, ['accepted_types' => '*']); $this->add_action_buttons(false, 'Send form'); } diff --git a/lib/formslib.php b/lib/formslib.php index b5314c17c8e6f..f3ba9e2069a18 100644 --- a/lib/formslib.php +++ b/lib/formslib.php @@ -3526,7 +3526,7 @@ function finishForm(&$form){ if (!$form->isFrozen()) { $args = $form->getLockOptionObject(); if (count($args[1]) > 0) { - $PAGE->requires->js_init_call('M.form.initFormDependencies', $args, true, moodleform::get_js_module()); + //$PAGE->requires->js_init_call('M.form.initFormDependencies', $args, true, moodleform::get_js_module()); $PAGE->requires->js_call_amd('core_form/form', 'init', $args); } }