MDL-52167 admin: now able to show/hide settings based on conditions

This commit is contained in:
Davo Smith 2017-12-07 14:31:32 +00:00 committed by Sara Arjona
parent 2bc0774cc1
commit 8ed3671d4c
4 changed files with 459 additions and 0 deletions

View File

@ -156,4 +156,11 @@ $PAGE->requires->yui_module('moodle-core-formchangechecker',
);
$PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle');
if ($settingspage->has_dependencies()) {
$opts = [
'dependencies' => $settingspage->get_dependencies_for_javascript()
];
$PAGE->requires->js_call_amd('core/showhidesettings', 'init', [$opts]);
}
echo $OUTPUT->footer();

View File

@ -1317,6 +1317,84 @@ class admin_externalpage implements part_of_admin_tree {
}
}
/**
* Used to store details of the dependency between two settings elements.
*
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright 2017 Davo Smith, Synergy Learning
*/
class admin_settingdependency {
/** @var string the name of the setting to be shown/hidden */
public $settingname;
/** @var string the setting this is dependent on */
public $dependenton;
/** @var string the condition to show/hide the element */
public $condition;
/** @var string the value to compare against */
public $value;
/** @var string[] list of valid conditions */
private static $validconditions = ['checked', 'notchecked', 'noitemselected', 'eq', 'neq', 'in'];
/**
* admin_settingdependency constructor.
* @param string $settingname
* @param string $dependenton
* @param string $condition
* @param string $value
* @throws \coding_exception
*/
public function __construct($settingname, $dependenton, $condition, $value) {
$this->settingname = $this->parse_name($settingname);
$this->dependenton = $this->parse_name($dependenton);
$this->condition = $condition;
$this->value = $value;
if (!in_array($this->condition, self::$validconditions)) {
throw new coding_exception("Invalid condition '$condition'");
}
}
/**
* Convert the setting name into the form field name.
* @param string $name
* @return string
*/
private function parse_name($name) {
$bits = explode('/', $name);
$name = array_pop($bits);
$plugin = '';
if ($bits) {
$plugin = array_pop($bits);
if ($plugin === 'moodle') {
$plugin = '';
}
}
return 's_'.$plugin.'_'.$name;
}
/**
* Gather together all the dependencies in a format suitable for initialising javascript
* @param admin_settingdependency[] $dependencies
* @return array
*/
public static function prepare_for_javascript($dependencies) {
$result = [];
foreach ($dependencies as $d) {
if (!isset($result[$d->dependenton])) {
$result[$d->dependenton] = [];
}
if (!isset($result[$d->dependenton][$d->condition])) {
$result[$d->dependenton][$d->condition] = [];
}
if (!isset($result[$d->dependenton][$d->condition][$d->value])) {
$result[$d->dependenton][$d->condition][$d->value] = [];
}
$result[$d->dependenton][$d->condition][$d->value][] = $d->settingname;
}
return $result;
}
}
/**
* Used to group a number of admin_setting objects into a page and add them to the admin tree.
@ -1334,6 +1412,9 @@ class admin_settingpage implements part_of_admin_tree {
/** @var mixed An array of admin_setting objects that are part of this setting page. */
public $settings;
/** @var admin_settingdependency[] list of settings to hide when certain conditions are met */
protected $dependencies = [];
/** @var string The role capability/permission a user must have to access this external page. */
public $req_capability;
@ -1463,6 +1544,18 @@ class admin_settingpage implements part_of_admin_tree {
return true;
}
/**
* Hide the named setting if the specified condition is matched.
*
* @param string $settingname
* @param string $dependenton
* @param string $condition
* @param string $value
*/
public function hide_if($settingname, $dependenton, $condition = 'notchecked', $value = '1') {
$this->dependencies[] = new admin_settingdependency($settingname, $dependenton, $condition, $value);
}
/**
* see admin_externalpage
*
@ -1521,6 +1614,25 @@ class admin_settingpage implements part_of_admin_tree {
}
return false;
}
/**
* Should any of the settings on this page be shown / hidden based on conditions?
* @return bool
*/
public function has_dependencies() {
return (bool)$this->dependencies;
}
/**
* Format the setting show/hide conditions ready to initialise the page javascript
* @return array
*/
public function get_dependencies_for_javascript() {
if (!$this->has_dependencies()) {
return [];
}
return admin_settingdependency::prepare_for_javascript($this->dependencies);
}
}

1
lib/amd/build/showhidesettings.min.js vendored Normal file
View File

@ -0,0 +1 @@
define(["jquery"],function(a){function b(a){return a.is("input[type=hidden]")&&a.siblings('input[type=checkbox][name="'+a.attr("name")+'"]').length}function c(a,b){return a.is("input[type=radio]")&&a.attr("value")!==b}function d(a,d){return!b(a)&&!c(a,d)}function e(a){return a.is("input[type=radio]")&&!a.prop("checked")}function f(a){return a.is("input[type=checkbox]")&&!a.prop("checked")}function g(a){return a.is("select")&&a.prop("multiple")}function h(a,b){var c=a.val()||[];if(!b.length)return!1;if(c.length!==b.length)return!1;for(var d in c)if(c.hasOwnProperty(d)&&b.indexOf(c[d])===-1)return!1;return!0}function i(b){return a('[name="'+b+'"],[name="'+b+'[]"]')}function j(b){return a(b).attr("name").replace(/\[]/,"")}function k(b,c,d){return a.isFunction(o[c])?o[c](b,d):o.defaultCondition(b,d)}function l(b,c){c=c||j(b.currentTarget);var d=i(c);if(n.hasOwnProperty(c)){var e={};a.each(n[c],function(b,c){a.each(c,function(c,f){var g=k(d,b,c);a.each(f,function(a,b){e.hasOwnProperty(b)?e[b]=e[b]||g:e[b]=g})})}),a.each(e,function(b,c){i(b).each(function(b,d){var e=a(d).closest(".form-item");e.length&&(c?(e.attr("hidden","hidden"),e.css("display","none")):(e.removeAttr("hidden"),e.css("display","")))})})}}function m(){a.each(n,function(a){var b=i(a);b.length&&(b.on("change",l),l(null,a))})}var n,o={notchecked:function(b,c){var e=!1;return c=String(c),b.each(function(b,f){var g=a(f);d(g,c)&&(e=e||!g.prop("checked"))}),e},checked:function(b,c){var e=!1;return c=String(c),b.each(function(b,f){var g=a(f);d(g,c)&&(e=e||g.prop("checked"))}),e},noitemselected:function(b){var c=!1;return b.each(function(b,d){var e=a(d);c=c||e.prop("selectedIndex")===-1}),c},eq:function(c,d){var i=!1,j=!1;return d=String(d),c.each(function(c,k){var l=a(k);if(!e(l)){if(b(l))return void(j=l.val()===d);if(f(l))return void(i=i||j);if(g(l)){var m=d.split("|");return void(i=h(l,m))}i=i||l.val()===d}}),i},"in":function(c,d){var i=!1,j=!1,k=d.split("|");return c.each(function(c,d){var l=a(d);if(!e(l))return b(l)?void(j=k.indexOf(l.val())>-1):f(l)?void(i=i||j):g(l)?void(i=h(l,k)):void(i=i||k.indexOf(l.val())>-1)}),i},defaultCondition:function(c,d){var i=!1,j=!1;return d=String(d),c.each(function(c,k){var l=a(k);if(!e(l)){if(b(l))return void(j=l.val()!==d);if(f(l))return void(i=i||j);if(g(l)){var m=d.split("|");return void(i=!h(l,m))}i=i||l.val()!==d}}),i}};return{init:function(a){n=a.dependencies,m()}}});

View File

@ -0,0 +1,339 @@
/**
* Show/hide admin settings based on other settings selected
*
* @package core
* @copyright 2018 Davo Smith, Synergy Learning
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery'], function($) {
var dependencies;
// -------------------------------------------------
// Support functions, used by dependency functions.
// -------------------------------------------------
/**
* Check to see if the given element is the hidden element that makes sure checkbox
* elements always submit a value.
* @param {jQuery} $el
* @returns {boolean}
*/
function isCheckboxHiddenElement($el) {
return ($el.is('input[type=hidden]') && $el.siblings('input[type=checkbox][name="' + $el.attr('name') + '"]').length);
}
/**
* Check to see if this is a radio button with the wrong value (i.e. a radio button from
* the group we are interested in, but not the specific one we wanted).
* @param {jQuery} $el
* @param {string} value
* @returns {boolean}
*/
function isWrongRadioButton($el, value) {
return ($el.is('input[type=radio]') && $el.attr('value') !== value);
}
/**
* Is this element relevant when we're looking for checked / not checked status?
* @param {jQuery} $el
* @param {string} value
* @returns {boolean}
*/
function isCheckedRelevant($el, value) {
return (!isCheckboxHiddenElement($el) && !isWrongRadioButton($el, value));
}
/**
* Is this an unchecked radio button? (If it is, we want to skip it, as
* we're only interested in the value of the radio button that is checked)
* @param {jQuery} $el
* @returns {boolean}
*/
function isUncheckedRadioButton($el) {
return ($el.is('input[type=radio]') && !$el.prop('checked'));
}
/**
* Is this an unchecked checkbox?
* @param {jQuery} $el
* @returns {boolean}
*/
function isUncheckedCheckbox($el) {
return ($el.is('input[type=checkbox]') && !$el.prop('checked'));
}
/**
* Is this a multi-select select element?
* @param {jQuery} $el
* @returns {boolean}
*/
function isMultiSelect($el) {
return ($el.is('select') && $el.prop('multiple'));
}
/**
* Does the multi-select exactly match the list of values provided?
* @param {jQuery} $el
* @param {array} values
* @returns {boolean}
*/
function multiSelectMatches($el, values) {
var selected = $el.val() || [];
if (!values.length) {
// No values - nothing to match against.
return false;
}
if (selected.length !== values.length) {
// Different number of expected and actual values - cannot possibly be a match.
return false;
}
for (var i in selected) {
if (selected.hasOwnProperty(i)) {
if (values.indexOf(selected[i]) === -1) {
return false; // Found a non-matching value - give up immediately.
}
}
}
// Didn't find a non-matching value, so we have a match.
return true;
}
// -------------------------------
// Specific dependency functions.
// -------------------------------
var depFns = {
notchecked: function($dependon, value) {
var hide = false;
value = String(value);
$dependon.each(function(idx, el) {
var $el = $(el);
if (isCheckedRelevant($el, value)) {
hide = hide || !$el.prop('checked');
}
});
return hide;
},
checked: function($dependon, value) {
var hide = false;
value = String(value);
$dependon.each(function(idx, el) {
var $el = $(el);
if (isCheckedRelevant($el, value)) {
hide = hide || $el.prop('checked');
}
});
return hide;
},
noitemselected: function($dependon) {
var hide = false;
$dependon.each(function(idx, el) {
var $el = $(el);
hide = hide || ($el.prop('selectedIndex') === -1);
});
return hide;
},
eq: function($dependon, value) {
var hide = false;
var hiddenVal = false;
value = String(value);
$dependon.each(function(idx, el) {
var $el = $(el);
if (isUncheckedRadioButton($el)) {
// For radio buttons, we're only interested in the one that is checked.
return;
}
if (isCheckboxHiddenElement($el)) {
// This is the hidden input that is part of the checkbox setting.
// We will use this value, if the associated checkbox is unchecked.
hiddenVal = ($el.val() === value);
return;
}
if (isUncheckedCheckbox($el)) {
// Checkbox is not checked - hide depends on the 'unchecked' value stored in
// the associated hidden element, which we have already found, above.
hide = hide || hiddenVal;
return;
}
if (isMultiSelect($el)) {
// Expect a list of values to match, separated by '|' - all of them must
// match the values selected.
var values = value.split('|');
hide = multiSelectMatches($el, values);
return;
}
// All other element types - just compare the value directly.
hide = hide || ($el.val() === value);
});
return hide;
},
'in': function($dependon, value) {
var hide = false;
var hiddenVal = false;
var values = value.split('|');
$dependon.each(function(idx, el) {
var $el = $(el);
if (isUncheckedRadioButton($el)) {
// For radio buttons, we're only interested in the one that is checked.
return;
}
if (isCheckboxHiddenElement($el)) {
// This is the hidden input that is part of the checkbox setting.
// We will use this value, if the associated checkbox is unchecked.
hiddenVal = (values.indexOf($el.val()) > -1);
return;
}
if (isUncheckedCheckbox($el)) {
// Checkbox is not checked - hide depends on the 'unchecked' value stored in
// the associated hidden element, which we have already found, above.
hide = hide || hiddenVal;
return;
}
if (isMultiSelect($el)) {
// For multiselect, we check to see if the list of values provided matches the list selected.
hide = multiSelectMatches($el, values);
return;
}
// All other element types - check to see if the value is in the list.
hide = hide || (values.indexOf($el.val()) > -1);
});
return hide;
},
defaultCondition: function($dependon, value) { // Not equal.
var hide = false;
var hiddenVal = false;
value = String(value);
$dependon.each(function(idx, el) {
var $el = $(el);
if (isUncheckedRadioButton($el)) {
// For radio buttons, we're only interested in the one that is checked.
return;
}
if (isCheckboxHiddenElement($el)) {
// This is the hidden input that is part of the checkbox setting.
// We will use this value, if the associated checkbox is unchecked.
hiddenVal = ($el.val() !== value);
return;
}
if (isUncheckedCheckbox($el)) {
// Checkbox is not checked - hide depends on the 'unchecked' value stored in
// the associated hidden element, which we have already found, above.
hide = hide || hiddenVal;
return;
}
if (isMultiSelect($el)) {
// Expect a list of values to match, separated by '|' - all of them must
// match the values selected to *not* hide the element.
var values = value.split('|');
hide = !multiSelectMatches($el, values);
return;
}
// All other element types - just compare the value directly.
hide = hide || ($el.val() !== value);
});
return hide;
}
};
/**
* Find the element with the given name
* @param {String} name
* @returns {*|jQuery|HTMLElement}
*/
function getElementsByName(name) {
return $('[name="' + name + '"],[name="' + name + '[]"]');
}
/**
* Find the name of the given element
* @param {EventTarget} el
* @returns {String}
*/
function getElementName(el) {
return $(el).attr('name').replace(/\[]/, '');
}
/**
* Check to see whether a particular condition is met
* @param {*|jQuery|HTMLElement} $dependon
* @param {String} condition
* @param {mixed} value
* @returns {Boolean}
*/
function checkDependency($dependon, condition, value) {
if ($.isFunction(depFns[condition])) {
return depFns[condition]($dependon, value);
}
return depFns.defaultCondition($dependon, value);
}
/**
* Show / hide the elements that depend on the element(s) with the given name
* OR (if no dependonname given) the element(s) with the same name as the element that
* triggered the event e.
* @param {Event} e
* @param {String} dependonname (optional)
*/
function updateDependencies(e, dependonname) {
dependonname = dependonname || getElementName(e.currentTarget);
var $dependon = getElementsByName(dependonname);
if (!dependencies.hasOwnProperty(dependonname)) {
return;
}
// Process all dependency conditions related to the updated element.
var toHide = {};
$.each(dependencies[dependonname], function(condition, values) {
$.each(values, function(value, elements) {
var hide = checkDependency($dependon, condition, value);
$.each(elements, function(idx, elToHide) {
if (toHide.hasOwnProperty(elToHide)) {
toHide[elToHide] = toHide[elToHide] || hide;
} else {
toHide[elToHide] = hide;
}
});
});
});
// Update the hidden status of all relevant elements.
$.each(toHide, function(elToHide, hide) {
getElementsByName(elToHide).each(function(idx, el) {
var $parent = $(el).closest('.form-item');
if ($parent.length) {
if (hide) {
$parent.attr('hidden', 'hidden');
$parent.css('display', 'none');
} else {
$parent.removeAttr('hidden');
$parent.css('display', '');
}
}
});
});
}
/**
* Initialise the event handlers.
*/
function initHandlers() {
$.each(dependencies, function(depname) {
var $el = getElementsByName(depname);
if ($el.length) {
$el.on('change', updateDependencies);
updateDependencies(null, depname);
}
});
}
return {
init: function(opts) {
dependencies = opts.dependencies;
initHandlers();
}
};
});