From d20498625cdac659f96a8373e1cdd1fc7aecd7bc Mon Sep 17 00:00:00 2001 From: Davo Smith Date: Mon, 19 Dec 2016 11:36:54 +0000 Subject: [PATCH] MDL-53848 form: add hideIf functionality --- lib/form/form.js | 68 ++++++++------- lib/form/tests/behat/hideif.feature | 52 ++++++++++++ .../tests/fixtures/formhideiftestpage.php | 83 +++++++++++++++++++ lib/formslib.php | 72 +++++++++++++++- 4 files changed, 240 insertions(+), 35 deletions(-) create mode 100644 lib/form/tests/behat/hideif.feature create mode 100644 lib/form/tests/fixtures/formhideiftestpage.php diff --git a/lib/form/form.js b/lib/form/form.js index 38e518bef44..13464fbaf31 100644 --- a/lib/form/form.js +++ b/lib/form/form.js @@ -69,8 +69,10 @@ if (typeof M.form.dependencyManager === 'undefined') { names[i] = new Y.NodeList(); for (var condition in conditions) { for (var value in conditions[condition]) { - for (var ei in conditions[condition][value]) { - names[conditions[condition][value][ei]] = new Y.NodeList(); + for (var hide in conditions[condition][value]) { + for (var ei in conditions[condition][value][hide]) { + names[conditions[condition][value][hide][ei]] = new Y.NodeList(); + } } } } @@ -118,7 +120,7 @@ if (typeof M.form.dependencyManager === 'undefined') { var dependencies = this.get('dependencies'), tohide = {}, tolock = {}, - condition, value, lock, hide, + condition, value, isHide, lock, hide, checkfunction, result, elements; if (!({}).hasOwnProperty.call(dependencies, dependon)) { return true; @@ -126,26 +128,28 @@ if (typeof M.form.dependencyManager === 'undefined') { elements = this.elementsByName(dependon); for (condition in dependencies[dependon]) { for (value in dependencies[dependon][condition]) { - checkfunction = '_dependency' + condition[0].toUpperCase() + condition.slice(1); - if (Y.Lang.isFunction(this[checkfunction])) { - result = this[checkfunction].apply(this, [elements, value, e]); - } else { - result = this._dependencyDefault(elements, value, e); - } - lock = result.lock || false; - hide = result.hide || false; - for (var ei in dependencies[dependon][condition][value]) { - var eltolock = dependencies[dependon][condition][value][ei]; - if (({}).hasOwnProperty.call(tohide, eltolock)) { - tohide[eltolock] = tohide[eltolock] || hide; + for (isHide in dependencies[dependon][condition][value]) { + checkfunction = '_dependency' + condition[0].toUpperCase() + condition.slice(1); + if (Y.Lang.isFunction(this[checkfunction])) { + result = this[checkfunction].apply(this, [elements, value, !!isHide, e]); } else { - tohide[eltolock] = hide; + result = this._dependencyDefault(elements, value, !!isHide, e); } + lock = result.lock || false; + hide = result.hide || false; + for (var ei in dependencies[dependon][condition][value][isHide]) { + var eltolock = dependencies[dependon][condition][value][isHide][ei]; + if (({}).hasOwnProperty.call(tohide, eltolock)) { + tohide[eltolock] = tohide[eltolock] || hide; + } else { + tohide[eltolock] = hide; + } - if (({}).hasOwnProperty.call(tolock, eltolock)) { - tolock[eltolock] = tolock[eltolock] || lock; - } else { - tolock[eltolock] = lock; + if (({}).hasOwnProperty.call(tolock, eltolock)) { + tolock[eltolock] = tolock[eltolock] || lock; + } else { + tolock[eltolock] = lock; + } } } } @@ -291,7 +295,7 @@ if (typeof M.form.dependencyManager === 'undefined') { return false; }, - _dependencyNotchecked: function(elements, value) { + _dependencyNotchecked: function(elements, value, isHide) { var lock = false; elements.each(function() { if (this.getAttribute('type').toLowerCase() == 'hidden' && @@ -306,10 +310,10 @@ if (typeof M.form.dependencyManager === 'undefined') { }); return { lock: lock, - hide: false + hide: isHide ? lock : false }; }, - _dependencyChecked: function(elements, value) { + _dependencyChecked: function(elements, value, isHide) { var lock = false; elements.each(function() { if (this.getAttribute('type').toLowerCase() == 'hidden' && @@ -324,20 +328,20 @@ if (typeof M.form.dependencyManager === 'undefined') { }); return { lock: lock, - hide: false + hide: isHide ? lock : false }; }, - _dependencyNoitemselected: function(elements, value) { + _dependencyNoitemselected: function(elements, value, isHide) { var lock = false; elements.each(function() { lock = lock || this.get('selectedIndex') == -1; }); return { lock: lock, - hide: false + hide: isHide ? lock : false }; }, - _dependencyEq: function(elements, value) { + _dependencyEq: function(elements, value, isHide) { var lock = false; var hiddenVal = false; var options, v, selected, values; @@ -391,7 +395,7 @@ if (typeof M.form.dependencyManager === 'undefined') { }); return { lock: lock, - hide: false + hide: isHide ? lock : false }; }, /** @@ -402,7 +406,7 @@ if (typeof M.form.dependencyManager === 'undefined') { * @returns {{lock: boolean, hide: boolean}} * @private */ - _dependencyIn: function(elements, values) { + _dependencyIn: function(elements, values, isHide) { // A pipe (|) is used as a value separator // when multiple values have to be passed on at the same time. values = values.split('|'); @@ -458,7 +462,7 @@ if (typeof M.form.dependencyManager === 'undefined') { }); return { lock: lock, - hide: false + hide: isHide ? lock : false }; }, _dependencyHide: function(elements, value) { @@ -467,7 +471,7 @@ if (typeof M.form.dependencyManager === 'undefined') { hide: true }; }, - _dependencyDefault: function(elements, value, ev) { + _dependencyDefault: function(elements, value, isHide) { var lock = false, hiddenVal = false, values @@ -521,7 +525,7 @@ if (typeof M.form.dependencyManager === 'undefined') { }); return { lock: lock, - hide: false + hide: isHide ? lock : false }; } }, { diff --git a/lib/form/tests/behat/hideif.feature b/lib/form/tests/behat/hideif.feature new file mode 100644 index 00000000000..864216a8fa7 --- /dev/null +++ b/lib/form/tests/behat/hideif.feature @@ -0,0 +1,52 @@ +@core @javascript +Feature: hideIf functionality in forms + For forms including hideIf functions + As a user + If I trigger the hideIf condition then the form elements will be hidden + + Background: + Given the following "activities" exist: + | activity | name | intro | course | section | idnumber | + | label | L1 | HideIfLink | Acceptance test site | 1 | L1 | + And I am on site homepage + And I follow "HideIfLink" + + Scenario: When 'eq' hideIf conditions are not met, the relevant elements are shown + When I set the field "Select yesno example" to "Yes" + Then I should see "Test eq hideif" + And "#id_testeqhideif" "css_element" should be visible + + Scenario: When 'eq' hideIf conditions are met, the relevant elements are hidden + When I set the field "Select yesno example" to "No" + Then I should not see "Test eq hideif" + And "#id_testeqhideif" "css_element" should not be visible + + Scenario: When 'checked' hideIf conditions are not met, the relevant elements are shown + When I set the field "Checkbox example" to "0" + Then I should see "Test checked hideif" + And "#id_testcheckedhideif" "css_element" should be visible + + Scenario: When 'checked' hideIf conditions are met, the relevant elements are hidden + When I set the field "Checkbox example" to "1" + Then I should not see "Test checked hideif" + And "#id_testcheckedhideif" "css_element" should not be visible + + Scenario: When 'notchecked' hideIf conditions are not met, the relevant elements are shown + When I set the field "Checkbox example" to "1" + Then I should see "Test not checked hideif" + And "#id_testnotcheckedhideif" "css_element" should be visible + + Scenario: When 'notchecked' hideIf conditions are met, the relevant elements are hidden + When I set the field "Checkbox example" to "0" + Then I should not see "Test not checked hideif" + And "#id_testnotcheckedhideif" "css_element" should not be visible + + Scenario: When 'in' hideIf conditions are not met, the relevant elements are shown + When I set the field "Select example" to "3" + Then I should see "Test in hideif" + And "#id_testinhideif" "css_element" should be visible + + Scenario: When 'in' hideIf conditions are met, the relevant elements are hidden + When I set the field "Select example" to "2" + Then I should not see "Test in hideif" + And "#id_testinhideif" "css_element" should not be visible diff --git a/lib/form/tests/fixtures/formhideiftestpage.php b/lib/form/tests/fixtures/formhideiftestpage.php new file mode 100644 index 00000000000..86a856ed0f7 --- /dev/null +++ b/lib/form/tests/fixtures/formhideiftestpage.php @@ -0,0 +1,83 @@ +. + +/** + * To support behat tests for hideif functionality (which is not yet used by any core forms). + * + * @package core + * @copyright 2016 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__).'/../../../../config.php'); +global $CFG, $PAGE, $OUTPUT; +require_once($CFG->libdir.'/formslib.php'); + +// Behat test fixture only. +defined('BEHAT_SITE_RUNNING') || die('Only available on Behat test server'); + +/** + * Class hideif_form + * @copyright 2016 Davo Smith, Synergy Learning + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class hideif_form extends moodleform { + + /** + * Form definition. + */ + protected function definition() { + $mform = $this->_form; + + // Use 'selectyesno' to show/hide element. + $mform->addElement('selectyesno', 'selectyesnoexample', 'Select yesno example'); + $mform->setDefault('selectyesnoexample', 0); + + $mform->addElement('text', 'testeqhideif', 'Test eq hideif'); + $mform->setType('testeqhideif', PARAM_TEXT); + $mform->hideIf('testeqhideif', 'selectyesnoexample', 'eq', 0); + + // Use 'checkbox' to show/hide element. + $mform->addElement('advcheckbox', 'checkboxexample', 'Checkbox example'); + $mform->setDefault('checkboxexample', 0); + + $mform->addElement('text', 'testcheckedhideif', 'Test checked hideif'); + $mform->setType('testcheckedhideif', PARAM_TEXT); + $mform->hideIf('testcheckedhideif', 'checkboxexample', 'checked'); + + $mform->addElement('text', 'testnotcheckedhideif', 'Test not checked hideif'); + $mform->setType('testnotcheckedhideif', PARAM_TEXT); + $mform->hideIf('testnotcheckedhideif', 'checkboxexample', 'notchecked'); + + // Use 'select' to show/hide element. + $opts = [1, 2, 3, 4, 5]; + $opts = array_combine($opts, $opts); + $mform->addElement('select', 'selectexample', 'Select example', $opts); + $mform->setDefault('selectexample', 1); + + $mform->addElement('text', 'testinhideif', 'Test in hideif'); + $mform->setType('testinhideif', PARAM_TEXT); + $mform->hideIf('testinhideif', 'selectexample', 'in', [1, 2, 5]); + } +} + +$PAGE->set_url('/lib/tests/fixtures/form_hideif.php'); +$PAGE->set_context(context_system::instance()); +$form = new hideif_form(); + +echo $OUTPUT->header(); +$form->display(); +echo $OUTPUT->footer(); diff --git a/lib/formslib.php b/lib/formslib.php index 965b0a4f51a..5c41ad2d9d9 100644 --- a/lib/formslib.php +++ b/lib/formslib.php @@ -1412,6 +1412,11 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless { /** @var array dependent state for the element/'s */ var $_dependencies = array(); + /** + * @var array elements that will become hidden based on another element + */ + protected $_hideifs = array(); + /** @var array Array of buttons that if pressed do not result in the processing of the form. */ var $_noSubmitButtons=array(); @@ -1460,6 +1465,12 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless { */ protected $clientvalidation = false; + /** + * Is this a 'disableIf' dependency of a 'hideIf' dependency? + */ + const DEP_DISABLE = 0; + const DEP_HIDE = 1; + /** * Class constructor - same parameters as HTML_QuickForm_DHTMLRulesTableless * @@ -2426,8 +2437,7 @@ require(["core/event", "jquery"], function(Event, $) { foreach ($conditions as $condition=>$values) { $result[$dependentOn][$condition] = array(); foreach ($values as $value=>$dependents) { - $result[$dependentOn][$condition][$value] = array(); - $i = 0; + $result[$dependentOn][$condition][$value][self::DEP_DISABLE] = array(); foreach ($dependents as $dependent) { $elements = $this->_getElNamesRecursive($dependent); if (empty($elements)) { @@ -2438,7 +2448,29 @@ require(["core/event", "jquery"], function(Event, $) { if ($element == $dependentOn) { continue; } - $result[$dependentOn][$condition][$value][] = $element; + $result[$dependentOn][$condition][$value][0][] = $element; + } + } + } + } + } + foreach ($this->_hideifs as $dependenton => $conditions){ + $result[$dependenton] = array(); + foreach ($conditions as $condition => $values) { + $result[$dependenton][$condition] = array(); + foreach ($values as $value => $dependents) { + $result[$dependenton][$condition][$value][self::DEP_HIDE] = array(); + foreach ($dependents as $dependent) { + $elements = $this->_getElNamesRecursive($dependent); + if (empty($elements)) { + // Probably element inside of some group. + $elements = array($dependent); + } + foreach ($elements as $element) { + if ($element == $dependenton) { + continue; + } + $result[$dependenton][$condition][$value][1][] = $element; } } } @@ -2533,6 +2565,40 @@ require(["core/event", "jquery"], function(Event, $) { $this->_dependencies[$dependentOn][$condition][$value][] = $elementName; } + /** + * Adds a dependency for $elementName which will be hidden if $condition is met. + * If $condition = 'notchecked' (default) then the condition is that the $dependentOn element + * is not checked. If $condition = 'checked' then the condition is that the $dependentOn element + * is checked. If $condition is something else (like "eq" for equals) then it is checked to see if the value + * of the $dependentOn element is $condition (such as equal) to $value. + * + * When working with multiple selects, the dependentOn has to be the real name of the select, meaning that + * it will most likely end up with '[]'. Also, the value should be an array of required values, or a string + * containing the values separated by pipes: array('red', 'blue') or 'red|blue'. + * + * @param string $elementname the name of the element which will be hidden + * @param string $dependenton the name of the element whose state will be checked for condition + * @param string $condition the condition to check + * @param mixed $value used in conjunction with condition. + */ + public function hideIf($elementname, $dependenton, $condition = 'notchecked', $value = '1') { + // Multiple selects allow for a multiple selection, we transform the array to string here as + // an array cannot be used as a key in an associative array. + if (is_array($value)) { + $value = implode('|', $value); + } + if (!array_key_exists($dependenton, $this->_hideifs)) { + $this->_hideifs[$dependenton] = array(); + } + if (!array_key_exists($condition, $this->_hideifs[$dependenton])) { + $this->_hideifs[$dependenton][$condition] = array(); + } + if (!array_key_exists($value, $this->_hideifs[$dependenton][$condition])) { + $this->_hideifs[$dependenton][$condition][$value] = array(); + } + $this->_hideifs[$dependenton][$condition][$value][] = $elementname; + } + /** * Registers button as no submit button *