mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 00:12:56 +02:00
Merge branch 'MDL-75155-master' of https://github.com/rezaies/moodle
This commit is contained in:
commit
c9a108973e
@ -27,9 +27,6 @@ use moodle_url;
|
||||
*/
|
||||
class export_action_bar extends action_bar {
|
||||
|
||||
/** @var moodle_url $exportactiveurl The URL that should be set as active in the exports URL selector element. */
|
||||
protected $exportactiveurl;
|
||||
|
||||
/** @var string $activeplugin The plugin of the current export grades page (xml, ods, ...). */
|
||||
protected $activeplugin;
|
||||
|
||||
@ -37,12 +34,14 @@ class export_action_bar extends action_bar {
|
||||
* The class constructor.
|
||||
*
|
||||
* @param \context $context The context object.
|
||||
* @param moodle_url $exportactiveurl The URL that should be set as active in the exports URL selector element.
|
||||
* @param null $unused This parameter has been deprecated since 4.1 and should not be used anymore.
|
||||
* @param string $activeplugin The plugin of the current export grades page (xml, ods, ...).
|
||||
*/
|
||||
public function __construct(\context $context, moodle_url $exportactiveurl, string $activeplugin) {
|
||||
public function __construct(\context $context, $unused, string $activeplugin) {
|
||||
if ($unused !== null) {
|
||||
debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
|
||||
}
|
||||
parent::__construct($context);
|
||||
$this->exportactiveurl = $exportactiveurl;
|
||||
$this->activeplugin = $activeplugin;
|
||||
}
|
||||
|
||||
@ -85,14 +84,18 @@ class export_action_bar extends action_bar {
|
||||
}
|
||||
|
||||
$exportsmenu = [];
|
||||
$exportactiveurl = null;
|
||||
// Generate the data for the exports navigation selector menu.
|
||||
foreach ($exports as $export) {
|
||||
$exportsmenu[$export->link->out()] = $export->string;
|
||||
if ($export->id == $this->activeplugin) {
|
||||
$exportactiveurl = $export->link->out();
|
||||
}
|
||||
}
|
||||
|
||||
// This navigation selector menu will contain the links to all available grade export plugin pages.
|
||||
$exportsurlselect = new \url_select($exportsmenu, $this->exportactiveurl->out(false), null,
|
||||
'gradesexportactionselect');
|
||||
$exportsurlselect = new \core\output\select_menu('exportas', $exportsmenu, $exportactiveurl);
|
||||
$exportsurlselect->set_label(get_string('exportas', 'grades'));
|
||||
$data['exportselector'] = $exportsurlselect->export_for_template($output);
|
||||
|
||||
return $data;
|
||||
|
@ -48,8 +48,7 @@ class export_key_manager_action_bar extends action_bar {
|
||||
}
|
||||
$courseid = $this->context->instanceid;
|
||||
// Get the data used to output the general navigation selector and exports navigation selector.
|
||||
$exportnavselectors = new export_action_bar($this->context,
|
||||
new moodle_url('/grade/export/keymanager.php', ['id' => $courseid]), 'keymanager');
|
||||
$exportnavselectors = new export_action_bar($this->context, null, 'keymanager');
|
||||
$data = $exportnavselectors->export_for_template($output);
|
||||
|
||||
// Add a button to the action bar with a link to the 'add user key' page.
|
||||
|
@ -27,9 +27,6 @@ use moodle_url;
|
||||
*/
|
||||
class import_action_bar extends action_bar {
|
||||
|
||||
/** @var moodle_url $importactiveurl The URL that should be set as active in the imports URL selector element. */
|
||||
protected $importactiveurl;
|
||||
|
||||
/** @var string $activeplugin The plugin of the current import grades page (xml, csv, ...). */
|
||||
protected $activeplugin;
|
||||
|
||||
@ -37,12 +34,14 @@ class import_action_bar extends action_bar {
|
||||
* The class constructor.
|
||||
*
|
||||
* @param \context $context The context object.
|
||||
* @param moodle_url $importactiveurl The URL that should be set as active in the imports URL selector element.
|
||||
* @param null $unused This parameter has been deprecated since 4.1 and should not be used anymore.
|
||||
* @param string $activeplugin The plugin of the current import grades page (xml, csv, ...).
|
||||
*/
|
||||
public function __construct(\context $context, moodle_url $importactiveurl, string $activeplugin) {
|
||||
public function __construct(\context $context, $unused, string $activeplugin) {
|
||||
if ($unused !== null) {
|
||||
debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
|
||||
}
|
||||
parent::__construct($context);
|
||||
$this->importactiveurl = $importactiveurl;
|
||||
$this->activeplugin = $activeplugin;
|
||||
}
|
||||
|
||||
@ -85,14 +84,18 @@ class import_action_bar extends action_bar {
|
||||
}
|
||||
|
||||
$importsmenu = [];
|
||||
$importactiveurl = null;
|
||||
// Generate the data for the imports navigation selector menu.
|
||||
foreach ($imports as $import) {
|
||||
$importsmenu[$import->link->out()] = $import->string;
|
||||
if ($import->id == $this->activeplugin) {
|
||||
$importactiveurl = $import->link->out();
|
||||
}
|
||||
}
|
||||
|
||||
// This navigation selector menu will contain the links to all available grade export plugin pages.
|
||||
$importsurlselect = new \url_select($importsmenu, $this->importactiveurl->out(false), null,
|
||||
'gradesimportactionselect');
|
||||
$importsurlselect = new \core\output\select_menu('importas', $importsmenu, $importactiveurl);
|
||||
$importsurlselect->set_label(get_string('importas', 'grades'));
|
||||
$data['importselector'] = $importsurlselect->export_for_template($output);
|
||||
|
||||
return $data;
|
||||
|
@ -48,8 +48,7 @@ class import_key_manager_action_bar extends action_bar {
|
||||
}
|
||||
$courseid = $this->context->instanceid;
|
||||
// Get the data used to output the general navigation selector and imports navigation selector.
|
||||
$importnavselectors = new import_action_bar($this->context,
|
||||
new moodle_url('/grade/import/keymanager.php', ['id' => $courseid]), 'keymanager');
|
||||
$importnavselectors = new import_action_bar($this->context, null, 'keymanager');
|
||||
$data = $importnavselectors->export_for_template($output);
|
||||
|
||||
// Add a button to the action bar with a link to the 'add user key' page.
|
||||
|
@ -33,7 +33,7 @@ $context = context_course::instance($id);
|
||||
require_capability('moodle/grade:export', $context);
|
||||
require_capability('gradeexport/ods:view', $context);
|
||||
|
||||
$actionbar = new \core_grades\output\export_action_bar($context, $PAGE->url, 'ods');
|
||||
$actionbar = new \core_grades\output\export_action_bar($context, null, 'ods');
|
||||
print_grade_page_head($COURSE->id, 'export', 'ods',
|
||||
get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_ods'),
|
||||
false, false, true, null, null, null, $actionbar);
|
||||
|
@ -33,7 +33,7 @@ $context = context_course::instance($id);
|
||||
require_capability('moodle/grade:export', $context);
|
||||
require_capability('gradeexport/txt:view', $context);
|
||||
|
||||
$actionbar = new \core_grades\output\export_action_bar($context, $PAGE->url, 'txt');
|
||||
$actionbar = new \core_grades\output\export_action_bar($context, null, 'txt');
|
||||
print_grade_page_head($COURSE->id, 'export', 'txt',
|
||||
get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_txt'),
|
||||
false, false, true, null, null, null, $actionbar);
|
||||
|
@ -33,7 +33,7 @@ $context = context_course::instance($id);
|
||||
require_capability('moodle/grade:export', $context);
|
||||
require_capability('gradeexport/xls:view', $context);
|
||||
|
||||
$actionbar = new \core_grades\output\export_action_bar($context, $PAGE->url, 'xls');
|
||||
$actionbar = new \core_grades\output\export_action_bar($context, null, 'xls');
|
||||
print_grade_page_head($COURSE->id, 'export', 'xls',
|
||||
get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_xls'),
|
||||
false, false, true, null, null, null, $actionbar);
|
||||
|
@ -33,7 +33,7 @@ $context = context_course::instance($id);
|
||||
require_capability('moodle/grade:export', $context);
|
||||
require_capability('gradeexport/xml:view', $context);
|
||||
|
||||
$actionbar = new \core_grades\output\export_action_bar($context, $PAGE->url, 'xml');
|
||||
$actionbar = new \core_grades\output\export_action_bar($context, null, 'xml');
|
||||
print_grade_page_head($COURSE->id, 'export', 'xml',
|
||||
get_string('exportto', 'grades') . ' ' . get_string('pluginname', 'gradeexport_xml'),
|
||||
false, false, true, null, null, null, $actionbar);
|
||||
|
@ -51,7 +51,7 @@ $separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and
|
||||
!has_capability('moodle/site:accessallgroups', $context));
|
||||
$currentgroup = groups_get_course_group($course);
|
||||
|
||||
$actionbar = new \core_grades\output\import_action_bar($context, $PAGE->url, 'csv');
|
||||
$actionbar = new \core_grades\output\import_action_bar($context, null, 'csv');
|
||||
print_grade_page_head($course->id, 'import', 'csv', get_string('importcsv', 'grades'), false, false, true,
|
||||
'importcsv', 'grades', null, $actionbar);
|
||||
|
||||
|
@ -47,7 +47,7 @@ $separatemode = (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and
|
||||
!has_capability('moodle/site:accessallgroups', $context));
|
||||
$currentgroup = groups_get_course_group($course);
|
||||
|
||||
$actionbar = new \core_grades\output\import_action_bar($context, $PAGE->url, 'direct');
|
||||
$actionbar = new \core_grades\output\import_action_bar($context, null, 'direct');
|
||||
print_grade_page_head($course->id, 'import', 'direct', get_string('pluginname', 'gradeimport_direct'), false, false, true,
|
||||
'userdata', 'gradeimport_direct', null, $actionbar);
|
||||
|
||||
|
@ -87,7 +87,7 @@ if ($data = $mform->get_data()) {
|
||||
}
|
||||
}
|
||||
|
||||
$actionbar = new \core_grades\output\import_action_bar($context, $PAGE->url, 'xml');
|
||||
$actionbar = new \core_grades\output\import_action_bar($context, null, 'xml');
|
||||
print_grade_page_head($COURSE->id, 'import', 'xml', get_string('importxml', 'grades'),
|
||||
false, false, true, 'importxml', 'gradeimport_xml', null, $actionbar);
|
||||
|
||||
|
@ -44,23 +44,25 @@
|
||||
"title": null
|
||||
},
|
||||
"exportselector": {
|
||||
"id": "url_select56789",
|
||||
"action": "https://example.com/get",
|
||||
"formid": "gradesexportactionselect",
|
||||
"sesskey": "sesskey",
|
||||
"classes": "urlselect",
|
||||
"label": "",
|
||||
"helpicon": false,
|
||||
"showbutton": null,
|
||||
"name": "exportas",
|
||||
"value": "https://example.com/grade/export/ods/index.php",
|
||||
"baseid": "select-menu56789",
|
||||
"label": "Export as",
|
||||
"labelattributes": [
|
||||
{
|
||||
"name": "class",
|
||||
"value": "font-weight-bold"
|
||||
}
|
||||
],
|
||||
"selectedoption": "OpenDocument spreadsheet",
|
||||
"options": [
|
||||
{
|
||||
"name": "OpenDocument spreadsheet",
|
||||
"value": "/grade/export/ods/index.php",
|
||||
"selected": true
|
||||
"value": "https://example.com/grade/export/ods/index.php",
|
||||
"selected": true,
|
||||
"id": "select-menu-option56789"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"title": null
|
||||
]
|
||||
}
|
||||
}
|
||||
}}
|
||||
@ -73,8 +75,13 @@
|
||||
{{/generalnavselector}}
|
||||
{{#exportselector}}
|
||||
<div class="navitem">
|
||||
{{>core/url_select}}
|
||||
{{>core/select_menu}}
|
||||
</div>
|
||||
{{#js}}
|
||||
document.querySelector('#{{baseid}}').addEventListener('change', function(e) {
|
||||
window.location.href = e.target.value;
|
||||
});
|
||||
{{/js}}
|
||||
{{/exportselector}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,23 +44,25 @@
|
||||
"title": null
|
||||
},
|
||||
"importselector": {
|
||||
"id": "url_select56789",
|
||||
"action": "https://example.com/get",
|
||||
"formid": "gradesimportactionselect",
|
||||
"sesskey": "sesskey",
|
||||
"classes": "urlselect",
|
||||
"label": "",
|
||||
"helpicon": false,
|
||||
"showbutton": null,
|
||||
"name": "importas",
|
||||
"value": "https://example.com/grade/import/csv/index.php",
|
||||
"baseid": "select-menu56789",
|
||||
"label": "Import as",
|
||||
"labelattributes": [
|
||||
{
|
||||
"name": "class",
|
||||
"value": "font-weight-bold"
|
||||
}
|
||||
],
|
||||
"selectedoption": "CSV file",
|
||||
"options": [
|
||||
{
|
||||
"name": "CSV file",
|
||||
"value": "/grade/import/csv/index.php",
|
||||
"selected": true
|
||||
"value": "https://example.com/grade/import/csv/index.php",
|
||||
"selected": true,
|
||||
"id": "select-menu-option56789"
|
||||
}
|
||||
],
|
||||
"disabled": false,
|
||||
"title": null
|
||||
]
|
||||
}
|
||||
}
|
||||
}}
|
||||
@ -73,8 +75,13 @@
|
||||
{{/generalnavselector}}
|
||||
{{#importselector}}
|
||||
<div class="navitem">
|
||||
{{>core/url_select}}
|
||||
{{>core/select_menu}}
|
||||
</div>
|
||||
{{#js}}
|
||||
document.querySelector('#{{baseid}}').addEventListener('change', function(e) {
|
||||
window.location.href = e.target.value;
|
||||
});
|
||||
{{/js}}
|
||||
{{/importselector}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -349,7 +349,7 @@ class behat_grade extends behat_base {
|
||||
*/
|
||||
public function i_navigate_to_import_page_in_the_course_gradebook($gradeimportoption) {
|
||||
$this->i_navigate_to_in_the_course_gradebook("More > Import");
|
||||
$this->select_in_gradebook_navigation_selector($gradeimportoption, 'gradesimportactionselect');
|
||||
$this->execute('behat_forms::i_set_the_field_to', [get_string('importas', 'grades'), $gradeimportoption]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -364,7 +364,7 @@ class behat_grade extends behat_base {
|
||||
*/
|
||||
public function i_navigate_to_export_page_in_the_course_gradebook($gradeexportoption) {
|
||||
$this->i_navigate_to_in_the_course_gradebook("More > Export");
|
||||
$this->select_in_gradebook_navigation_selector($gradeexportoption, 'gradesexportactionselect');
|
||||
$this->execute('behat_forms::i_set_the_field_to', [get_string('exportas', 'grades'), $gradeexportoption]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,10 @@
|
||||
This file describes API changes in /grade/* ;
|
||||
Information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1 ===
|
||||
* The $importactiveurl parameter in the constructor of the core_grades\output\import_action_bar class has been deprecated and is not used anymore.
|
||||
* The $exportactiveurl parameter in the constructor of the core_grades\output\export_action_bar class has been deprecated and is not used anymore.
|
||||
|
||||
=== 4.0 ===
|
||||
|
||||
* The select_in_gradebook_tabs() function in behat_grade.php has been deprecated. Please use the function
|
||||
|
@ -212,6 +212,7 @@ $string['excluded_help'] = 'If ticked, the grade will not be included in any agg
|
||||
$string['expand'] = 'Expand category';
|
||||
$string['expandcriterion'] = 'Expand criterion';
|
||||
$string['export'] = 'Export';
|
||||
$string['exportas'] = 'Export as';
|
||||
$string['exportalloutcomes'] = 'Export all outcomes';
|
||||
$string['exportfeedback'] = 'Include feedback in export';
|
||||
$string['exportfeedback_desc'] = 'This can be overridden during export.';
|
||||
@ -400,6 +401,7 @@ $string['identifier'] = 'Identify user by';
|
||||
$string['idnumbers'] = 'ID numbers';
|
||||
$string['ignore'] = 'Ignore';
|
||||
$string['import'] = 'Import';
|
||||
$string['importas'] = 'Import as';
|
||||
$string['importcsv'] = 'Import CSV';
|
||||
$string['importcsv_help'] = 'Grades can be imported via a CSV file with format as follows:
|
||||
|
||||
|
@ -190,6 +190,12 @@ class behat_field_manager {
|
||||
}
|
||||
}
|
||||
|
||||
if ($tagname == 'div') {
|
||||
if ($node->getAttribute('role') == 'combobox') {
|
||||
return 'select_menu';
|
||||
}
|
||||
}
|
||||
|
||||
// We can not provide a closer field type.
|
||||
return false;
|
||||
}
|
||||
|
@ -279,6 +279,11 @@ XPATH
|
||||
'date_time' => <<<XPATH
|
||||
.//fieldset[(%idMatch% or ./legend[%exactTagTextMatch%]) and (@data-fieldtype='date' or @data-fieldtype='date_time')]
|
||||
XPATH
|
||||
,
|
||||
'select_menu' => <<<XPATH
|
||||
//*[@role='combobox'][@aria-labelledby = //label[contains(normalize-space(string(.)), %locator%)]/@id]
|
||||
XPATH
|
||||
,
|
||||
],
|
||||
];
|
||||
|
||||
|
48
lib/behat/form_field/behat_form_select_menu.php
Normal file
48
lib/behat/form_field/behat_form_select_menu.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once(__DIR__ . '/behat_form_field.php');
|
||||
|
||||
/**
|
||||
* Custom interaction with select_menu elements
|
||||
*
|
||||
* @package core_form
|
||||
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_form_select_menu extends behat_form_field {
|
||||
public function set_value($value) {
|
||||
self::require_javascript();
|
||||
|
||||
$rootnode = $this->field->getParent();
|
||||
$options = $rootnode->findAll('css', '[role=option]');
|
||||
$this->field->click();
|
||||
foreach ($options as $option) {
|
||||
if (trim($option->getHtml()) == $value) {
|
||||
$option->click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function get_value() {
|
||||
$rootnode = $this->field->getParent();
|
||||
$input = $rootnode->find('css', 'input');
|
||||
return $input->getValue();
|
||||
}
|
||||
}
|
145
lib/classes/output/select_menu.php
Normal file
145
lib/classes/output/select_menu.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core\output;
|
||||
|
||||
use renderer_base;
|
||||
|
||||
/**
|
||||
* A single-select combobox widget that is functionally similar to an HTML select element.
|
||||
*
|
||||
* @package core
|
||||
* @category output
|
||||
* @copyright 2022 Shamim Rezaie <shamim@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class select_menu implements \renderable, \templatable {
|
||||
/** @var array List of options. */
|
||||
protected $options;
|
||||
|
||||
/** @var string|null The value of the preselected option. */
|
||||
protected $selected;
|
||||
|
||||
/** @var string The combobox label */
|
||||
protected $label;
|
||||
|
||||
/** @var array Button label's attributes */
|
||||
protected $labelattributes;
|
||||
|
||||
/** @var string Name of the combobox element */
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* select_menu constructor.
|
||||
*
|
||||
* @param string $name Name of the combobox element
|
||||
* @param array $options List of options in an associative array format like ['val' => 'Option'].
|
||||
* Supports grouped options as well.
|
||||
* @param string|null $selected The value of the preselected option.
|
||||
*/
|
||||
public function __construct(string $name, array $options, string $selected = null) {
|
||||
$this->name = $name;
|
||||
$this->options = $options;
|
||||
$this->selected = $selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the select menu's label.
|
||||
*
|
||||
* @param string $label The label.
|
||||
* @param array $attributes List of attributes to apply on the label element.
|
||||
*/
|
||||
public function set_label(string $label, array $attributes = []) {
|
||||
$this->label = $label;
|
||||
$this->labelattributes = $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten the options for Mustache.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function flatten_options(): array {
|
||||
$flattened = [];
|
||||
|
||||
foreach ($this->options as $value => $option) {
|
||||
if (is_array($option)) {
|
||||
foreach ($option as $groupname => $optoptions) {
|
||||
if (!isset($flattened[$groupname])) {
|
||||
$flattened[$groupname] = [
|
||||
'name' => $groupname,
|
||||
'isgroup' => true,
|
||||
'id' => \html_writer::random_id('select-menu-group'),
|
||||
'options' => []
|
||||
];
|
||||
}
|
||||
foreach ($optoptions as $optvalue => $optoption) {
|
||||
$flattened[$groupname]['options'][$optvalue] = [
|
||||
'name' => $optoption,
|
||||
'value' => $optvalue,
|
||||
'selected' => $this->selected == $optvalue,
|
||||
'id' => \html_writer::random_id('select-menu-option'),
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$flattened[$value] = [
|
||||
'name' => $option,
|
||||
'value' => $value,
|
||||
'selected' => $this->selected == $value,
|
||||
'id' => \html_writer::random_id('select-menu-option'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Make non-associative array.
|
||||
foreach ($flattened as $key => $value) {
|
||||
if (!empty($value['options'])) {
|
||||
$flattened[$key]['options'] = array_values($value['options']);
|
||||
}
|
||||
}
|
||||
$flattened = array_values($flattened);
|
||||
|
||||
return $flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export for template.
|
||||
*
|
||||
* @param renderer_base $output The renderer.
|
||||
* @return \stdClass
|
||||
*/
|
||||
public function export_for_template(renderer_base $output): \stdClass {
|
||||
$data = new \stdClass();
|
||||
$data->baseid = \html_writer::random_id('select-menu');
|
||||
$data->label = $this->label;
|
||||
$data->options = $this->flatten_options($this->options);
|
||||
$data->selectedoption = array_column($data->options, 'name', 'value')[$this->selected];
|
||||
$data->name = $this->name;
|
||||
$data->value = $this->selected;
|
||||
|
||||
// Label attributes.
|
||||
$data->labelattributes = [];
|
||||
// Map the label attributes.
|
||||
foreach ($this->labelattributes as $key => $value) {
|
||||
$data->labelattributes[] = ['name' => $key, 'value' => $value];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
133
lib/templates/select_menu.mustache
Normal file
133
lib/templates/select_menu.mustache
Normal file
@ -0,0 +1,133 @@
|
||||
{{!
|
||||
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/>.
|
||||
}}
|
||||
{{!
|
||||
@template core/select_menu
|
||||
|
||||
Template for select_menu output component.
|
||||
|
||||
Context variables required for this template:
|
||||
* name - name of the form element
|
||||
* value - value of the form element
|
||||
* baseid - id of the dropdown element and to be used to generate id for other elements used internally
|
||||
* label - Element label
|
||||
* labelattributes - Label attributes.
|
||||
* selectedoption - Text of the selected option
|
||||
* options - Array of options for the select with value, name, selected, isgroup and id properites.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"name": "menuname",
|
||||
"value": "opt2",
|
||||
"baseid": "select-menu56789",
|
||||
"label": "Select one option",
|
||||
"labelattributes": [
|
||||
{
|
||||
"name": "class",
|
||||
"value": "font-weight-bold"
|
||||
}
|
||||
],
|
||||
"selectedoption": "Second option",
|
||||
"options": [
|
||||
{
|
||||
"name": "First option",
|
||||
"value": "opt1",
|
||||
"id": "select-menu-option1",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"name": "Second option",
|
||||
"value": "opt2",
|
||||
"id": "select-menu-option2",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"isgroup": {
|
||||
"name": "First group",
|
||||
"id": "select-menu-group1",
|
||||
"options": [
|
||||
{
|
||||
"name": "Third option",
|
||||
"value": "opt3",
|
||||
"id": "select-menu-option3",
|
||||
"selected": false
|
||||
},
|
||||
{
|
||||
"name": "Fourth option",
|
||||
"value": "opt4",
|
||||
"id": "select-menu-option4",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Fifth option",
|
||||
"value": "opt5",
|
||||
"id": "select-menu-option5",
|
||||
"selected": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
<div class="dropdown select-menu" id="{{baseid}}">
|
||||
{{#label}}
|
||||
<label id="{{baseid}}-label"{{#labelattributes}} {{name}}="{{value}}"{{/labelattributes}}>{{label}}</label>
|
||||
{{/label}}
|
||||
<div
|
||||
class="btn dropdown-toggle"
|
||||
role="combobox"
|
||||
data-toggle="dropdown"
|
||||
{{#label}}aria-labelledby="{{baseid}}-label{{/label}}"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="false"
|
||||
aria-controls="{{baseid}}-listbox"
|
||||
tabindex="0"
|
||||
>
|
||||
{{selectedoption}}
|
||||
</div>
|
||||
<ul class="dropdown-menu" role="listbox" id="{{baseid}}-listbox" {{#label}}aria-labelledby="{{baseid}}-label{{/label}}">
|
||||
{{#options}}
|
||||
{{#isgroup}}
|
||||
<li role="none">
|
||||
<ul role="group" aria-labelledby="{{id}}">
|
||||
<li role="presentation" id="{{id}}">{{name}}</li>
|
||||
{{#options}}
|
||||
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
|
||||
{{name}}
|
||||
</li>
|
||||
{{/options}}
|
||||
</ul>
|
||||
</li>
|
||||
{{/isgroup}}
|
||||
{{^isgroup}}
|
||||
<li class="dropdown-item" role="option" id="{{id}}" data-value="{{value}}" {{#selected}}aria-selected="true"{{/selected}}>
|
||||
{{name}}
|
||||
</li>
|
||||
{{/isgroup}}
|
||||
{{/options}}
|
||||
</ul>
|
||||
<input type="hidden" name="{{name}}" value="{{value}}" />
|
||||
</div>
|
||||
{{#js}}
|
||||
var label = document.getElementById('{{baseid}}-label');
|
||||
if (label) {
|
||||
label.addEventListener('click', function() {
|
||||
label.parentElement.querySelector('.dropdown-toggle').focus();
|
||||
});
|
||||
}
|
||||
{{/js}}
|
2
theme/boost/amd/build/aria.min.js
vendored
2
theme/boost/amd/build/aria.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -199,6 +199,144 @@ const dropdownFix = () => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* A lot of Bootstrap's out of the box features don't work if dropdown items are not focusable.
|
||||
*/
|
||||
const comboboxFix = () => {
|
||||
$(document).on('show.bs.dropdown', e => {
|
||||
if (e.relatedTarget.matches('[role="combobox"]')) {
|
||||
const combobox = e.relatedTarget;
|
||||
const listbox = combobox.parentElement.querySelector('[role="listbox"]');
|
||||
const selectedOption = listbox.querySelector('[role="option"][aria-selected="true"]');
|
||||
|
||||
// To make sure ArrowDown doesn't move the active option afterwards.
|
||||
setTimeout(() => {
|
||||
if (selectedOption) {
|
||||
selectedOption.classList.add('active');
|
||||
combobox.setAttribute('aria-activedescendant', selectedOption.id);
|
||||
} else {
|
||||
const firstOption = listbox.querySelector('[role="option"]');
|
||||
firstOption.setAttribute('aria-selected', 'true');
|
||||
firstOption.classList.add('active');
|
||||
combobox.setAttribute('aria-activedescendant', firstOption.id);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('hidden.bs.dropdown', e => {
|
||||
if (e.relatedTarget.matches('[role="combobox"]')) {
|
||||
const combobox = e.relatedTarget;
|
||||
const listbox = combobox.parentElement.querySelector('[role="listbox"]');
|
||||
|
||||
combobox.removeAttribute('aria-activedescendant');
|
||||
|
||||
setTimeout(() => {
|
||||
// Undo all previously highlighted options.
|
||||
listbox.querySelectorAll('.active[role="option"]').forEach(option => {
|
||||
option.classList.remove('active');
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Handling keyboard events for both navigating through and selecting options.
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.target.matches('.select-menu [role="combobox"]')) {
|
||||
const combobox = e.target;
|
||||
const trigger = e.key;
|
||||
let next = null;
|
||||
const options = combobox.parentElement.querySelectorAll('[role="listbox"] [role="option"]');
|
||||
const activeOption = combobox.parentElement.querySelector('[role="listbox"] .active[role="option"]');
|
||||
|
||||
// Under the special case that the dropdown menu is being shown as a result of they key press (like when the user
|
||||
// presses ArrowDown or Enter or ... to open the dropdown menu), activeOption is not set yet.
|
||||
// It's because of a race condition with show.bs.dropdown event handler.
|
||||
if (options && activeOption) {
|
||||
if (trigger == 'ArrowDown') {
|
||||
for (let i = 0; i < options.length - 1; i++) {
|
||||
if (options[i] == activeOption) {
|
||||
next = options[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} if (trigger == 'ArrowUp') {
|
||||
for (let i = 1; i < options.length; i++) {
|
||||
if (options[i] == activeOption) {
|
||||
next = options[i - 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (trigger == 'Home') {
|
||||
next = options[0];
|
||||
} else if (trigger == 'End') {
|
||||
next = options[options.length - 1];
|
||||
} else if (trigger == ' ' || trigger == 'Enter') {
|
||||
selectOption(combobox, activeOption);
|
||||
} else {
|
||||
// Search for options by finding the first option that has
|
||||
// text starting with the typed character (case insensitive).
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const option = options[i];
|
||||
const optionText = option.textContent.trim().toLowerCase();
|
||||
const keyPressed = e.key.toLowerCase();
|
||||
if (optionText.indexOf(keyPressed) == 0) {
|
||||
next = option;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Variable next is set if we do want to act on the keypress.
|
||||
if (next) {
|
||||
e.preventDefault();
|
||||
activeOption.classList.remove('active');
|
||||
next.classList.add('active');
|
||||
combobox.setAttribute('aria-activedescendant', next.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (e.target.matches('.select-menu [role="option"]')) {
|
||||
const option = e.target;
|
||||
const combobox = option.closest('.select-menu').querySelector('[role="combobox"]');
|
||||
combobox.focus();
|
||||
selectOption(combobox, option);
|
||||
}
|
||||
});
|
||||
|
||||
// In case some code somewhere else changes the value of the combobox.
|
||||
document.addEventListener('change', e => {
|
||||
if (e.target.matches('.select-menu input[type="hidden"]')) {
|
||||
const combobox = e.target.parentElement.querySelector('[role="combobox"]');
|
||||
const option = e.target.parentElement.querySelector(`[role="option"][data-value="${e.target.value}"]`);
|
||||
|
||||
if (combobox && option) {
|
||||
selectOption(combobox, option);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const selectOption = (combobox, option) => {
|
||||
const oldSelectedOption = combobox.parentElement.querySelector('[role="listbox"] [role="option"][aria-selected="true"]');
|
||||
const inputElement = combobox.parentElement.querySelector('input[type="hidden"]');
|
||||
|
||||
if (oldSelectedOption != option) {
|
||||
if (oldSelectedOption) {
|
||||
oldSelectedOption.removeAttribute('aria-selected');
|
||||
}
|
||||
option.setAttribute('aria-selected', 'true');
|
||||
}
|
||||
combobox.textContent = option.textContent;
|
||||
if (inputElement.value != option.dataset.value) {
|
||||
inputElement.value = option.dataset.value;
|
||||
inputElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* After page load, focus on any element with special autofocus attribute.
|
||||
*/
|
||||
@ -303,6 +441,7 @@ const collapseFix = () => {
|
||||
|
||||
export const init = () => {
|
||||
dropdownFix();
|
||||
comboboxFix();
|
||||
autoFocus();
|
||||
tabElementFix();
|
||||
collapseFix();
|
||||
|
@ -2369,6 +2369,7 @@ $footer-link-color: $bg-inverse-link-color !default;
|
||||
width: 100%;
|
||||
color: $body-color;
|
||||
}
|
||||
&.active,
|
||||
&:active,
|
||||
&:hover,
|
||||
&:focus,
|
||||
@ -2380,13 +2381,14 @@ $footer-link-color: $bg-inverse-link-color !default;
|
||||
color: $dropdown-link-active-color;
|
||||
}
|
||||
}
|
||||
&[aria-current="true"] {
|
||||
&[aria-current="true"],
|
||||
&[aria-selected="true"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:before {
|
||||
@include fa-icon();
|
||||
content: $fa-var-circle;
|
||||
content: $fa-var-check;
|
||||
position: absolute;
|
||||
left: 0.4rem;
|
||||
font-size: 0.7rem;
|
||||
@ -2943,3 +2945,22 @@ body.dragging {
|
||||
width: 9px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.select-menu {
|
||||
ul[role="group"] {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
li:first-child {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
padding: 0.25rem 1.5rem;
|
||||
display: block;
|
||||
}
|
||||
.dropdown-item {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
}
|
||||
.dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
@ -11758,25 +11758,25 @@ ul {
|
||||
width: 100%;
|
||||
color: #1d2125; }
|
||||
|
||||
.dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within {
|
||||
.dropdown-item.active, .dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within {
|
||||
outline: 0;
|
||||
background-color: #0f6cbf;
|
||||
color: #fff; }
|
||||
.dropdown-item:active a, .dropdown-item:hover a, .dropdown-item:focus a, .dropdown-item:focus-within a {
|
||||
.dropdown-item.active a, .dropdown-item:active a, .dropdown-item:hover a, .dropdown-item:focus a, .dropdown-item:focus-within a {
|
||||
color: #fff; }
|
||||
|
||||
.dropdown-item[aria-current="true"] {
|
||||
.dropdown-item[aria-current="true"], .dropdown-item[aria-selected="true"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.dropdown-item[aria-current="true"]:before {
|
||||
.dropdown-item[aria-current="true"]:before, .dropdown-item[aria-selected="true"]:before {
|
||||
display: inline-block;
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
content: "";
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.4rem;
|
||||
font-size: 0.7rem; }
|
||||
@ -12260,6 +12260,20 @@ body.dragging .dragging {
|
||||
width: 9px;
|
||||
border: 0; }
|
||||
|
||||
.select-menu ul[role="group"] {
|
||||
padding: 0;
|
||||
margin: 0; }
|
||||
.select-menu ul[role="group"] li:first-child {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
padding: 0.25rem 1.5rem;
|
||||
display: block; }
|
||||
.select-menu ul[role="group"] .dropdown-item {
|
||||
padding-left: 3rem; }
|
||||
|
||||
.select-menu .dropdown-item {
|
||||
cursor: pointer; }
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
|
@ -11758,25 +11758,25 @@ ul {
|
||||
width: 100%;
|
||||
color: #1d2125; }
|
||||
|
||||
.dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within {
|
||||
.dropdown-item.active, .dropdown-item:active, .dropdown-item:hover, .dropdown-item:focus, .dropdown-item:focus-within {
|
||||
outline: 0;
|
||||
background-color: #0f6cbf;
|
||||
color: #fff; }
|
||||
.dropdown-item:active a, .dropdown-item:hover a, .dropdown-item:focus a, .dropdown-item:focus-within a {
|
||||
.dropdown-item.active a, .dropdown-item:active a, .dropdown-item:hover a, .dropdown-item:focus a, .dropdown-item:focus-within a {
|
||||
color: #fff; }
|
||||
|
||||
.dropdown-item[aria-current="true"] {
|
||||
.dropdown-item[aria-current="true"], .dropdown-item[aria-selected="true"] {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center; }
|
||||
.dropdown-item[aria-current="true"]:before {
|
||||
.dropdown-item[aria-current="true"]:before, .dropdown-item[aria-selected="true"]:before {
|
||||
display: inline-block;
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
content: "";
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0.4rem;
|
||||
font-size: 0.7rem; }
|
||||
@ -12260,6 +12260,20 @@ body.dragging .dragging {
|
||||
width: 9px;
|
||||
border: 0; }
|
||||
|
||||
.select-menu ul[role="group"] {
|
||||
padding: 0;
|
||||
margin: 0; }
|
||||
.select-menu ul[role="group"] li:first-child {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
padding: 0.25rem 1.5rem;
|
||||
display: block; }
|
||||
.select-menu ul[role="group"] .dropdown-item {
|
||||
padding-left: 3rem; }
|
||||
|
||||
.select-menu .dropdown-item {
|
||||
cursor: pointer; }
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
|
Loading…
x
Reference in New Issue
Block a user