Merge branch 'MDL-78826-master' of https://github.com/roland04/moodle

This commit is contained in:
Jun Pataleta
2023-09-13 19:28:25 +08:00
20 changed files with 1089 additions and 38 deletions
@@ -184,41 +184,195 @@ $dialog = new core\output\local\dropdown\status('Open dialog button', $choice);
echo $OUTPUT->render($dialog);
{{< / php >}}
## Capturing events with Javascript
#### Sync button text with selected status
Unfortunately, the current implementation does not yet include an ADM module for rendering or controlling dropdowns. However, you can create ad-hoc modules by adding id and data attributes to the relevant elements.
The status dropdown can be configured to sync the button text with the selected status.
Here is an example of how to include id and data attributes on the main component:
To do so, you need to set the `buttonsync` $definition attribute to `true`.
{{< php >}}
$dialog = new core\output\local\dropdown\dialog(
'Open dialog',
'Dialog content',
$choice = new core\output\choicelist();
$choice->add_option('option1', get_string('option1', YOURPLUGIN));
$choice->add_option('option2', get_string('option2', YOURPLUGIN));
$choice->set_selected_value('option2');
// Add some attribute to select through a query selector.
$dialog = new core\output\local\dropdown\status(
get_string('buttontext', YOURPLUGIN),
$choice,
[
extras' => ['id' => 'mydropdown', 'data-foo' => 'bar']
'extras' => ['id' => 'mydropdown'],
'buttonsync' => true,
// With 'updatestatus' it will change the status when the user clicks an option
// See "Dropdown status in update mode" section for more information.
'updatestatus' => true,
]
);
echo $OUTPUT->render($dialog);
{{< / php >}}
Below is an example of how to include additional attributes to the options provided to the user:
## Javascript
### Controlling dropdowns
Both `core/local/dropdown/status` and `core/local/dropdown/status` AMD modules provide functions to:
- Open and close the dropdown.
- Change the button content.
- Get the main dropdown HTML element.
Both modules are object-oriented. To get the dropdown instance, the process is as follows:
1. Add id or data attributes to the main component to select it using a query selector.
2. Import `getDropdownDialog` from `core/local/dropdown/dialog`, or `getDropdownStatus` from `core/local/dropdown/status`, depending on whether you use a dialogue or a status dropdown.
3. Call `getDropdownDialog` or `getDropdownStatus` with the query selector to get the instance
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.
- `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:
```js
import {getDropdownDialog} from 'core/local/dropdown/';
const dialog = getDropdownDialog('[MYDROPDOWNSELECTOR]');
document.querySelector('[data-for="openDropdown"]').addEventListener('click', (event) => {
event.stopPropagation();
dialog.setVisible(true);
});
```
### Specific dropdown status methods
The `core/local/dropdown/status` provides extra controls for the status selector, such as:
- `getSelectedValue()` and `setSelectedValue(String)` to control the currently selected status.
- `isButtonSyncEnabled()` and `setButtonSyncEnabled(Boolean)` to synchronise the button text with the selected status.
- `isUpdateStatusEnabled()` and `setUpdateStatusEnabled(Boolean)` to control the auto-update status mode.
## Using dropdown status from the frontend
The dropdown status can operate in two different ways.
### Dropdown status in display only
The display-only is the default behaviour for any dropdown. In display-only mode, the component will show all the status values to the user, but it won't handle and click the event nor change the current status.
If a plugin wants to change the status value when the user clicks, it should code a custom module to:
1. Capture `click` event listeners to the choice items.
2. Send the new status to the backend (using an ad-hoc webservice).
3. If the webservice execution is ok, update the component value using the `setSelectedValue` instance method.
The following example shows how to render a display-only dropdown status in the backend:
{{< php >}}
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1', [
extras' => ['id' => 'myoption1', 'data-foo' => 'bar1']
// Add some data attributes to the choices.
$choice->add_option(
'option1',
get_string('option1', YOURPLUGIN), [
extras' => ['data-action' => 'updateActionName']
]);
$choice->add_option('option2', 'Option 2', [
extras' => ['id' => 'myoption2', 'data-foo' => 'bar2']
$choice->add_option(
'option2',
get_string('option2', YOURPLUGIN), [
extras' => ['data-action' => 'updateActionName']
]);
$choice->set_selected_value('option2');
$dialog = new core\output\local\dropdown\status('Open dialog button', $choice);
// Add some attribute to select through a query selector.
$dialog = new core\output\local\dropdown\status(
get_string('buttontext', YOURPLUGIN),
$choice,
['extras' => ['id' => 'mydropdown']]
);
echo $OUTPUT->render($dialog);
{{< / php >}}
Having this PHP code, the AMD controller could be something like:
```js
import {getDropdownStatus} from 'core/local/dropdown/status';
import {sendValueToTheBackend} from 'YOURPLUGIN/example';
const status = getDropdownStatus('#mydropdown');
status.getElement().addEventListener('click', (event) => {
const option = event.target.closest("[data-action='updateActionName']");
if (!option) {
return;
}
try {
if(sendValueToTheBackend(option.dataset.value)) {
status.setSelectedValue(option.dataset.value);
}
} catch (error) {
// Do some error handling here.
}
});
```
### Dropdown status in update mode
The component will act more like an HTML radio button in update mode. It will store the current status value and will trigger `change` events when the value changes.
In this case, the plugin controller has to:
1. Capture the component element `change` event. Remember that, as in radio events, the `change` event won't bubble, so it cannot be delegated to a parent element.
2. Send the new status to the backend (using an ad-hoc webservice).
3. If the webservice execution fails, do a value rollback using the `setSelectedValue` instance method.
The following example shows how to render an update mode dropdown status in the backend:
{{< php >}}
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', get_string('option1', YOURPLUGIN));
$choice->add_option('option2', get_string('option2', YOURPLUGIN));
$choice->set_selected_value('option2');
// Add some attribute to select through a query selector.
$dialog = new core\output\local\dropdown\status(
get_string('buttontext', YOURPLUGIN),
$choice,
[
'extras' => ['id' => 'mydropdown'],
'updatestatus' => true,
]
);
echo $OUTPUT->render($dialog);
{{< / php >}}
Having this PHP code, the AMD controller could be something like:
```js
import {getDropdownStatus} from 'core/local/dropdown/status';
import {sendValueToTheBackend} from 'YOURPLUGIN/example';
const status = getDropdownStatus('#mydropdown');
let currentValue = status.getSelectedValue();
status.getElement().addEventListener('change', (event) => {
if (currentValue == status.getSelectedValue()) {
return;
}
try {
sendValueToTheBackend(status.getSelectedValue());
currentValue = status.getSelectedValue();
} catch (error) {
status.setSelectedValue(currentValue);
}
});
```
**Note**: the `event.target` is also the main element. You can also get the current value from `event.target.dataset.value` if you prefer.
## Examples
<!-- markdownlint-disable-next-line MD033 -->
<iframe src="../../../../examples/dropdowns.php" style="overflow:hidden;height:400px;width:100%;border:0" title="Moodle dynamic tabs"></iframe>
@@ -44,7 +44,14 @@ $output = $PAGE->get_renderer('core');
echo $output->header();
echo $output->paragraph(
'<strong>Important note:</strong> dropdowns are not prepared
to be displayed inside iframes. You may need to scroll to see the
the dropdown content.'
);
echo $output->heading("Dropdown dialog example", 3);
echo '<div class="p-3">';
$dialog = new core\output\local\dropdown\dialog(
'Open dialog',
@@ -55,15 +62,18 @@ $dialog = new core\output\local\dropdown\dialog(
</ul>'
);
echo $OUTPUT->render($dialog);
echo "</div>";
echo $output->heading("Dropdown status example", 3);
echo '<div class="p-3">';
$choice = new core\output\choicelist('Choice description text');
// Option one is a link.
$choice->add_option('option1', 'Option 1', [
'description' => 'Option 1 description',
'icon' => new pix_icon('t/show', 'Eye icon 1'),
'url' => new moodle_url('/admin/tool/componentlibrary/examples/dropdowns.php'),
]);
// Option two has an icon and description.
$choice->add_option('option2', 'Option 2', [
@@ -74,11 +84,41 @@ $choice->add_option('option2', 'Option 2', [
$choice->add_option('option3', 'Option 3', [
'description' => 'Option 3 description',
'icon' => new pix_icon('t/stealth', 'Eye icon 3'),
'disabled' => true,
]);
$choice->set_selected_value('option2');
$dialog = new core\output\local\dropdown\status('Open dialog button', $choice);
echo $OUTPUT->render($dialog);
echo "</div>";
echo $output->heading("Dropdown status in update mode example", 3);
echo '<div class="p-3">';
$choice = new core\output\choicelist('Choice description text');
$choice->add_option('option1', 'Option 1', [
'description' => 'Option 1 description',
'icon' => new pix_icon('t/show', 'Eye icon 1'),
]);
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon 2'),
]);
$choice->set_selected_value('option2');
$dialog = new core\output\local\dropdown\status(
'Open dialog button',
$choice,
[
'buttonsync' => true,
'updatestatus' => true,
'dialogwidth' => core\output\local\dropdown\status::WIDTH['big']
]
);
echo $OUTPUT->render($dialog);
echo "</div>";
echo $output->footer();
+10
View File
@@ -0,0 +1,10 @@
define("core/local/dropdown/dialog",["exports","jquery","core/pagehelpers","core/pending"],(function(_exports,_jquery,_pagehelpers,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}
/**
* Dropdown status JS controls.
*
* @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()}}));
//# sourceMappingURL=dialog.min.js.map
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
define("core/local/dropdown/status",["exports","core/local/dropdown/dialog"],(function(_exports,_dialog){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.getDropdownStatus=_exports.DropdownStatus=void 0;
/**
* Dropdown status JS controls.
*
* The status controls enable extra configurarions for the dropdown like:
* - Sync the button text with the selected option.
* - 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
* @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()}}));
//# sourceMappingURL=status.min.js.map
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,4 +1,4 @@
define("core/pagehelpers",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.previousFocusableElement=_exports.nextFocusableElement=_exports.isSmall=_exports.isLarge=_exports.isExtraSmall=_exports.getCurrentWidth=_exports.focusableElements=_exports.firstFocusableElement=void 0;
define("core/pagehelpers",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.previousFocusableElement=_exports.nextFocusableElement=_exports.lastFocusableElement=_exports.isSmall=_exports.isLarge=_exports.isExtraSmall=_exports.getCurrentWidth=_exports.focusableElements=_exports.firstFocusableElement=void 0;
/**
* Page utility helpers.
*
@@ -6,6 +6,6 @@ define("core/pagehelpers",["exports"],(function(_exports){Object.defineProperty(
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const Sizes_small=576,Sizes_medium=991,Sizes_large=1400,Selectors_focusable='a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',getCurrentWidth=()=>{const DomRect=document.body.getBoundingClientRect();return DomRect.x+DomRect.width};_exports.getCurrentWidth=getCurrentWidth;_exports.isExtraSmall=()=>getCurrentWidth()<Sizes_small;_exports.isSmall=()=>getCurrentWidth()<Sizes_medium;_exports.isLarge=()=>getCurrentWidth()>=Sizes_large;_exports.firstFocusableElement=container=>(container||document).querySelector(Selectors_focusable);const focusableElements=container=>(container||document).querySelectorAll(Selectors_focusable);_exports.focusableElements=focusableElements;_exports.previousFocusableElement=(container,loopSelection)=>getRelativeFocusableElement(container,loopSelection,-1);_exports.nextFocusableElement=(container,loopSelection)=>getRelativeFocusableElement(container,loopSelection,1);const getRelativeFocusableElement=(container,loopSelection,direction)=>{var _focusables;const focusedElement=document.activeElement,focusables=[...focusableElements(container)],focusedIndex=focusables.indexOf(focusedElement);if(-1===focusedIndex)return null;const newIndex=focusedIndex+direction;return void 0!==focusables[newIndex]?focusables[newIndex]:1!=loopSelection?null:direction>0?null!==(_focusables$=focusables[0])&&void 0!==_focusables$?_focusables$:null:null!==(_focusables=focusables[focusables.length-1])&&void 0!==_focusables?_focusables:null;var _focusables$}}));
const Sizes_small=576,Sizes_medium=991,Sizes_large=1400,Selectors_focusable='a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])',getCurrentWidth=()=>{const DomRect=document.body.getBoundingClientRect();return DomRect.x+DomRect.width};_exports.getCurrentWidth=getCurrentWidth;_exports.isExtraSmall=()=>getCurrentWidth()<Sizes_small;_exports.isSmall=()=>getCurrentWidth()<Sizes_medium;_exports.isLarge=()=>getCurrentWidth()>=Sizes_large;_exports.firstFocusableElement=container=>(container||document).querySelector(Selectors_focusable);_exports.lastFocusableElement=container=>{var _focusableElements;const focusableElements=(container||document).querySelectorAll(Selectors_focusable);return null!==(_focusableElements=focusableElements[focusableElements.length-1])&&void 0!==_focusableElements?_focusableElements:null};const focusableElements=container=>(container||document).querySelectorAll(Selectors_focusable);_exports.focusableElements=focusableElements;_exports.previousFocusableElement=(container,loopSelection)=>getRelativeFocusableElement(container,loopSelection,-1);_exports.nextFocusableElement=(container,loopSelection)=>getRelativeFocusableElement(container,loopSelection,1);const getRelativeFocusableElement=(container,loopSelection,direction)=>{var _focusables;const focusedElement=document.activeElement,focusables=[...focusableElements(container)],focusedIndex=focusables.indexOf(focusedElement);if(-1===focusedIndex)return null;const newIndex=focusedIndex+direction;return void 0!==focusables[newIndex]?focusables[newIndex]:1!=loopSelection?null:direction>0?null!==(_focusables$=focusables[0])&&void 0!==_focusables$?_focusables$:null:null!==(_focusables=focusables[focusables.length-1])&&void 0!==_focusables?_focusables:null;var _focusables$}}));
//# sourceMappingURL=pagehelpers.min.js.map
File diff suppressed because one or more lines are too long
+202
View File
@@ -0,0 +1,202 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Dropdown status JS controls.
*
* @module core/local/dropdown/dialog
* @copyright 2023 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// The jQuery module is only used for interacting with Bootstrap 4. It can be removed when MDL-71979 is integrated.
import jQuery from 'jquery';
import {
firstFocusableElement,
lastFocusableElement,
previousFocusableElement,
nextFocusableElement,
} from 'core/pagehelpers';
import Pending from 'core/pending';
const Selectors = {
dropdownButton: '[data-for="dropdowndialog_button"]',
dropdownDialog: '[data-for="dropdowndialog_dialog"]',
};
/**
* Dropdown dialog class.
* @private
*/
export class DropdownDialog {
/**
* Constructor.
* @param {HTMLElement} element The element to initialize.
*/
constructor(element) {
this.element = element;
this.button = element.querySelector(Selectors.dropdownButton);
this.panel = element.querySelector(Selectors.dropdownDialog);
}
/**
* Initialize the subpanel element.
*
* This method adds the event listeners to the subpanel and the position classes.
*/
init() {
if (this.element.dataset.dropdownDialogInitialized) {
return;
}
// Menu Item events.
this.button.addEventListener('keydown', this._buttonKeyHandler.bind(this));
// Subpanel content events.
this.panel.addEventListener('keydown', this._contentKeyHandler.bind(this));
this.element.dataset.dropdownDialogInitialized = true;
}
/**
* Dropdown button key handler.
* @param {Event} event
* @private
*/
_buttonKeyHandler(event) {
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
event.stopPropagation();
event.preventDefault();
this.setVisible(false);
return;
}
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
event.stopPropagation();
event.preventDefault();
this.setVisible(true);
this._focusPanelContent();
}
}
/**
* Sub panel content key handler.
* @param {Event} event
* @private
*/
_contentKeyHandler(event) {
let newFocus = null;
if (event.key === 'End') {
newFocus = lastFocusableElement(this.panel);
}
if (event.key === 'Home') {
newFocus = firstFocusableElement(this.panel);
}
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
newFocus = previousFocusableElement(this.panel, false);
if (!newFocus) {
newFocus = this.button;
}
}
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
newFocus = nextFocusableElement(this.panel, false);
}
if (newFocus !== null) {
event.stopPropagation();
event.preventDefault();
newFocus.focus();
}
}
/**
* Focus on the first focusable element of the subpanel.
* @private
*/
_focusPanelContent() {
const pendingPromise = new Pending('core/dropdown/dialog:focuscontent');
// Some Bootstrap events are triggered after the click event.
// To prevent this from affecting the focus we wait a bit.
setTimeout(() => {
const firstFocusable = firstFocusableElement(this.panel);
if (firstFocusable) {
firstFocusable.focus();
}
pendingPromise.resolve();
}, 100);
}
/**
* Set the visibility of a subpanel.
* @param {Boolean} visible true if the subpanel should be visible.
*/
setVisible(visible) {
if (visible === this.isVisible()) {
return;
}
// All jQuery in this code can be replaced when MDL-71979 is integrated.
jQuery(this.button).dropdown('toggle');
}
/**
* Get the visibility of a subpanel.
* @returns {Boolean} true if the subpanel is visible.
*/
isVisible() {
return this.button.getAttribute('aria-expanded') === 'true';
}
/**
* Set the content of the button.
* @param {String} content
*/
setButtonContent(content) {
this.button.innerHTML = content;
}
/**
* Return the main dropdown HTML element.
* @returns {HTMLElement} The element.
*/
getElement() {
return this.element;
}
}
/**
* Get the dropdown dialog instance from a selector.
* @param {string} selector The query selector to init.
* @returns {DropdownDialog|null} The dropdown dialog instance if any.
*/
export const getDropdownDialog = (selector) => {
const dropdownElement = document.querySelector(selector);
if (!dropdownElement) {
return null;
}
return new DropdownDialog(dropdownElement);
};
/**
* Initialize module.
*
* @method
* @param {string} selector The query selector to init.
*/
export const init = (selector) => {
const dropdown = getDropdownDialog(selector);
if (!dropdown) {
throw new Error(`Dopdown dialog element not found: ${selector}`);
}
dropdown.init();
};
+286
View File
@@ -0,0 +1,286 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Dropdown status JS controls.
*
* The status controls enable extra configurarions for the dropdown like:
* - Sync the button text with the selected option.
* - 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
* @copyright 2023 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {DropdownDialog} from 'core/local/dropdown/dialog';
const Selectors = {
checkedIcon: '[data-for="checkedIcon"]',
option: '[role="option"]',
optionItem: '[data-optionnumber]',
optionIcon: '.option-icon',
selectedOption: '[role="option"][aria-selected="true"]',
uncheckedIcon: '[data-for="uncheckedIcon"]',
};
const Classes = {
selectedBg: 'bg-light',
selected: 'selected',
disabled: 'disabled',
hidden: 'd-none',
};
/**
* Dropdown dialog class.
* @private
*/
export class DropdownStatus extends DropdownDialog {
/**
* Constructor.
* @param {HTMLElement} element The element to initialize.
*/
constructor(element) {
super(element);
this.buttonSync = element.dataset.buttonSync == 'true';
this.updateStatus = element.dataset.updateStatus == 'true';
}
/**
* Initialize the subpanel element.
*
* This method adds the event listeners to the subpanel and the position classes.
* @private
*/
init() {
super.init();
if (this.element.dataset.dropdownStatusInitialized) {
return;
}
this.panel.addEventListener('click', this._contentClickHandler.bind(this));
if (this.element.dataset.buttonSync == 'true') {
this.setButtonSyncEnabled(true);
}
if (this.element.dataset.updateStatus == 'true') {
this.setUpdateStatusEnabled(true);
}
this.element.dataset.dropdownStatusInitialized = true;
}
/**
* Handle click events on the status content.
* @param {Event} event The event.
* @private
*/
_contentClickHandler(event) {
const option = event.target.closest(Selectors.option);
if (!option) {
return;
}
if (option.getAttribute('aria-disabled') === 'true') {
return;
}
if (option.getAttribute('aria-selected') === 'true') {
return;
}
if (this.isUpdateStatusEnabled()) {
this.setSelectedValue(option.dataset.value);
}
}
/**
* Sets the selected value.
* @param {string} value The value to set.
*/
setSelectedValue(value) {
const selected = this.panel.querySelector(Selectors.selectedOption);
if (selected && selected.dataset.value === value) {
return;
}
if (selected) {
this._updateOptionChecked(selected, false);
}
const option = this.panel.querySelector(`${Selectors.option}[data-value="${value}"]`);
if (option) {
this._updateOptionChecked(option, true);
}
if (this.isButtonSyncEnabled()) {
this.syncButtonText();
}
// Emit standard radio button event with the selected option.
this.element.dispatchEvent(new Event('change'));
}
/**
* Update the option checked content.
* @private
* @param {HTMLElement} option the option element to set
* @param {Boolean} checked the new checked value
*/
_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);
if (optionItem) {
this._updateOptionItemChecked(optionItem, checked);
}
if (checked) {
this.element.dataset.value = option.dataset.value;
} else if (this.element.dataset.value === option.dataset.value) {
delete this.element.dataset.value;
}
}
/**
* Update the option item checked content.
* @private
* @param {HTMLElement} optionItem
* @param {Boolean} checked
*/
_updateOptionItemChecked(optionItem, checked) {
optionItem.classList.toggle(Classes.selectedBg, checked);
optionItem.classList.toggle(Classes.selected, checked);
if (checked) {
optionItem.dataset.selected = checked;
} else {
delete optionItem?.dataset.selected;
}
const checkedIcon = optionItem.querySelector(Selectors.checkedIcon);
if (checkedIcon) {
checkedIcon.classList.toggle(Classes.hidden, !checked);
}
const uncheckedIcon = optionItem.querySelector(Selectors.uncheckedIcon);
if (uncheckedIcon) {
uncheckedIcon.classList.toggle(Classes.hidden, checked);
}
}
/**
* Return the selected value.
* @returns {string|null} The selected value.
*/
getSelectedValue() {
const selected = this.panel.querySelector(Selectors.selectedOption);
return selected?.dataset.value ?? null;
}
/**
* Set the button sync value.
*
* If the sync is enabled, the button text will show the selected option.
*
* @param {Boolean} value The value to set.
*/
setButtonSyncEnabled(value) {
if (value) {
this.element.dataset.buttonSync = 'true';
} else {
delete this.element.dataset.buttonSync;
}
if (value) {
this.syncButtonText();
}
}
/**
* Return if the button sync is enabled.
* @returns {Boolean} The button sync value.
*/
isButtonSyncEnabled() {
return this.element.dataset.buttonSync == 'true';
}
/**
* Sync the button text with the selected option.
*/
syncButtonText() {
const selected = this.panel.querySelector(Selectors.selectedOption);
if (!selected) {
return;
}
let newText = selected.textContent;
const optionIcon = this._getOptionIcon(selected);
if (optionIcon) {
newText = optionIcon.innerHTML + newText;
}
this.button.innerHTML = newText;
}
/**
* Set the update status value.
*
* @param {Boolean} value The value to set.
*/
setUpdateStatusEnabled(value) {
if (value) {
this.element.dataset.updateStatus = 'true';
} else {
delete this.element.dataset.updateStatus;
}
}
/**
* Return if the update status is enabled.
* @returns {Boolean} The update status value.
*/
isUpdateStatusEnabled() {
return this.element.dataset.updateStatus == 'true';
}
_getOptionIcon(option) {
const optionItem = option.closest(Selectors.optionItem);
if (!optionItem) {
return null;
}
return optionItem.querySelector(Selectors.optionIcon);
}
}
/**
* Get the dropdown dialog instance form a selector.
* @param {string} selector The query selector to init.
* @returns {DropdownStatus|null} The dropdown dialog instance if any.
*/
export const getDropdownStatus = (selector) => {
const dropdownElement = document.querySelector(selector);
if (!dropdownElement) {
return null;
}
return new DropdownStatus(dropdownElement);
};
/**
* Initialize module.
*
* @method
* @param {string} selector The query selector to init.
*/
export const init = (selector) => {
const dropdown = getDropdownStatus(selector);
if (!dropdown) {
throw new Error(`Dopdown status element not found: ${selector}`);
}
dropdown.init();
};
+11
View File
@@ -86,6 +86,17 @@ export const firstFocusableElement = (container) => {
return containerElement.querySelector(Selectors.focusable);
};
/**
* Get the last focusable element inside a container.
* @param {HTMLElement} [container] Container to search in. Defaults to document.
* @returns {HTMLElement|null}
*/
export const lastFocusableElement = (container) => {
const containerElement = container || document;
const focusableElements = containerElement.querySelectorAll(Selectors.focusable);
return focusableElements[focusableElements.length - 1] ?? null;
};
/**
* Get all focusable elements inside a container.
* @param {HTMLElement} [container] Container to search in. Defaults to document.
+8 -2
View File
@@ -179,7 +179,7 @@ class dialog implements named_templatable, renderable {
* @param string $value the value
*/
public function add_button_id(string $value) {
$this->extras['id'] = $value;
$this->extras['buttonid'] = $value;
}
/**
@@ -198,6 +198,11 @@ class dialog implements named_templatable, renderable {
*/
public function export_for_template(\renderer_base $output): array {
$extras = [];
// Id is required to add JS controls to the dropdown.
$dropdownid = $this->extras['id'] ?? \html_writer::random_id('dropdownDialog_');
if (isset($this->extras['id'])) {
unset($this->extras['id']);
}
foreach ($this->extras as $attribute => $value) {
$extras[] = [
'attribute' => $attribute,
@@ -206,7 +211,8 @@ class dialog implements named_templatable, renderable {
}
$data = [
// Id is required for the correct HTML labelling.
'buttonid' => \html_writer::random_id('dropwdownbutton_'),
'dropdownid' => $dropdownid,
'buttonid' => $this->extras['buttonid'] ?? \html_writer::random_id('dropwdownbutton_'),
'buttoncontent' => (string) $this->buttoncontent,
'dialogcontent' => (string) $this->dialogcontent,
'classes' => $this->classes,
@@ -44,6 +44,8 @@ class status extends dialog {
* - buttonclasses: the button CSS classes.
* - dialogwidth: the dropdown width.
* - extras: extra HTML attributes (attribute => value).
* - buttonsync: if the button should be synced with the selected value.
* - updatestatus: if component must update the status and trigger a change event when clicked.
*
* @param string $buttoncontent the button content
* @param choicelist $choices the choice object
@@ -52,6 +54,12 @@ class status extends dialog {
public function __construct(string $buttoncontent, choicelist $choices, array $definition = []) {
parent::__construct($buttoncontent, '', $definition);
$this->set_choice($choices);
if ($definition['buttonsync'] ?? false) {
$this->extras['data-button-sync'] = 'true';
}
if ($definition['updatestatus'] ?? false) {
$this->extras['data-update-status'] = 'true';
}
}
/**
+25 -9
View File
@@ -33,21 +33,23 @@
* dialogcontent - the dropdown dialog content.
* buttoncontent - the dropdown trigger button content.
* extras - custom HTML attributes for the component.
* dropdownid - the dropdown id (will be auto-generate if no id is passed).
Example context (json):
{
"buttonid" : "someinternalid",
"buttoncontent" : "Trigger button",
"dialogcontent" : "<a href=\"#\">Moodle</a>",
"extras" : [
"dropdownid": "internaldropdownid",
"buttonid": "internalbuttonid",
"buttoncontent": "Trigger button",
"dialogcontent": "<a href=\"#\">Moodle</a>",
"extras": [
{
"attribute" : "data-example",
"value" : "stickyfooter"
"attribute": "data-example",
"value": "stickyfooter"
}
],
"buttonclasses" : "extraclasses",
"dialogclasses" : "extraclasses",
"classes" : "extraclasses"
"buttonclasses": "extraclasses",
"dialogclasses": "extraclasses",
"classes": "extraclasses"
}
}}
<div
@@ -55,6 +57,10 @@
}} {{$ dropdownclasses }} {{!
}} {{#classes}} {{classes}} {{/classes}} {{!
}} {{/ dropdownclasses }}"
id="{{$ dropdownid }}{{!
}}{{#dropdownid}}{{dropdownid}}{{/dropdownid}}{{!
}}{{^dropdownid}}dropdownDialog_{{uniqid}}{{/dropdownid}}{{!
}}{{/ dropdownid }}"
{{$ extras }}
{{#extras}}
{{attribute}}="{{value}}"
@@ -71,6 +77,7 @@
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
data-for="dropdowndialog_button"
>
{{$ buttoncontent }}
{{{ buttoncontent }}}
@@ -83,6 +90,7 @@
}} {{#dialogclasses}} {{dialogclasses}} {{/dialogclasses}} {{!
}} {{/ dialogclasses }}"
aria-labelledby="{{#buttonid}}{{buttonid}}{{/buttonid}}{{^buttonid}}dropdownDialog{{uniqid}}{{/buttonid}}"
data-for="dropdowndialog_dialog"
>
<div class="p-2" data-for="dropdowndialog_content">
{{$ dialogcontent }}
@@ -91,3 +99,11 @@
</div>
</div>
</div>
{{#js}}
require(['core/local/dropdown/dialog'], function(Module) {
Module.init('#' + '{{$ dropdownid }}{{!
}}{{#dropdownid}}{{dropdownid}}{{/dropdownid}}{{!
}}{{^dropdownid}}dropdownDialog_{{uniqid}}{{/dropdownid}}{{!
}}{{/ dropdownid }}');
});
{{/js}}
+15 -6
View File
@@ -98,7 +98,8 @@
<div class="d-flex flex-column" role="listbox">
{{#options}}
<div
class="d-flex flex-row align-items-start p-2 position-relative rounded {{!
class="d-flex flex-row align-items-start p-2 mb-1 {{!
}} position-relative rounded dropdown-item-outline {{!
}} {{#disabled}} dimmed_text {{/disabled}} {{!
}} {{#selected}} bg-light selected {{/selected}}"
data-optionnumber="{{optionnumber}}"
@@ -109,13 +110,13 @@
{{>core/pix_icon}}
</div>
{{/icon}}
<div class="otion-select-indicator">
{{#selected}}
<div class="option-select-indicator">
<span class="{{^selected}} d-none {{/selected}}" data-for="checkedIcon">
{{#pix}} i/checkedcircle, core, {{#str}} selected, form {{/str}} {{/pix}}
{{/selected}}
{{^selected}}
</span>
<span class="{{#selected}} d-none {{/selected}}" data-for="uncheckedIcon">
{{#pix}} i/uncheckedcircle{{/pix}}
{{/selected}}
</span>
</div>
<div class="option-name">
<a
@@ -151,3 +152,11 @@
{{/choices}}
{{/ dialogcontent }}
{{/ core/local/dropdown/dialog }}
{{#js}}
require(['core/local/dropdown/status'], function(Module) {
Module.init('#' + '{{$ dropdownid }}{{!
}}{{#dropdownid}}{{dropdownid}}{{/dropdownid}}{{!
}}{{^dropdownid}}dropdownDialog_{{uniqid}}{{/dropdownid}}{{!
}}{{/ dropdownid }}');
});
{{/js}}
+107 -3
View File
@@ -41,9 +41,9 @@ Feature: Test dropdown output module
Scenario: Dropdown status can have as selected option
When I click on "Open dialog" "button" in the "statusselectedscenario" "region"
Then "Selected" "icon" should exist in the "#statusselectedscenario [data-optionnumber='2']" "css_element"
And "Selected" "icon" should not exist in the "#statusselectedscenario [data-optionnumber='1']" "css_element"
And "Selected" "icon" should not exist in the "#statusselectedscenario [data-optionnumber='3']" "css_element"
Then "Selected" "icon" in the "#statusselectedscenario [data-optionnumber='2']" "css_element" should be visible
And "Selected" "icon" in the "#statusselectedscenario [data-optionnumber='1']" "css_element" should not be visible
And "Selected" "icon" in the "#statusselectedscenario [data-optionnumber='3']" "css_element" should not be visible
Scenario: Dropdown status can have a disabled option
When I click on "Open dialog" "button" in the "statusdisablescenario" "region"
@@ -62,3 +62,107 @@ Feature: Test dropdown output module
When I click on "Open dialog" "button" in the "statusoptionurl" "region"
And I click on "Option 2" "link" in the "statusoptionurl" "region"
Then I should see "Foo param value: bar"
Scenario: Dropdowns dialogs can be controlled via javascript
Given "Open dialog" "button" should exist in the "dialogjscontrolssection" "region"
And I should see "The dropdown is hidden" in the "dialogjscontrolssection" "region"
# Change button text.
When I click on "Change button text" "button" in the "dialogjscontrolssection" "region"
Then "New button text" "button" should exist in the "dialogjscontrolssection" "region"
# Open dropdown.
And I click on "Open" "button" in the "dialogjscontrolssection" "region"
And I should see "Dialog content" in the "dialogjscontrolssection" "region"
And I should see "The dropdown is visible" in the "dialogjscontrolssection" "region"
# Close dropdown.
And I click on "Close" "button" in the "dialogjscontrolssection" "region"
And I should not see "Dialog content" in the "dialogjscontrolssection" "region"
And I should see "The dropdown is hidden" in the "dialogjscontrolssection" "region"
Scenario: Dropdown status can sync the clicked option with the button text
Given I should see "Option 2" in the "statussyncbutton" "region"
When I click on "Option 2" "button" in the "statussyncbutton" "region"
And "Selected" "icon" in the "#statussyncbutton [data-optionnumber='2']" "css_element" should be visible
And "Selected" "icon" in the "#statussyncbutton [data-optionnumber='3']" "css_element" should not be visible
And I click on "Option 3" "link" in the "statussyncbutton" "region"
Then I should see "Option 3" in the "statussyncbutton" "region"
And I should not see "Option 2" in the "statussyncbutton" "region"
And I click on "Option 3" "button" in the "statussyncbutton" "region"
And "Selected" "icon" in the "#statussyncbutton [data-optionnumber='2']" "css_element" should not be visible
And "Selected" "icon" in the "#statussyncbutton [data-optionnumber='3']" "css_element" should be visible
Scenario: Dropdowns status can be controlled via javascript
Given "Open dialog" "button" should exist in the "statusjscontrolsection" "region"
And I should see "The status value is option2" in the "statusjscontrolsection" "region"
# Change value.
When I click on "Change selected value" "button" in the "statusjscontrolsection" "region"
Then I should see "The status value is option3" in the "statusjscontrolsection" "region"
And I click on "Open dialog" "button" in the "statusjscontrolsection" "region"
And "Selected" "icon" in the "#statusjscontrolsection [data-optionnumber='2']" "css_element" should not be visible
And "Selected" "icon" in the "#statusjscontrolsection [data-optionnumber='3']" "css_element" should be visible
# Enable button sync.
And I click on "Enable sync" "button" in the "statusjscontrolsection" "region"
And I should see "Option 3" in the "statusjscontrolsection" "region"
And I click on "Option 3" "button" in the "statusjscontrolsection" "region"
And I click on "Option 2" "link" in the "statusjscontrolsection" "region"
And I should see "The status value is option2" in the "statusjscontrolsection" "region"
And I should see "Option 2" in the "statusjscontrolsection" "region"
# Trigger change event with button text sync.
And I click on "Change selected value" "button" in the "statusjscontrolsection" "region"
And I should see "Option 3" in the "statusjscontrolsection" "region"
And I should see "The status value is option3" in the "statusjscontrolsection" "region"
# Disable button text sync.
And I click on "Disable sync" "button" in the "statusjscontrolsection" "region"
And I click on "Option 3" "button" in the "statusjscontrolsection" "region"
And I click on "Option 1" "link" in the "statusjscontrolsection" "region"
And I should see "Option 3" in the "statusjscontrolsection" "region"
And I should see "The status value is option1" in the "statusjscontrolsection" "region"
And I click on "Change selected value" "button" in the "statusjscontrolsection" "region"
And I should see "Option 3" in the "statusjscontrolsection" "region"
And I should see "The status value is option2" in the "statusjscontrolsection" "region"
# Disable update.
And I click on "Disable update" "button" in the "statusjscontrolsection" "region"
And I click on "Option 3" "button" in the "statusjscontrolsection" "region"
And I click on "Option 1" "link" in the "statusjscontrolsection" "region"
And I should see "The status value is option2" in the "statusjscontrolsection" "region"
And I click on "Option 3" "button" in the "statusjscontrolsection" "region"
And "Selected" "icon" in the "#statusjscontrolsection [data-optionnumber='1']" "css_element" should not be visible
And "Selected" "icon" in the "#statusjscontrolsection [data-optionnumber='2']" "css_element" should be visible
Scenario: Dropdown status content is accessible with keyboard
Given I click on "Focus helper" "button" in the "statussyncbutton" "region"
When I press the tab key
# Open and close dropdown with enter key.
Then I press the enter key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should see "Option 1" in the "statussyncbutton" "region"
And I press the enter key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should not see "Option 1" in the "statussyncbutton" "region"
# Open and close with down and up keys.
And I press the down key
And the focused element is "[data-optionnumber='1'] a" "css_element" in the "statussyncbutton" "region"
And I should see "Option 1" in the "statussyncbutton" "region"
And I press the up key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should see "Option 1" in the "statussyncbutton" "region"
And I press the up key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should not see "Option 1" in the "statussyncbutton" "region"
# Select to option 3 and check user cannot go beyond that.
And I press the down key
And the focused element is "[data-optionnumber='1'] a" "css_element" in the "statussyncbutton" "region"
And I press the down key
And the focused element is "[data-optionnumber='2'] a" "css_element" in the "statussyncbutton" "region"
And I press the down key
And the focused element is "[data-optionnumber='3'] a" "css_element" in the "statussyncbutton" "region"
And I press the down key
And the focused element is "[data-optionnumber='3'] a" "css_element" in the "statussyncbutton" "region"
And I press the enter key
And I should see "Option 3" in the "statussyncbutton" "region"
# Close dropdown with escape key.
And I press the down key
And the focused element is "[data-optionnumber='1'] a" "css_element" in the "statussyncbutton" "region"
And I should see "Option 1" in the "statussyncbutton" "region"
And I press the escape key
And the focused element is "[data-for='dropdowndialog_button']" "css_element" in the "statussyncbutton" "region"
And I should not see "Option 1" in the "statussyncbutton" "region"
@@ -123,6 +123,59 @@ $dialog->set_classes('mb-3');
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="dialogjscontrolssection" class="mb-4">';
echo "<h3>Dropdown JS module controls</h3>";
echo '<div class="mb-2">
<button class="btn btn-secondary" id="buttontext">Change button text</button>
<button class="btn btn-secondary" id="opendropdown">Open</button>
<button class="btn btn-secondary" id="closedropdown">Close</button>
<span id="dialogvisibility"></span>
</div>';
$dialog = new core\output\local\dropdown\dialog('Open dialog', 'Dialog content', [
'extras' => ['id' => 'dialogjscontrols'],
]);
echo $OUTPUT->render($dialog);
echo '</div>';
$inlinejs = <<<EOF
require(
['core/local/dropdown/dialog', 'jquery'],
(Module, jQuery) => {
const dialog = Module.getDropdownDialog('#dialogjscontrols');
document.querySelector('#buttontext').addEventListener('click', () => {
dialog.setButtonContent('New button text');
});
document.querySelector('#opendropdown').addEventListener('click', (e) => {
e.stopPropagation();
dialog.setVisible(true);
});
document.querySelector('#closedropdown').addEventListener('click', (e) => {
e.stopPropagation();
dialog.setVisible(false);
});
const visibility = () => {
const text = 'The dropdown is ' + (dialog.isVisible() ? 'visible' : 'hidden') + '.';
document.querySelector('#dialogvisibility').innerHTML = text;
}
visibility();
// Bootstrap 4 events are still jQuery.
jQuery(dialog.getElement()).on('shown.bs.dropdown', (e) => {
visibility();
});
jQuery(dialog.getElement()).on('hidden.bs.dropdown', (e) => {
visibility();
});
}
);
EOF;
$PAGE->requires->js_amd_inline($inlinejs);
echo "<h2>Dropdown status test page</h2>";
echo '<div id="statusregularscenario" class="mb-4">';
@@ -205,4 +258,109 @@ $foo = optional_param('foo', 'none', PARAM_TEXT);
echo "<p>Foo param value: $foo</p>";
echo '</div>';
echo '<div id="statussyncbutton" class="mb-4">';
echo "<h3>Sync button text</h3>";
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1', [
'description' => 'Option 1 description',
'icon' => new pix_icon('t/show', 'Eye icon 1')
]);
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon 2')
]);
$choice->add_option('option3', 'Option 3', [
'description' => 'Option 3 description',
'icon' => new pix_icon('t/stealth', 'Eye icon 3')
]);
$choice->set_selected_value('option2');
$dialog = new core\output\local\dropdown\status(
'Open dialog',
$choice,
['buttonsync' => true, 'updatestatus' => true]
);
echo '<button class="btn">Focus helper</button>';
echo $OUTPUT->render($dialog);
echo '</div>';
echo '<div id="statusjscontrolsection" class="mb-4">';
echo "<h3>Status JS controls</h3>";
echo '<div class="mb-2">
<button class="btn btn-secondary" id="setselected">Change selected value</button>
<button class="btn btn-secondary" id="syncbutton">Enable sync</button>
<button class="btn btn-secondary" id="updatestatus">Disable update</button>
<span id="statusvalue"></span>
</div>';
$choice = new core\output\choicelist('Dialog content');
$choice->add_option('option1', 'Option 1', [
'description' => 'Option 1 description',
'icon' => new pix_icon('t/show', 'Eye icon 1')
]);
$choice->add_option('option2', 'Option 2', [
'description' => 'Option 2 description',
'icon' => new pix_icon('t/hide', 'Eye icon 2')
]);
$choice->add_option('option3', 'Option 3', [
'description' => 'Option 3 description',
'icon' => new pix_icon('t/stealth', 'Eye icon 3')
]);
$choice->set_selected_value('option2');
$dialog = new core\output\local\dropdown\status(
'Open dialog',
$choice,
[
'extras' => ['id' => 'statusjscontrols'],
'updatestatus' => true
],
);
echo $OUTPUT->render($dialog);
echo '</div>';
$inlinejs = <<<EOF
require(
['core/local/dropdown/status', 'jquery'],
(Module, jQuery) => {
const status = Module.getDropdownStatus('#statusjscontrols');
const printValue = () => {
const text = 'The status value is ' + status.getSelectedValue() + '.';
document.querySelector('#statusvalue').innerHTML = text;
}
printValue();
document.querySelector('#setselected').addEventListener('click', () => {
if (status.getSelectedValue() == 'option2') {
status.setSelectedValue('option3');
} else {
status.setSelectedValue('option2');
}
});
document.querySelector('#syncbutton').addEventListener('click', (e) => {
if (status.isButtonSyncEnabled()) {
status.setButtonSyncEnabled(false);
} else {
status.setButtonSyncEnabled(true);
}
e.target.innerHTML = (status.isButtonSyncEnabled()) ? 'Disable sync': 'Enable sync';
});
document.querySelector('#updatestatus').addEventListener('click', (e) => {
if (status.isUpdateStatusEnabled()) {
status.setUpdateStatusEnabled(false);
} else {
status.setUpdateStatusEnabled(true);
}
e.target.innerHTML = (status.isUpdateStatusEnabled()) ? 'Disable update': 'Enable update';
});
status.getElement().addEventListener('change', () => {
printValue();
});
}
);
EOF;
$PAGE->requires->js_amd_inline($inlinejs);
echo $OUTPUT->footer();
+13
View File
@@ -117,3 +117,16 @@
}
}
}
.dropdown-item-outline {
&:focus,
&:focus-within {
outline: solid $dropdown-link-active-bg;
}
a:focus,
a:focus-visible {
outline: 0;
}
}
+8
View File
@@ -26083,6 +26083,14 @@ blockquote {
font-size: 0.7rem;
}
.dropdown-item-outline:focus, .dropdown-item-outline:focus-within {
outline: solid #0f6cbf;
}
.dropdown-item-outline a:focus,
.dropdown-item-outline a:focus-visible {
outline: 0;
}
.icon {
font-size: 16px;
width: 16px;
+8
View File
@@ -26083,6 +26083,14 @@ blockquote {
font-size: 0.7rem;
}
.dropdown-item-outline:focus, .dropdown-item-outline:focus-within {
outline: solid #0f6cbf;
}
.dropdown-item-outline a:focus,
.dropdown-item-outline a:focus-visible {
outline: 0;
}
.icon {
font-size: 16px;
width: 16px;