MDL-79985 output: prepare dropdown components for forms

Add new features to the dropdown components (dialog and status) needed
for using them in quick forms. Now the dialog dropdown have methods to
disable the dropdown button (to disable the field). The status dropdown
has one minor style bugfix and the selected item styles are now stored
as a data attribute.
This commit is contained in:
Ferran Recio
2023-12-19 09:33:45 +01:00
parent 14414fe253
commit 02de5b79fd
10 changed files with 42 additions and 9 deletions
@@ -232,6 +232,7 @@ Both classes provide the following methods:
- `setVisible(Boolean)` to open or close the dropdown.
- `isVisible()` to know if it is open or closed.
- `setButtonContent(String)` to replace the button content.
- `setButtonDisabled(Boolean)` to disable or enable the dropdown button.
- `getElement()`to get the main HTMLElement to add eventListeners.
The following example uses the module to open the dropdown when an extra button is preset:
+1 -1
View File
@@ -5,6 +5,6 @@ define("core/local/dropdown/dialog",["exports","jquery","core/pagehelpers","core
* @module core/local/dropdown/dialog
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.getDropdownDialog=_exports.DropdownDialog=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending);const Selectors_dropdownButton='[data-for="dropdowndialog_button"]',Selectors_dropdownDialog='[data-for="dropdowndialog_dialog"]';class DropdownDialog{constructor(element){this.element=element,this.button=element.querySelector(Selectors_dropdownButton),this.panel=element.querySelector(Selectors_dropdownDialog)}init(){this.element.dataset.dropdownDialogInitialized||(this.button.addEventListener("keydown",this._buttonKeyHandler.bind(this)),this.panel.addEventListener("keydown",this._contentKeyHandler.bind(this)),this.element.dataset.dropdownDialogInitialized=!0)}_buttonKeyHandler(event){if("ArrowUp"===event.key||"ArrowLeft"===event.key)return event.stopPropagation(),event.preventDefault(),void this.setVisible(!1);"ArrowDown"!==event.key&&"ArrowRight"!==event.key||(event.stopPropagation(),event.preventDefault(),this.setVisible(!0),this._focusPanelContent())}_contentKeyHandler(event){let newFocus=null;"End"===event.key&&(newFocus=(0,_pagehelpers.lastFocusableElement)(this.panel)),"Home"===event.key&&(newFocus=(0,_pagehelpers.firstFocusableElement)(this.panel)),"ArrowUp"!==event.key&&"ArrowLeft"!==event.key||(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panel,!1),newFocus||(newFocus=this.button)),"ArrowDown"!==event.key&&"ArrowRight"!==event.key||(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panel,!1)),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/dropdown/dialog:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panel);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisible(visible){visible!==this.isVisible()&&(0,_jquery.default)(this.button).dropdown("toggle")}isVisible(){return"true"===this.button.getAttribute("aria-expanded")}setButtonContent(content){this.button.innerHTML=content}getElement(){return this.element}}_exports.DropdownDialog=DropdownDialog;const getDropdownDialog=selector=>{const dropdownElement=document.querySelector(selector);return dropdownElement?new DropdownDialog(dropdownElement):null};_exports.getDropdownDialog=getDropdownDialog;_exports.init=selector=>{const dropdown=getDropdownDialog(selector);if(!dropdown)throw new Error("Dopdown dialog element not found: ".concat(selector));dropdown.init()}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.getDropdownDialog=_exports.DropdownDialog=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending);const Selectors_dropdownButton='[data-for="dropdowndialog_button"]',Selectors_dropdownDialog='[data-for="dropdowndialog_dialog"]';class DropdownDialog{constructor(element){this.element=element,this.button=element.querySelector(Selectors_dropdownButton),this.panel=element.querySelector(Selectors_dropdownDialog)}init(){this.element.dataset.dropdownDialogInitialized||(this.button.addEventListener("keydown",this._buttonKeyHandler.bind(this)),this.panel.addEventListener("keydown",this._contentKeyHandler.bind(this)),this.element.dataset.dropdownDialogInitialized=!0)}_buttonKeyHandler(event){if("ArrowUp"===event.key||"ArrowLeft"===event.key)return event.stopPropagation(),event.preventDefault(),void this.setVisible(!1);"ArrowDown"!==event.key&&"ArrowRight"!==event.key||(event.stopPropagation(),event.preventDefault(),this.setVisible(!0),this._focusPanelContent())}_contentKeyHandler(event){let newFocus=null;"End"===event.key&&(newFocus=(0,_pagehelpers.lastFocusableElement)(this.panel)),"Home"===event.key&&(newFocus=(0,_pagehelpers.firstFocusableElement)(this.panel)),"ArrowUp"!==event.key&&"ArrowLeft"!==event.key||(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panel,!1),newFocus||(newFocus=this.button)),"ArrowDown"!==event.key&&"ArrowRight"!==event.key||(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panel,!1)),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/dropdown/dialog:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panel);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisible(visible){visible!==this.isVisible()&&(0,_jquery.default)(this.button).dropdown("toggle")}isVisible(){return"true"===this.button.getAttribute("aria-expanded")}setButtonContent(content){this.button.innerHTML=content}setButtonDisabled(disabled){disabled?this.button.setAttribute("disabled","disabled"):this.button.removeAttribute("disabled")}getElement(){return this.element}}_exports.DropdownDialog=DropdownDialog;const getDropdownDialog=selector=>{const dropdownElement=document.querySelector(selector);return dropdownElement?new DropdownDialog(dropdownElement):null};_exports.getDropdownDialog=getDropdownDialog;_exports.init=selector=>{const dropdown=getDropdownDialog(selector);if(!dropdown)throw new Error("Dopdown dialog element not found: ".concat(selector));dropdown.init()}}));
//# sourceMappingURL=dialog.min.js.map
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,10 +7,10 @@ define("core/local/dropdown/status",["exports","core/local/dropdown/dialog"],(fu
* - Update the status of the button when the selected option changes. This will
* trigger a "change" event when the status changes.
*
* @module core/local/dropdown/dialog
* @module core/local/dropdown/status
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Selectors_checkedIcon='[data-for="checkedIcon"]',Selectors_option='[role="option"]',Selectors_optionItem="[data-optionnumber]",Selectors_optionIcon=".option-icon",Selectors_selectedOption='[role="option"][aria-selected="true"]',Selectors_uncheckedIcon='[data-for="uncheckedIcon"]',Classes_selectedBg="bg-light",Classes_selected="selected",Classes_disabled="disabled",Classes_hidden="d-none";class DropdownStatus extends _dialog.DropdownDialog{constructor(element){super(element),this.buttonSync="true"==element.dataset.buttonSync,this.updateStatus="true"==element.dataset.updateStatus}init(){super.init(),this.element.dataset.dropdownStatusInitialized||(this.panel.addEventListener("click",this._contentClickHandler.bind(this)),"true"==this.element.dataset.buttonSync&&this.setButtonSyncEnabled(!0),"true"==this.element.dataset.updateStatus&&this.setUpdateStatusEnabled(!0),this.element.dataset.dropdownStatusInitialized=!0)}_contentClickHandler(event){const option=event.target.closest(Selectors_option);option&&"true"!==option.getAttribute("aria-disabled")&&"true"!==option.getAttribute("aria-selected")&&this.isUpdateStatusEnabled()&&this.setSelectedValue(option.dataset.value)}setSelectedValue(value){const selected=this.panel.querySelector(Selectors_selectedOption);if(selected&&selected.dataset.value===value)return;selected&&this._updateOptionChecked(selected,!1);const option=this.panel.querySelector("".concat(Selectors_option,'[data-value="').concat(value,'"]'));option&&this._updateOptionChecked(option,!0),this.isButtonSyncEnabled()&&this.syncButtonText(),this.element.dispatchEvent(new Event("change"))}_updateOptionChecked(option,checked){option.setAttribute("aria-selected",checked.toString()),option.classList.toggle(Classes_selected,checked),option.classList.toggle(Classes_disabled,checked);const optionItem=option.closest(Selectors_optionItem);optionItem&&this._updateOptionItemChecked(optionItem,checked),checked?this.element.dataset.value=option.dataset.value:this.element.dataset.value===option.dataset.value&&delete this.element.dataset.value}_updateOptionItemChecked(optionItem,checked){optionItem.classList.toggle(Classes_selectedBg,checked),optionItem.classList.toggle(Classes_selected,checked),checked?optionItem.dataset.selected=checked:null==optionItem||delete optionItem.dataset.selected;const checkedIcon=optionItem.querySelector(Selectors_checkedIcon);checkedIcon&&checkedIcon.classList.toggle(Classes_hidden,!checked);const uncheckedIcon=optionItem.querySelector(Selectors_uncheckedIcon);uncheckedIcon&&uncheckedIcon.classList.toggle(Classes_hidden,checked)}getSelectedValue(){var _selected$dataset$val;const selected=this.panel.querySelector(Selectors_selectedOption);return null!==(_selected$dataset$val=null==selected?void 0:selected.dataset.value)&&void 0!==_selected$dataset$val?_selected$dataset$val:null}setButtonSyncEnabled(value){value?this.element.dataset.buttonSync="true":delete this.element.dataset.buttonSync,value&&this.syncButtonText()}isButtonSyncEnabled(){return"true"==this.element.dataset.buttonSync}syncButtonText(){const selected=this.panel.querySelector(Selectors_selectedOption);if(!selected)return;let newText=selected.textContent;const optionIcon=this._getOptionIcon(selected);optionIcon&&(newText=optionIcon.innerHTML+newText),this.button.innerHTML=newText}setUpdateStatusEnabled(value){value?this.element.dataset.updateStatus="true":delete this.element.dataset.updateStatus}isUpdateStatusEnabled(){return"true"==this.element.dataset.updateStatus}_getOptionIcon(option){const optionItem=option.closest(Selectors_optionItem);return optionItem?optionItem.querySelector(Selectors_optionIcon):null}}_exports.DropdownStatus=DropdownStatus;const getDropdownStatus=selector=>{const dropdownElement=document.querySelector(selector);return dropdownElement?new DropdownStatus(dropdownElement):null};_exports.getDropdownStatus=getDropdownStatus;_exports.init=selector=>{const dropdown=getDropdownStatus(selector);if(!dropdown)throw new Error("Dopdown status element not found: ".concat(selector));dropdown.init()}}));
const Selectors_checkedIcon='[data-for="checkedIcon"]',Selectors_option='[role="option"]',Selectors_optionItem="[data-optionnumber]",Selectors_optionIcon=".option-icon",Selectors_selectedOption='[role="option"][aria-selected="true"]',Selectors_uncheckedIcon='[data-for="uncheckedIcon"]',Classes_selected="selected",Classes_disabled="disabled",Classes_hidden="d-none";class DropdownStatus extends _dialog.DropdownDialog{constructor(element){super(element),this.buttonSync="true"==element.dataset.buttonSync,this.updateStatus="true"==element.dataset.updateStatus}init(){super.init(),this.element.dataset.dropdownStatusInitialized||(this.panel.addEventListener("click",this._contentClickHandler.bind(this)),"true"==this.element.dataset.buttonSync&&this.setButtonSyncEnabled(!0),"true"==this.element.dataset.updateStatus&&this.setUpdateStatusEnabled(!0),this.element.dataset.dropdownStatusInitialized=!0)}_contentClickHandler(event){const option=event.target.closest(Selectors_option);option&&"true"!==option.getAttribute("aria-disabled")&&"true"!==option.getAttribute("aria-selected")&&this.isUpdateStatusEnabled()&&this.setSelectedValue(option.dataset.value)}setSelectedValue(value){const selected=this.panel.querySelector(Selectors_selectedOption);if(selected&&selected.dataset.value===value)return;selected&&this._updateOptionChecked(selected,!1);const option=this.panel.querySelector("".concat(Selectors_option,'[data-value="').concat(value,'"]'));option&&this._updateOptionChecked(option,!0),this.isButtonSyncEnabled()&&this.syncButtonText(),this.element.dispatchEvent(new Event("change"))}_updateOptionChecked(option,checked){option.setAttribute("aria-selected",checked.toString()),option.classList.toggle(Classes_selected,checked),option.classList.toggle(Classes_disabled,checked);const optionItem=option.closest(Selectors_optionItem);optionItem&&this._updateOptionItemChecked(optionItem,checked),checked?this.element.dataset.value=option.dataset.value:this.element.dataset.value===option.dataset.value&&delete this.element.dataset.value}_updateOptionItemChecked(optionItem,checked){var _optionItem$dataset$s;const selectedClasses=null!==(_optionItem$dataset$s=optionItem.dataset.selectedClasses)&&void 0!==_optionItem$dataset$s?_optionItem$dataset$s:Classes_selected;for(const selectedClass of selectedClasses.split(" "))optionItem.classList.toggle(selectedClass,checked);checked?optionItem.dataset.selected=checked:null==optionItem||delete optionItem.dataset.selected;const checkedIcon=optionItem.querySelector(Selectors_checkedIcon);checkedIcon&&checkedIcon.classList.toggle(Classes_hidden,!checked);const uncheckedIcon=optionItem.querySelector(Selectors_uncheckedIcon);uncheckedIcon&&uncheckedIcon.classList.toggle(Classes_hidden,checked)}getSelectedValue(){var _selected$dataset$val;const selected=this.panel.querySelector(Selectors_selectedOption);return null!==(_selected$dataset$val=null==selected?void 0:selected.dataset.value)&&void 0!==_selected$dataset$val?_selected$dataset$val:null}setButtonSyncEnabled(value){value?this.element.dataset.buttonSync="true":delete this.element.dataset.buttonSync,value&&this.syncButtonText()}isButtonSyncEnabled(){return"true"==this.element.dataset.buttonSync}syncButtonText(){const selected=this.panel.querySelector(Selectors_selectedOption);if(!selected)return;let newText=selected.textContent;const optionIcon=this._getOptionIcon(selected);optionIcon&&(newText=optionIcon.innerHTML+newText),this.button.innerHTML=newText}setUpdateStatusEnabled(value){value?this.element.dataset.updateStatus="true":delete this.element.dataset.updateStatus}isUpdateStatusEnabled(){return"true"==this.element.dataset.updateStatus}_getOptionIcon(option){const optionItem=option.closest(Selectors_optionItem);return optionItem?optionItem.querySelector(Selectors_optionIcon):null}}_exports.DropdownStatus=DropdownStatus;const getDropdownStatus=selector=>{const dropdownElement=document.querySelector(selector);return dropdownElement?new DropdownStatus(dropdownElement):null};_exports.getDropdownStatus=getDropdownStatus;_exports.init=selector=>{const dropdown=getDropdownStatus(selector);if(!dropdown)throw new Error("Dopdown status element not found: ".concat(selector));dropdown.init()}}));
//# sourceMappingURL=status.min.js.map
File diff suppressed because one or more lines are too long
+12
View File
@@ -165,6 +165,18 @@ export class DropdownDialog {
this.button.innerHTML = content;
}
/**
* Set the disabled state of the button.
* @param {Boolean} disabled
*/
setButtonDisabled(disabled) {
if (disabled) {
this.button.setAttribute('disabled', 'disabled');
} else {
this.button.removeAttribute('disabled');
}
}
/**
* Return the main dropdown HTML element.
* @returns {HTMLElement} The element.
+5 -4
View File
@@ -21,7 +21,7 @@
* - Update the status of the button when the selected option changes. This will
* trigger a "change" event when the status changes.
*
* @module core/local/dropdown/dialog
* @module core/local/dropdown/status
* @copyright 2023 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -38,7 +38,6 @@ const Selectors = {
};
const Classes = {
selectedBg: 'bg-light',
selected: 'selected',
disabled: 'disabled',
hidden: 'd-none',
@@ -158,8 +157,10 @@ export class DropdownStatus extends DropdownDialog {
* @param {Boolean} checked
*/
_updateOptionItemChecked(optionItem, checked) {
optionItem.classList.toggle(Classes.selectedBg, checked);
optionItem.classList.toggle(Classes.selected, checked);
const selectedClasses = optionItem.dataset.selectedClasses ?? Classes.selected;
for (const selectedClass of selectedClasses.split(' ')) {
optionItem.classList.toggle(selectedClass, checked);
}
if (checked) {
optionItem.dataset.selected = checked;
} else {
@@ -80,6 +80,11 @@ class dialog implements named_templatable, renderable {
*/
protected $extras = [];
/**
* @var bool if the element is disabled.
*/
protected $disabled = false;
/**
* Constructor.
*
@@ -190,6 +195,14 @@ class dialog implements named_templatable, renderable {
$this->dropdownposition = $position;
}
/**
* Set the dropdown disabled attribute.
* @param bool $disabled the disabled value
*/
public function set_disabled(bool $disabled) {
$this->disabled = $disabled;
}
/**
* Export this data so it can be used as the context for a mustache template (core/inplace_editable).
*
@@ -220,6 +233,9 @@ class dialog implements named_templatable, renderable {
'dialogclasses' => $this->dropdownwidth,
'extras' => $extras,
];
if ($this->disabled) {
$data['disabledbutton'] = true;
}
// Bootstrap 4 dropdown position still uses left and right literals.
$data["position"] = $this->dropdownposition;
if (right_to_left()) {
@@ -55,6 +55,7 @@
}} {{#selected}} border bg-primary-light selected {{/selected}}"
data-optionnumber="{{optionnumber}}"
data-selected="{{selected}}"
data-selected-classes = "border bg-primary-light selected"
>
<div class="option-select-indicator">
<span class="{{^selected}} d-none {{/selected}}" data-for="checkedIcon">
@@ -49,6 +49,7 @@
],
"buttonclasses": "extraclasses",
"dialogclasses": "extraclasses",
"disabledbutton": false,
"classes": "extraclasses"
}
}}
@@ -78,6 +79,7 @@
aria-haspopup="true"
aria-expanded="false"
data-for="dropdowndialog_button"
{{#disabledbutton}} disabled {{/disabledbutton}}
>
{{$ buttoncontent }}
{{{ buttoncontent }}}