diff --git a/lib/classes/output/choicelist.php b/lib/classes/output/choicelist.php index 20f6f37ebe5..02905a255f4 100644 --- a/lib/classes/output/choicelist.php +++ b/lib/classes/output/choicelist.php @@ -41,6 +41,9 @@ class choicelist implements renderable, named_templatable { /** @var string the choice description. */ protected $description = null; + /** @var bool if the selected value can be empty. */ + protected $allowempty = null; + /** * Constructor. * @@ -106,9 +109,37 @@ class choicelist implements renderable, named_templatable { * @return string|null The value of the selected option. */ public function get_selected_value(): ?string { + if (empty($this->selected) && !$this->allowempty && !empty($this->options)) { + return array_key_first($this->options); + } return $this->selected; } + /** + * Set the allow empty option. + * @param bool $allowempty Whether the selected value can be empty. + */ + public function set_allow_empty(bool $allowempty) { + $this->allowempty = $allowempty; + } + + /** + * Get the allow empty option. + * @return bool Whether the selected value can be empty. + */ + public function get_allow_empty(): bool { + return $this->allowempty; + } + + /** + * Check if the value is in the options. + * @param string $value The value to check. + * @return bool + */ + public function has_value(string $value): bool { + return isset($this->options[$value]); + } + /** * Set the general choice description option. * @@ -140,7 +171,9 @@ class choicelist implements renderable, named_templatable { } /** - * Set the option disabled. + * Sets the HTML attributes to the option. + * + * This method will remove any previous extra attributes. * * @param string $value The value of the option. * @param array $extras an array to add HTML attributes to the option (attribute => value). @@ -149,14 +182,48 @@ class choicelist implements renderable, named_templatable { if (!isset($this->options[$value])) { return; } - $extrasattributes = []; + $this->options[$value]['extras'] = []; + $this->add_option_extras($value, $extras); + } + + /** + * Add HTML attributes to the option. + * @param string $value The value of the option. + * @param array $extras an array to add HTML attributes to the option (attribute => value). + */ + public function add_option_extras(string $value, array $extras) { + if (!isset($this->options[$value])) { + return; + } + if (!isset($this->options[$value]['extras'])) { + $this->options[$value]['extras'] = []; + } foreach ($extras as $attribute => $attributevalue) { - $extrasattributes[] = [ + $this->options[$value]['extras'][] = [ 'attribute' => $attribute, 'value' => $attributevalue, ]; } - $this->options[$value]['extras'] = $extrasattributes; + } + + /** + * Get the selected option HTML. + * + * This method is used to display the selected option and the option icon. + * + * @param renderer_base $output The renderer. + * @return string + */ + public function get_selected_content(renderer_base $output): string { + if (empty($this->selected)) { + return ''; + } + $option = $this->options[$this->selected]; + $icon = ''; + if (!empty($option['icon'])) { + $icon = $output->render($option['icon']); + } + return $icon . $option['name']; } /** @@ -178,7 +245,7 @@ class choicelist implements renderable, named_templatable { } $option['hasurl'] = !empty($option['url']); - if ($option['value'] == $this->selected) { + if ($option['value'] == $this->get_selected_value()) { $option['selected'] = true; } diff --git a/lib/form/amd/build/choicedropdown.min.js b/lib/form/amd/build/choicedropdown.min.js new file mode 100644 index 00000000000..9ac51fd7445 --- /dev/null +++ b/lib/form/amd/build/choicedropdown.min.js @@ -0,0 +1,19 @@ +define("core_form/choicedropdown",["exports","core/local/dropdown/status","core_form/changechecker"],(function(_exports,_status,_changechecker){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; +/** + * Field controller for choicedropdown field. + * + * @module core_form/choicedropdown + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const Classes_hidden="d-none"; +/** + * Internal form element class. + * + * @private + * @class FieldController + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */class FieldController{constructor(elementId){this.elementId=elementId,this.mainSelect=document.getElementById(this.elementId),this.dropdown=(0,_status.getDropdownStatus)('[data-form-controls="'.concat(this.elementId,'"]')),this.dropdown.getElement().classList.remove(Classes_hidden)}addEventListeners(){this.dropdown.getElement().addEventListener("change",this.updateSelect.bind(this)),this.dropdown.getElement().addEventListener("click",(event=>event.preventDefault())),this.mainSelect.addEventListener("change",this.updateDropdown.bind(this));new MutationObserver((mutations=>{mutations.forEach((mutation=>{"attributes"===mutation.type&&"disabled"===mutation.attributeName&&this.updateDropdown()}))})).observe(this.mainSelect,{attributeFilter:["disabled"]})}isDisabled(){var _this$mainSelect;return null===(_this$mainSelect=this.mainSelect)||void 0===_this$mainSelect?void 0:_this$mainSelect.hasAttribute("disabled")}async updateDropdown(){this.dropdown.setButtonDisabled(this.isDisabled()),this.dropdown.getSelectedValue()!=this.mainSelect.value&&this.dropdown.setSelectedValue(this.mainSelect.value)}async updateSelect(){this.dropdown.getSelectedValue()!=this.mainSelect.value&&(this.mainSelect.value=this.dropdown.getSelectedValue(),(0,_changechecker.markFormAsDirty)(this.mainSelect.closest("form")),this.mainSelect.dispatchEvent(new Event("change")))}disableInteractiveDialog(){var _this$mainSelect2;null===(_this$mainSelect2=this.mainSelect)||void 0===_this$mainSelect2||_this$mainSelect2.classList.remove(Classes_hidden);this.dropdown.getElement().classList.add(Classes_hidden)}hasForceDialog(){var _this$mainSelect3;return!(null===(_this$mainSelect3=this.mainSelect)||void 0===_this$mainSelect3||!_this$mainSelect3.dataset.forceDialog)}}_exports.init=elementId=>{const field=new FieldController(elementId);!document.body.classList.contains("behat-site")||field.hasForceDialog()?field.addEventListeners():field.disableInteractiveDialog()}})); + +//# sourceMappingURL=choicedropdown.min.js.map \ No newline at end of file diff --git a/lib/form/amd/build/choicedropdown.min.js.map b/lib/form/amd/build/choicedropdown.min.js.map new file mode 100644 index 00000000000..8e2d0437a09 --- /dev/null +++ b/lib/form/amd/build/choicedropdown.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"choicedropdown.min.js","sources":["../src/choicedropdown.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Field controller for choicedropdown field.\n *\n * @module core_form/choicedropdown\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getDropdownStatus} from 'core/local/dropdown/status';\nimport {markFormAsDirty} from 'core_form/changechecker';\n\nconst Classes = {\n notClickable: 'not-clickable',\n hidden: 'd-none',\n};\n\n/**\n * Internal form element class.\n *\n * @private\n * @class FieldController\n * @copyright 2023 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nclass FieldController {\n /**\n * Class constructor.\n *\n * @param {String} elementId Form element id\n */\n constructor(elementId) {\n this.elementId = elementId;\n this.mainSelect = document.getElementById(this.elementId);\n this.dropdown = getDropdownStatus(`[data-form-controls=\"${this.elementId}\"]`);\n this.dropdown.getElement().classList.remove(Classes.hidden);\n }\n\n /**\n * Add form element event listener.\n */\n addEventListeners() {\n this.dropdown.getElement().addEventListener(\n 'change',\n this.updateSelect.bind(this)\n );\n // Click on a dropdown link can trigger a wrong dirty form reload warning.\n this.dropdown.getElement().addEventListener(\n 'click',\n (event) => event.preventDefault()\n );\n this.mainSelect.addEventListener(\n 'change',\n this.updateDropdown.bind(this)\n );\n // Enabling or disabling the select does not trigger any JS event.\n const observerCallback = (mutations) => {\n mutations.forEach((mutation) => {\n if (mutation.type !== 'attributes' || mutation.attributeName !== 'disabled') {\n return;\n }\n this.updateDropdown();\n });\n };\n new MutationObserver(observerCallback).observe(\n this.mainSelect,\n {attributeFilter: ['disabled']}\n );\n }\n\n /**\n * Check if the field is disabled.\n * @returns {Boolean}\n */\n isDisabled() {\n return this.mainSelect?.hasAttribute('disabled');\n }\n\n /**\n * Update selected option preview in form.\n */\n async updateDropdown() {\n this.dropdown.setButtonDisabled(this.isDisabled());\n if (this.dropdown.getSelectedValue() == this.mainSelect.value) {\n return;\n }\n this.dropdown.setSelectedValue(this.mainSelect.value);\n }\n\n /**\n * Update selected option preview in form.\n */\n async updateSelect() {\n if (this.dropdown.getSelectedValue() == this.mainSelect.value) {\n return;\n }\n this.mainSelect.value = this.dropdown.getSelectedValue();\n markFormAsDirty(this.mainSelect.closest('form'));\n // Change the select element via JS does not trigger the standard change event.\n this.mainSelect.dispatchEvent(new Event('change'));\n }\n\n /**\n * Disable the choice dialog and convert it into a regular select field.\n */\n disableInteractiveDialog() {\n this.mainSelect?.classList.remove(Classes.hidden);\n const dropdownElement = this.dropdown.getElement();\n dropdownElement.classList.add(Classes.hidden);\n }\n\n /**\n * Check if the field has a force dialog attribute.\n // *\n * The force dialog is a setting to force the javascript control even in\n * behat test.\n *\n * @returns {Boolean} if the dialog modal should be forced or not\n */\n hasForceDialog() {\n return !!this.mainSelect?.dataset.forceDialog;\n }\n}\n\n/**\n * Initialises a choice dialog field.\n *\n * @method init\n * @param {String} elementId Form element id\n * @listens event:uploadStarted\n * @listens event:uploadCompleted\n */\nexport const init = (elementId) => {\n const field = new FieldController(elementId);\n // This field is just a select wrapper. To optimize tests, we don't want to keep behat\n // waiting for extra loadings in this case. The set field steps are about testing other\n // stuff, not to test fancy javascript form fields. However, we keep the possibility of\n // testing the javascript part using behat when necessary.\n if (document.body.classList.contains('behat-site') && !field.hasForceDialog()) {\n field.disableInteractiveDialog();\n return;\n }\n field.addEventListeners();\n};\n"],"names":["Classes","FieldController","constructor","elementId","mainSelect","document","getElementById","this","dropdown","getElement","classList","remove","addEventListeners","addEventListener","updateSelect","bind","event","preventDefault","updateDropdown","MutationObserver","mutations","forEach","mutation","type","attributeName","observe","attributeFilter","isDisabled","_this$mainSelect","hasAttribute","setButtonDisabled","getSelectedValue","value","setSelectedValue","closest","dispatchEvent","Event","disableInteractiveDialog","add","hasForceDialog","_this$mainSelect3","dataset","forceDialog","field","body","contains"],"mappings":";;;;;;;;MA0BMA,eAEM;;;;;;;;WAWNC,gBAMFC,YAAYC,gBACHA,UAAYA,eACZC,WAAaC,SAASC,eAAeC,KAAKJ,gBAC1CK,UAAW,4DAA0CD,KAAKJ,sBAC1DK,SAASC,aAAaC,UAAUC,OAAOX,gBAMhDY,yBACSJ,SAASC,aAAaI,iBACvB,SACAN,KAAKO,aAAaC,KAAKR,YAGtBC,SAASC,aAAaI,iBACvB,SACCG,OAAUA,MAAMC,wBAEhBb,WAAWS,iBACZ,SACAN,KAAKW,eAAeH,KAAKR,WAWzBY,kBARsBC,YACtBA,UAAUC,SAASC,WACO,eAAlBA,SAASC,MAAoD,aAA3BD,SAASE,oBAG1CN,uBAG0BO,QACnClB,KAAKH,WACL,CAACsB,gBAAiB,CAAC,cAQ3BC,kEACWpB,KAAKH,8CAALwB,iBAAiBC,aAAa,wCAOhCrB,SAASsB,kBAAkBvB,KAAKoB,cACjCpB,KAAKC,SAASuB,oBAAsBxB,KAAKH,WAAW4B,YAGnDxB,SAASyB,iBAAiB1B,KAAKH,WAAW4B,4BAO3CzB,KAAKC,SAASuB,oBAAsBxB,KAAKH,WAAW4B,aAGnD5B,WAAW4B,MAAQzB,KAAKC,SAASuB,sDACtBxB,KAAKH,WAAW8B,QAAQ,cAEnC9B,WAAW+B,cAAc,IAAIC,MAAM,YAM5CC,gFACSjC,2DAAYM,UAAUC,OAAOX,gBACVO,KAAKC,SAASC,aACtBC,UAAU4B,IAAItC,gBAWlCuC,yEACahC,KAAKH,0CAALoC,kBAAiBC,QAAQC,4BAYrBvC,kBACXwC,MAAQ,IAAI1C,gBAAgBE,YAK9BE,SAASuC,KAAKlC,UAAUmC,SAAS,eAAkBF,MAAMJ,iBAI7DI,MAAM/B,oBAHF+B,MAAMN"} \ No newline at end of file diff --git a/lib/form/amd/src/choicedropdown.js b/lib/form/amd/src/choicedropdown.js new file mode 100644 index 00000000000..71da5974fc1 --- /dev/null +++ b/lib/form/amd/src/choicedropdown.js @@ -0,0 +1,158 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Field controller for choicedropdown field. + * + * @module core_form/choicedropdown + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {getDropdownStatus} from 'core/local/dropdown/status'; +import {markFormAsDirty} from 'core_form/changechecker'; + +const Classes = { + notClickable: 'not-clickable', + hidden: 'd-none', +}; + +/** + * Internal form element class. + * + * @private + * @class FieldController + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class FieldController { + /** + * Class constructor. + * + * @param {String} elementId Form element id + */ + constructor(elementId) { + this.elementId = elementId; + this.mainSelect = document.getElementById(this.elementId); + this.dropdown = getDropdownStatus(`[data-form-controls="${this.elementId}"]`); + this.dropdown.getElement().classList.remove(Classes.hidden); + } + + /** + * Add form element event listener. + */ + addEventListeners() { + this.dropdown.getElement().addEventListener( + 'change', + this.updateSelect.bind(this) + ); + // Click on a dropdown link can trigger a wrong dirty form reload warning. + this.dropdown.getElement().addEventListener( + 'click', + (event) => event.preventDefault() + ); + this.mainSelect.addEventListener( + 'change', + this.updateDropdown.bind(this) + ); + // Enabling or disabling the select does not trigger any JS event. + const observerCallback = (mutations) => { + mutations.forEach((mutation) => { + if (mutation.type !== 'attributes' || mutation.attributeName !== 'disabled') { + return; + } + this.updateDropdown(); + }); + }; + new MutationObserver(observerCallback).observe( + this.mainSelect, + {attributeFilter: ['disabled']} + ); + } + + /** + * Check if the field is disabled. + * @returns {Boolean} + */ + isDisabled() { + return this.mainSelect?.hasAttribute('disabled'); + } + + /** + * Update selected option preview in form. + */ + async updateDropdown() { + this.dropdown.setButtonDisabled(this.isDisabled()); + if (this.dropdown.getSelectedValue() == this.mainSelect.value) { + return; + } + this.dropdown.setSelectedValue(this.mainSelect.value); + } + + /** + * Update selected option preview in form. + */ + async updateSelect() { + if (this.dropdown.getSelectedValue() == this.mainSelect.value) { + return; + } + this.mainSelect.value = this.dropdown.getSelectedValue(); + markFormAsDirty(this.mainSelect.closest('form')); + // Change the select element via JS does not trigger the standard change event. + this.mainSelect.dispatchEvent(new Event('change')); + } + + /** + * Disable the choice dialog and convert it into a regular select field. + */ + disableInteractiveDialog() { + this.mainSelect?.classList.remove(Classes.hidden); + const dropdownElement = this.dropdown.getElement(); + dropdownElement.classList.add(Classes.hidden); + } + + /** + * Check if the field has a force dialog attribute. + // * + * The force dialog is a setting to force the javascript control even in + * behat test. + * + * @returns {Boolean} if the dialog modal should be forced or not + */ + hasForceDialog() { + return !!this.mainSelect?.dataset.forceDialog; + } +} + +/** + * Initialises a choice dialog field. + * + * @method init + * @param {String} elementId Form element id + * @listens event:uploadStarted + * @listens event:uploadCompleted + */ +export const init = (elementId) => { + const field = new FieldController(elementId); + // This field is just a select wrapper. To optimize tests, we don't want to keep behat + // waiting for extra loadings in this case. The set field steps are about testing other + // stuff, not to test fancy javascript form fields. However, we keep the possibility of + // testing the javascript part using behat when necessary. + if (document.body.classList.contains('behat-site') && !field.hasForceDialog()) { + field.disableInteractiveDialog(); + return; + } + field.addEventListeners(); +}; diff --git a/lib/form/choicedropdown.php b/lib/form/choicedropdown.php new file mode 100644 index 00000000000..e01a752ac65 --- /dev/null +++ b/lib/form/choicedropdown.php @@ -0,0 +1,210 @@ +. + +defined('MOODLE_INTERNAL') || die(); + +use core\output\choicelist; +use core\output\local\dropdown\status; + +require_once('HTML/QuickForm/select.php'); +require_once('templatable_form_element.php'); + +/** + * User choice using a dropdown type form element. + * + * @package core_form + * @category form + * @copyright 2023 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class MoodleQuickForm_choicedropdown extends HTML_QuickForm_select implements templatable { + + use templatable_form_element { + export_for_template as export_for_template_base; + } + + /** + * @var string html for help button, if empty then no help. + */ + protected string $_helpbutton = ''; + + /** + * @var bool if true label will be hidden. + */ + protected bool $_hiddenLabel = false; + + /** + * @var choicelist the user choices. + */ + protected ?choicelist $choice = null; + + /** + * @var string[] Dropdown dialog width. + */ + public const WIDTH = status::WIDTH; + + /** + * @var string the dropdown width (from core\output\local\dropdown\status::WIDTH). + */ + protected string $dropdownwidth = status::WIDTH['small']; + + /** + * Constructor. + * + * @param string $elementname Select name attribute + * @param mixed $elementlabel Label(s) for the select + * @param choicelist $options Data to be used to populate options + * @param mixed $attributes Either a typical HTML attribute string or an associative array + */ + public function __construct( + $elementname = null, + $elementlabel = null, + choicelist $options = null, + $attributes = null + ) { + parent::__construct($elementname, $elementlabel, $options, $attributes); + $this->_type = 'choicedropdown'; + } + + /** + * Set the dropdown width. + * + * @param string $width + */ + public function set_dialog_width(string $width) { + $this->dropdownwidth = $width; + } + + /** + * Loads options from a choicelist. + * + * @param choicelist $choice Options source currently supports assoc array or DB_result + * @param string|null $value optional value (in case it is not defined in the choicelist) + * @param string|null $unused2 unused + * @param string|null $unused3 unused + * @param string|null $unused4 unused + * @return bool + */ + public function load(&$choice, $value = null, $unused2 = null, $unused3 = null, $unused4 = null): bool { + if (!$choice instanceof choicelist) { + throw new coding_exception('Choice must be instance of choicelist'); + } + $this->choice = $choice; + $this->choice->set_allow_empty(false); + if ($value !== null && is_string($value)) { + $choice->set_selected_value($value); + } + $this->setSelected($choice->get_selected_value()); + return true; + } + + /** + * Sets label to be hidden + * + * @param bool $hiddenLabel sets if label should be hidden + */ + public function setHiddenLabel($hiddenLabel) { + $this->_hiddenLabel = $hiddenLabel; + } + + /** + * Returns HTML for select form element. + * + * This method is only needed when forms renderer is forces via + * $GLOBALS['_HTML_QuickForm_default_renderer']. Otherwise the + * renderer will use mustache templates. + * + * @return string + */ + public function toHtml(): string { + $html = ''; + if ($this->_hiddenLabel) { + $this->_generateId(); + $html .= ''; + } + $html .= parent::toHtml(); + return $html; + } + + /** + * get html for help button + * + * @return string html for help button + */ + public function getHelpButton(): string { + return $this->_helpbutton; + } + + /** + * Slightly different container template when frozen. Don't want to use a label tag + * with a for attribute in that case for the element label but instead use a div. + * Templates are defined in renderer constructor. + * + * @return string + */ + public function getElementTemplateType(): string { + if ($this->_flagFrozen) { + return 'static'; + } else { + return 'default'; + } + } + + /** + * We check the options and return only the values that _could_ have been + * selected. We also return a scalar value if select is not "multiple" + * + * @param string $submitvalues submitted values + * @param bool $assoc if true the returned value is associated array + * @return string|null + */ + public function exportValue(&$submitvalues, $assoc = false) { + $value = $this->_findValue($submitvalues) ?? $this->getValue(); + if (is_array($value)) { + $value = reset($value); + } + if ($value === null) { + return $this->_prepareValue($value, $assoc); + } + if (!$this->choice->has_value($value)) { + $value = $this->choice->get_selected_value(); + } + return $this->_prepareValue($value, $assoc); + } + + public function export_for_template(renderer_base $output): array { + $context = $this->export_for_template_base($output); + + if (!empty($this->_values)) { + $this->choice->set_selected_value(reset($this->_values)); + } + + $dialog = new status( + $this->choice->get_selected_content($output), + $this->choice, + [ + 'extras' => ['data-form-controls' => $context['id']], + 'buttonsync' => true, + 'updatestatus' => true, + 'dialogwidth' => $this->dropdownwidth, + ] + ); + $context['dropdown'] = $dialog->export_for_template($output); + $context['select'] = $this->choice->export_for_template($output); + $context['nameraw'] = $this->getName(); + return $context; + } +} diff --git a/lib/form/templates/element-choicedropdown.mustache b/lib/form/templates/element-choicedropdown.mustache new file mode 100644 index 00000000000..21930f16564 --- /dev/null +++ b/lib/form/templates/element-choicedropdown.mustache @@ -0,0 +1,149 @@ +{{! + This file is part of Moodle - http://moodle.org/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_form/element-choicedropdown + Choice dialog form element template. + Context variables required for this template: + * id - Element id, + * nameraw - Raw Element name without '[]' + * name - Element name, + * label - Element label, + * multiple - multi select?, + * checked - checked?, + * error - Is there an error associated with this element?, + * size - Element size, + * value - Element value, + * helpbutton - Helpbutton, + * hiddenlabel - Element hidden flag, + * frozen - Element frozen flag, + * hardfrozen - Element hard fronzen flag, + * extraclasses - Extra classes assocaited, + * type - Element type, + * attributes - Element attributes, + * options - [ + { + text - Option text, + value - Option value, + selected - Selected?, + disabled - Disabled?, + optionattributes - Option attributes + } + ] + + Example context (json): + { + "element": { + "wrapperid": "fitem_id_choicedropdownfield", + "iderror": "error_id_choicedropdownfield", + "id": "id_choicedropdownfield", + "nameraw": "choicedropdownfield", + "name": "choicedropdownfield", + "label": null, + "multiple": null, + "checked": null, + "error": null, + "size": null, + "value": null, + "helpbutton": "", + "hiddenlabel": false, + "frozen": false, + "hardfrozen": false, + "extraclasses": null, + "type": "select", + "attributes": "", + "fieldicon": "", + "options": [ + { + "rawoptionid": "1", + "text": "Hidden", + "value": 0, + "selected": false, + "disabled": false, + "optionattributes": "" + }, + { + "rawoptionid": "2", + "text": "Visible to everyone", + "value": 1, + "selected": true, + "disabled": false, + "optionattributes": "" + }, + { + "rawoptionid": "3", + "text": "Allow only other course members to see my email address", + "value": 2, + "selected": false, + "disabled": false, + "optionattributes": "" + } + ] + } + } +}} +{{< core_form/element-template }} + {{$element}} + {{^element.frozen}} + + {{#element.dropdown}} + {{< core/local/dropdown/status}} + {{$ buttonclasses }} btn btn-outline-secondary dropdown-toggle {{/ buttonclasses }} + {{/ core/local/dropdown/status}} + {{/element.dropdown}} + {{/element.frozen}} + {{#element.frozen}} + {{#element.options}} + {{#selected}} + {{{text}}} + {{^element.hardfrozen}} + + {{/element.hardfrozen}} + {{/selected}} + {{/element.options}} + {{/element.frozen}} + {{/element}} +{{/ core_form/element-template }} +{{#js}} + require(['core_form/choicedropdown'], function(ChioceDropdown) { + ChioceDropdown.init('{{element.id}}'); + }); +{{/js}} diff --git a/lib/form/tests/behat/choicedropdown.feature b/lib/form/tests/behat/choicedropdown.feature new file mode 100644 index 00000000000..3dce5f549e9 --- /dev/null +++ b/lib/form/tests/behat/choicedropdown.feature @@ -0,0 +1,42 @@ +@core +Feature: Choice dropdown form behat test + In order to use choicelist in quickforms + As an admin + I need to be able to test it via behat + + Background: + Given I log in as "admin" + And I am on fixture page "/lib/form/tests/behat/fixtures/field_choicedropdown_testpage.php" + + Scenario: Set some value into choice dropdown + When I set the field "Basic choice dropdown" to "Text option 2" + And I click on "Send form" "button" + Then I should see "example0: option2" in the "submitted_data" "region" + + @javascript + Scenario: Set some value into choice dropdown with javascript enabled + When I set the field "Basic choice dropdown" to "Text option 2" + And I click on "Send form" "button" + Then I should see "example0: option2" in the "submitted_data" "region" + + @javascript + Scenario: Disable choice dropdown via javascript + When I click on "Check to disable the first choice dropdown field." "checkbox" + Then the "Disable if example" "field" should be disabled + + @javascript + Scenario: Hide choice dropdown via javascript + Given I should see "Hide if example" + When I click on "Check to hide the first choice dropdown field." "checkbox" + Then I should not see "Hide if example" + + @javascript + Scenario: Use a choice dropdown to disable and hide other fields + Given I should not see "Hide if element" + And the "Disabled if element" "field" should be disabled + When I set the field "Control choice dropdown" to "Show or enable subelements" + Then I should see "Hide if element" + And the "Disabled if element" "field" should be enabled + And I set the field "Control choice dropdown" to "Hide or disable subelements" + And I should not see "Hide if element" + And the "Disabled if element" "field" should be disabled diff --git a/lib/form/tests/behat/fixtures/field_choicedropdown_testpage.php b/lib/form/tests/behat/fixtures/field_choicedropdown_testpage.php new file mode 100644 index 00000000000..41f76fe0097 --- /dev/null +++ b/lib/form/tests/behat/fixtures/field_choicedropdown_testpage.php @@ -0,0 +1,117 @@ +. + +/** + * Test page for choice dropdown field type. + * + * @copyright 2023 Ferran Recio + * @package core_form + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use core\output\choicelist; + +require_once(__DIR__ . '/../../../../../config.php'); + +defined('BEHAT_SITE_RUNNING') || die(); + +global $CFG, $PAGE, $OUTPUT; +require_once($CFG->libdir . '/formslib.php'); +$PAGE->set_url('/lib/form/tests/behat/fixtures/field_choicedropdown_testpage.php'); +$PAGE->add_body_class('limitedwidth'); +require_login(); +$PAGE->set_context(core\context\system::instance()); + +/** + * Class test_choice_dropdown + * @package core_form + */ +class test_choice_dropdown extends moodleform { + /** + * Define the export form. + */ + public function definition() { + $mform = $this->_form; + + $options = new choicelist(); + $options->set_allow_empty(false); + $options->add_option('option1', "Text option 1", [ + 'description' => 'Option 1 description', + 'icon' => new pix_icon('t/hide', 'Eye icon 1'), + ]); + $options->add_option('option2', "Text option 2", [ + 'description' => 'Option 2 description', + 'icon' => new pix_icon('t/stealth', 'Eye icon 2'), + ]); + $options->add_option('option3', "Text option 3", [ + 'description' => 'Option 3 description', + 'icon' => new pix_icon('t/show', 'Eye icon 3'), + ]); + + $mform->addElement('header', 'database', "Basic example"); + $mform->addElement('choicedropdown', 'example0', "Basic choice dropdown", $options); + + $mform->addElement('header', 'database', "Disable choice dropdown"); + $mform->addElement('checkbox', 'disableme', 'Check to disable the first choice dropdown field.'); + $mform->addElement('choicedropdown', 'example1', "Disable if example", $options); + $mform->disabledIf('example1', 'disableme', 'checked'); + + $mform->addElement('header', 'database', "Hide choice dropdown"); + $mform->addElement('checkbox', 'hideme', 'Check to hide the first choice dropdown field.'); + $mform->addElement('choicedropdown', 'example2', "Hide if example", $options); + $mform->hideIf('example2', 'hideme', 'checked'); + + $options = new choicelist(); + $options->set_allow_empty(false); + $options->add_option('hide', 'Hide or disable subelements'); + $options->add_option('show', 'Show or enable subelements'); + + $mform->addElement('header', 'database', "Use choice dropdown to hide or disable other fields"); + $mform->addElement('choicedropdown', 'example3', "Control choice dropdown", $options); + + $mform->addElement('text', 'hideinput', 'Hide if element', ['maxlength' => 80, 'size' => 50]); + $mform->hideIf('hideinput', 'example3', 'eq', 'hide'); + $mform->setDefault('hideinput', 'Is this visible?'); + $mform->setType('hideinput', PARAM_TEXT); + + $mform->addElement('text', 'disabledinput', 'Disabled if element', ['maxlength' => 80, 'size' => 50]); + $mform->disabledIf('disabledinput', 'example3', 'eq', 'hide'); + $mform->setDefault('disabledinput', 'Is this enabled?'); + $mform->setType('disabledinput', PARAM_TEXT); + + $this->add_action_buttons(false, 'Send form'); + } +} + +echo $OUTPUT->header(); + +echo "

Quickform integration test

"; + +$form = new test_choice_dropdown(); + +$data = $form->get_data(); +if ($data) { + echo "

Submitted data

"; + echo '
    '; + $data = (array) $data; + foreach ($data as $field => $value) { + echo "
  • $field: $value
  • "; + } + echo '
'; +} +$form->display(); + +echo $OUTPUT->footer(); diff --git a/lib/formslib.php b/lib/formslib.php index 5ed5d2a53df..1e6b760f16b 100644 --- a/lib/formslib.php +++ b/lib/formslib.php @@ -3683,6 +3683,7 @@ MoodleQuickForm::registerElementType('course', "$CFG->libdir/form/course.php", ' MoodleQuickForm::registerElementType('cohort', "$CFG->libdir/form/cohort.php", 'MoodleQuickForm_cohort'); MoodleQuickForm::registerElementType('searchableselector', "$CFG->libdir/form/searchableselector.php", 'MoodleQuickForm_searchableselector'); MoodleQuickForm::registerElementType('checkbox', "$CFG->libdir/form/checkbox.php", 'MoodleQuickForm_checkbox'); +MoodleQuickForm::registerElementType('choicedropdown', "$CFG->libdir/form/choicedropdown.php", 'MoodleQuickForm_choicedropdown'); MoodleQuickForm::registerElementType('date_selector', "$CFG->libdir/form/dateselector.php", 'MoodleQuickForm_date_selector'); MoodleQuickForm::registerElementType('date_time_selector', "$CFG->libdir/form/datetimeselector.php", 'MoodleQuickForm_date_time_selector'); MoodleQuickForm::registerElementType('duration', "$CFG->libdir/form/duration.php", 'MoodleQuickForm_duration'); diff --git a/lib/tests/output/choicelist_test.php b/lib/tests/output/choicelist_test.php index 145766522f3..3742aa99271 100644 --- a/lib/tests/output/choicelist_test.php +++ b/lib/tests/output/choicelist_test.php @@ -192,4 +192,44 @@ class choicelist_test extends advanced_testcase { $this->assertFalse(isset($option['extras'])); } } + + /** + * Test for a choice with option selected. + * + * @covers ::_construct + * @covers ::add_option + * @covers ::set_selected_value + * @covers ::get_selected_value + * @covers ::set_allow_empty + * @covers ::get_allow_empty + * @covers ::export_for_template + */ + public function test_set_allow_empty(): void { + $choice = new choicelist('Choose an option'); + $choice->add_option('option1', 'Option 1'); + $choice->add_option('option2', 'Option 2'); + + $choice->set_allow_empty(true); + $this->assertTrue($choice->get_allow_empty()); + $this->assertNull($choice->get_selected_value()); + + $choice->set_allow_empty(false); + $this->assertFalse($choice->get_allow_empty()); + $this->assertEquals('option1', $choice->get_selected_value()); + + // Validate the null selected value is not changed when allow empty is set to true. + $choice->set_allow_empty(true); + $this->assertTrue($choice->get_allow_empty()); + $this->assertNull($choice->get_selected_value()); + + $choice->set_selected_value('option2'); + + $choice->set_allow_empty(false); + $this->assertFalse($choice->get_allow_empty()); + $this->assertEquals('option2', $choice->get_selected_value()); + + $choice->set_allow_empty(true); + $this->assertTrue($choice->get_allow_empty()); + $this->assertEquals('option2', $choice->get_selected_value()); + } }