Compare commits
269 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2d0712748 | |||
| f70a6d32cd | |||
| f691ccf8c6 | |||
| ceb2f856b0 | |||
| 955d89710e | |||
| 526e1af88b | |||
| b142840de1 | |||
| 8fb9177b74 | |||
| 6e15a26dce | |||
| b5a65e56fd | |||
| a6a0e98e53 | |||
| c28cd3215b | |||
| 39084a098b | |||
| 9290627da9 | |||
| bb5bd2eed8 | |||
| 12195684b4 | |||
| f01ad40145 | |||
| 1d64897a3c | |||
| 339dfec420 | |||
| 1d16c049f3 | |||
| 4d4635228d | |||
| ce9cb1156c | |||
| c0429c1c90 | |||
| be8c02fc2c | |||
| 99b5e1059d | |||
| ee519b690c | |||
| 814c63fcc3 | |||
| 799e962f1f | |||
| c0bea2a82d | |||
| e9fcdec98b | |||
| fc6764324a | |||
| b71f01981a | |||
| 5d1fcb942a | |||
| 24335d67bb | |||
| 1b828701a6 | |||
| f8e468c41a | |||
| b07df211ce | |||
| 4c289966d3 | |||
| 0d0fbec616 | |||
| ca751bdc1f | |||
| bc85007834 | |||
| dd553c1406 | |||
| 76ab8a1c51 | |||
| 242ecbf7f3 | |||
| cbfde8c11a | |||
| df4f8dd322 | |||
| 60ab09ac87 | |||
| 40fad4d6b7 | |||
| 1265b0ad84 | |||
| faa121ad30 | |||
| 797cbcc3da | |||
| 6bac910434 | |||
| a9b4b297ac | |||
| 073deca0e1 | |||
| 2cc1182b16 | |||
| 82a7b26648 | |||
| 2db1ddd290 | |||
| 96f0478ec1 | |||
| 38f5c818cb | |||
| 38c35247e9 | |||
| 242dcfbc55 | |||
| 2f601f1e51 | |||
| 31a3c8cda4 | |||
| 39cf556111 | |||
| 4c4be40c77 | |||
| c8f105800c | |||
| 3502ad3da7 | |||
| 15a5a938b6 | |||
| ec4eacd1da | |||
| bc34e3fef3 | |||
| 6aef92c24a | |||
| 4b7c58febf | |||
| 4ed6c8fbce | |||
| af585fc755 | |||
| 6ae5ee18a0 | |||
| ead7958219 | |||
| 8dc9cd6cdc | |||
| 69749509ab | |||
| 59eac981eb | |||
| ab0038da84 | |||
| 2cde957897 | |||
| a8eb481980 | |||
| 4c533b8204 | |||
| 26aaa7486a | |||
| f336875b1a | |||
| 6381daf52a | |||
| 5b8ad6ae30 | |||
| 3a38791d62 | |||
| 5563e99f2c | |||
| 8bc1c231d6 | |||
| 1226208a64 | |||
| 725277eb8b | |||
| b631966a70 | |||
| b796cbd03c | |||
| 1066f5fe68 | |||
| 202718f968 | |||
| 44d734147a | |||
| 346cb39cff | |||
| eb12428324 | |||
| 8b31922739 | |||
| 127174088a | |||
| f8e2445513 | |||
| cb8bf1e001 | |||
| 1f7063cc09 | |||
| 60954253d4 | |||
| d49ebb79c5 | |||
| 85a7c17414 | |||
| c9b7b94ed4 | |||
| 0bc265e900 | |||
| 6417d795b9 | |||
| c55473ad2e | |||
| ddaf4b7a58 | |||
| be58d68f20 | |||
| 131d80441a | |||
| 8a588f963c | |||
| 4e32a42415 | |||
| aa9a462a4b | |||
| 571c59b60e | |||
| 360f9e37f8 | |||
| 65ba7df04b | |||
| 02aed7f6fc | |||
| 2a4f86051b | |||
| 701541f35b | |||
| 839681dc77 | |||
| b33e5466df | |||
| fbbdfefdb6 | |||
| 8656271a63 | |||
| e1f3e7f232 | |||
| f6c4303c58 | |||
| 134dc470c7 | |||
| 45cd887f3b | |||
| b65ec774c5 | |||
| 215e9f0688 | |||
| 035ebed95b | |||
| a78f3a02c6 | |||
| 2890546f44 | |||
| a1df38dfc6 | |||
| 82f9585f55 | |||
| 0f092b86b5 | |||
| 74f3c34cbf | |||
| 1f337e7665 | |||
| fbb6dc4ca3 | |||
| ab3a244568 | |||
| 7b13a543ad | |||
| dd10dead0f | |||
| b84a48d1ee | |||
| 3794210019 | |||
| cca75ca956 | |||
| 93c91af6a9 | |||
| 8755738360 | |||
| 4f8c5ef086 | |||
| 5d10e53ba1 | |||
| cc0c62d564 | |||
| 48fb5b6e88 | |||
| bc4b6cf8f7 | |||
| f17b006dcb | |||
| d50bb07b2b | |||
| b85f6a660b | |||
| dd584ab981 | |||
| ab9b09908a | |||
| c99abd65ca | |||
| 436ed2c4cb | |||
| d0ddc7692b | |||
| 449a8b0b31 | |||
| 0f251ce165 | |||
| 5df8e8fa5f | |||
| 7f6888bd53 | |||
| 95d5b0aab0 | |||
| 9a05e393dd | |||
| 110ec6a703 | |||
| 714764d966 | |||
| ae63139985 | |||
| e144ff8a43 | |||
| cd528df093 | |||
| 6b96cf8676 | |||
| 2b0a0cecdf | |||
| 7b614d6f51 | |||
| e7cda153a2 | |||
| 0d6eaef200 | |||
| f32c46bf0c | |||
| eeae99afc7 | |||
| 8c9b54cc5d | |||
| da42c76300 | |||
| c2714ff1af | |||
| f2cbacbd24 | |||
| bc1d7e854b | |||
| 99bc8f2b30 | |||
| 67f739499b | |||
| 538e17e199 | |||
| 28d8c9c1e7 | |||
| 18703b71ac | |||
| f59220a76d | |||
| 84198e03c7 | |||
| f5d445a68f | |||
| 30b0b0cf40 | |||
| 69948eda10 | |||
| 1f4daa7402 | |||
| c1c1ca6e4d | |||
| 50d1671a54 | |||
| fd9b8bf4d0 | |||
| e405e6fd6a | |||
| 5898c3e5dd | |||
| d2bc54fba9 | |||
| 9b07f56add | |||
| 7c1c1071e7 | |||
| d2d49e150b | |||
| a7d71e0e2d | |||
| b924f6a355 | |||
| 3fa384384e | |||
| 66e88ace0f | |||
| fe07140e27 | |||
| fad4e118a4 | |||
| 0d80eea9af | |||
| 0b973b52e9 | |||
| b158354710 | |||
| ee3924702f | |||
| 0d4d201a8c | |||
| 0b2085648e | |||
| 939a416308 | |||
| 28d1db2f1d | |||
| 084c120c79 | |||
| 29a1cf86a3 | |||
| ea915c29e5 | |||
| deced83b06 | |||
| fac1828222 | |||
| 502536c16e | |||
| 3fee9d949f | |||
| 1bfc0a1d14 | |||
| f8301fb4bc | |||
| 9e97a353ad | |||
| 6e27e9e481 | |||
| e8cbc0b653 | |||
| b296667ebf | |||
| 231c25e498 | |||
| 3ff1293c12 | |||
| eda00e2c73 | |||
| 85b4e2312e | |||
| 5707e7d40f | |||
| 71e751565c | |||
| b3adaaf47f | |||
| 282df774a4 | |||
| 9f5492df71 | |||
| 0920bd0810 | |||
| a89cba84a1 | |||
| f0b3ff2134 | |||
| dbd7cedd24 | |||
| d5331827c4 | |||
| 91f503c3f6 | |||
| 4ee053a278 | |||
| 4bc3782c4c | |||
| 5a7ad7b284 | |||
| 34c452afb4 | |||
| 6c260e6ae7 | |||
| 09e74578a1 | |||
| f1a9181a77 | |||
| 8ee689f612 | |||
| a003b3b882 | |||
| 9617555887 | |||
| a5238d8c94 | |||
| 34e2d74921 | |||
| 76cb263244 | |||
| 4ce5755a63 | |||
| a509b37926 | |||
| 7e96a1c887 | |||
| c90c9b04f6 | |||
| 9fbd5fb219 | |||
| a00d72c622 | |||
| f23463f8d6 | |||
| c55f219d77 |
@@ -156,7 +156,7 @@ const getThirdPartyLibsList = relativeTo => {
|
||||
* @returns {array}
|
||||
*/
|
||||
const getThirdPartyPaths = () => {
|
||||
const DOMParser = require('xmldom').DOMParser;
|
||||
const DOMParser = require('@xmldom/xmldom').DOMParser;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const xpath = require('xpath');
|
||||
|
||||
@@ -45,6 +45,7 @@ module.exports = grunt => {
|
||||
grunt.log.write(result);
|
||||
|
||||
if (error) {
|
||||
grunt.log.error(result.stdout);
|
||||
process.exit(code);
|
||||
}
|
||||
done();
|
||||
@@ -56,7 +57,7 @@ module.exports = grunt => {
|
||||
* @param {array} args
|
||||
* @returns {object}
|
||||
*/
|
||||
const spawnNodeCall = args => grunt.util.spawn({
|
||||
const spawnNodeCall = (args) => grunt.util.spawn({
|
||||
cmd: 'node',
|
||||
args,
|
||||
}, getSpawnHandler(grunt.task.current.async()));
|
||||
@@ -91,13 +92,14 @@ module.exports = grunt => {
|
||||
* @returns {Object} Reference to the spawned task
|
||||
*/
|
||||
const cssBuild = () => spawnNodeCall([
|
||||
'node_modules/node-sass/bin/node-sass',
|
||||
'--output-style', 'expanded',
|
||||
'--source-map', true,
|
||||
'--source-map-contents', true,
|
||||
'node_modules/sass/sass.js',
|
||||
'--style', 'expanded',
|
||||
'--source-map',
|
||||
'--embed-sources',
|
||||
'--precision', 6,
|
||||
'--load-path', process.cwd(),
|
||||
getCLPath('hugo/scss/docs.scss'),
|
||||
'-o', getCLPath('hugo/dist/css/'),
|
||||
getCLPath('hugo/dist/css/docs.css'),
|
||||
]);
|
||||
|
||||
// Register the various component library tasks.
|
||||
|
||||
@@ -32,7 +32,7 @@ module.exports = grunt => {
|
||||
}
|
||||
},
|
||||
options: {
|
||||
implementation: require('node-sass'),
|
||||
implementation: require('sass'),
|
||||
includePaths: ["theme/boost/scss/", "theme/classic/scss/"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Reset course indentation
|
||||
*
|
||||
* @copyright 2023 Amaia Anabitarte <amaia@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once('../../config.php');
|
||||
|
||||
require_admin();
|
||||
|
||||
$format = required_param('format', PARAM_PLUGIN);
|
||||
$confirm = optional_param('confirm', false, PARAM_BOOL);
|
||||
$backurl = new moodle_url('/admin/settings.php', ['section' => 'formatsetting'.$format]);
|
||||
|
||||
$PAGE->set_url('/admin/course/resetindentation.php', ['format' => $format]);
|
||||
$PAGE->set_context(context_system::instance());
|
||||
|
||||
if ($confirm) {
|
||||
require_sesskey();
|
||||
$courses = $DB->get_records('course', ['format' => $format], 'id', 'id');
|
||||
if (!empty($courses)) {
|
||||
$courseids = array_keys($courses);
|
||||
list($courseinsql, $courseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED, 'course');
|
||||
$DB->set_field_select('course_modules', 'indent', '0', "course $courseinsql AND indent <> 0", $courseparams);
|
||||
rebuild_course_cache(0, true);
|
||||
}
|
||||
redirect(
|
||||
$backurl,
|
||||
get_string('resetindentationsuccess', 'admin'),
|
||||
null,
|
||||
\core\output\notification::NOTIFY_SUCCESS
|
||||
);
|
||||
}
|
||||
|
||||
$strtitle = get_string('resetindentation', 'admin');
|
||||
|
||||
$PAGE->set_title($strtitle);
|
||||
$PAGE->set_heading($strtitle);
|
||||
|
||||
navigation_node::override_active_url(new moodle_url(
|
||||
'/admin/course/resetindentation.php',
|
||||
['action' => 'confirm', 'format' => $format]
|
||||
));
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
$displayoptions = ['confirmtitle' => get_string('resetindentation_title', 'admin')];
|
||||
$confirmbutton = new single_button(
|
||||
new moodle_url('/admin/course/resetindentation.php', ['confirm' => 1, 'format' => $format, 'sesskey' => sesskey()]),
|
||||
get_string('resetindentation', 'admin'),
|
||||
'post'
|
||||
);
|
||||
$cancelbutton = new single_button($backurl, get_string('cancel'));
|
||||
echo $OUTPUT->confirm(
|
||||
get_string('resetindentation_help', 'admin', ['format' => get_string('pluginname', 'format_'.$format)]),
|
||||
$confirmbutton,
|
||||
$cancelbutton,
|
||||
$displayoptions
|
||||
);
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
@@ -4109,6 +4109,8 @@
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
+23
-12
@@ -30,25 +30,36 @@
|
||||
|
||||
require_once(__DIR__ . '/../config.php');
|
||||
|
||||
// The state parameter we've given (used in moodle as a redirect url).
|
||||
// Per https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1, state is required, even during error responses.
|
||||
$state = required_param('state', PARAM_LOCALURL);
|
||||
$redirecturl = new moodle_url($state);
|
||||
$params = $redirecturl->params();
|
||||
|
||||
$error = optional_param('error', '', PARAM_RAW);
|
||||
|
||||
if ($error) {
|
||||
$message = optional_param('error_description', '', PARAM_RAW);
|
||||
if ($message) {
|
||||
$SESSION->loginerrormsg = $message;
|
||||
redirect(new moodle_url(get_login_url()));
|
||||
} else {
|
||||
$SESSION->loginerrormsg = $error;
|
||||
redirect(new moodle_url(get_login_url()));
|
||||
$message = optional_param('error_description', null, PARAM_RAW);
|
||||
|
||||
// Errors can occur for authenticated users, such as when a user denies authorization for some internal service call.
|
||||
// In such cases, propagate the error to the component redirect URI.
|
||||
if (isloggedin()) {
|
||||
if (isset($params['sesskey']) && confirm_sesskey($params['sesskey'])) {
|
||||
$redirecturl->param('error', $error);
|
||||
if ($message) {
|
||||
$redirecturl->param('error_description', $message);
|
||||
}
|
||||
redirect($redirecturl);
|
||||
}
|
||||
}
|
||||
|
||||
// Not logged in or the sesskey verification failed, redirect to login + show errors.
|
||||
$SESSION->loginerrormsg = $message ?? $error;
|
||||
redirect(new moodle_url(get_login_url()));
|
||||
}
|
||||
|
||||
// The authorization code generated by the authorization server.
|
||||
$code = required_param('code', PARAM_RAW);
|
||||
// The state parameter we've given (used in moodle as a redirect url).
|
||||
$state = required_param('state', PARAM_LOCALURL);
|
||||
|
||||
$redirecturl = new moodle_url($state);
|
||||
$params = $redirecturl->params();
|
||||
|
||||
if (isset($params['sesskey']) and confirm_sesskey($params['sesskey'])) {
|
||||
$redirecturl->param('oauth2code', $code);
|
||||
|
||||
@@ -90,6 +90,7 @@ $title = get_string('overridepermissionsforrole', 'core_role', $a);
|
||||
$currenttab = 'permissions';
|
||||
|
||||
$PAGE->set_title($title);
|
||||
$PAGE->activityheader->disable();
|
||||
$PAGE->navbar->add($straction);
|
||||
switch ($context->contextlevel) {
|
||||
case CONTEXT_SYSTEM:
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<span data-passwordunmask="wrapper" data-passwordunmaskid="{{ id }}">
|
||||
<span data-passwordunmask="editor">
|
||||
<input type="password"
|
||||
autocomplete="new-password"
|
||||
name="{{ name }}"
|
||||
id="{{ id }}"
|
||||
value="{{ value }}"
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
@core @core_admin @core_course @javascript
|
||||
Feature: Reset course indentation
|
||||
In order to reset indentation in course modules
|
||||
As a admin
|
||||
I want change indent value for all the modules of a course format courses in one go
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Topics Course 1 | T1 | topics |
|
||||
| Topics Course 2 | T2 | topics |
|
||||
| Weekly Course 1 | W1 | weeks |
|
||||
| Weekly Course 2 | W2 | weeks |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber |
|
||||
| forum | Topics forum name | Topics forum description | T1 | forum1 |
|
||||
| data | Topics database name | Topics database description | T1 | data1 |
|
||||
| wiki | Topics wiki name | Topics wiki description | T2 | wiki1 |
|
||||
| forum | Weekly forum name | Weekly forum description | W1 | forum2 |
|
||||
| data | Weekly database name | Weekly database description | W1 | data2 |
|
||||
| wiki | Weekly wiki name | Weekly wiki description | W2 | wiki2 |
|
||||
And I log in as "admin"
|
||||
And I am on "Topics Course 1" course homepage with editing mode on
|
||||
And I open "Topics forum name" actions menu
|
||||
And I click on "Move right" "link" in the "Topics forum name" activity
|
||||
And I open "Topics forum name" actions menu
|
||||
And "Move right" "link" in the "Topics forum name" "activity" should not be visible
|
||||
And "Move left" "link" in the "Topics forum name" "activity" should be visible
|
||||
And I press the escape key
|
||||
And I open "Topics database name" actions menu
|
||||
And "Move right" "link" in the "Topics database name" "activity" should be visible
|
||||
And "Move left" "link" in the "Topics database name" "activity" should not be visible
|
||||
And I am on "Topics Course 2" course homepage with editing mode on
|
||||
And I open "Topics wiki name" actions menu
|
||||
And I click on "Move right" "link" in the "Topics wiki name" activity
|
||||
And I open "Topics wiki name" actions menu
|
||||
And "Move right" "link" in the "Topics wiki name" "activity" should not be visible
|
||||
And "Move left" "link" in the "Topics wiki name" "activity" should be visible
|
||||
And I am on "Weekly Course 1" course homepage with editing mode on
|
||||
And I open "Weekly forum name" actions menu
|
||||
And I click on "Move right" "link" in the "Weekly forum name" activity
|
||||
And I open "Weekly forum name" actions menu
|
||||
And "Move right" "link" in the "Weekly forum name" "activity" should not be visible
|
||||
And "Move left" "link" in the "Weekly forum name" "activity" should be visible
|
||||
And I press the escape key
|
||||
And I open "Weekly database name" actions menu
|
||||
And "Move right" "link" in the "Weekly database name" "activity" should be visible
|
||||
And "Move left" "link" in the "Weekly database name" "activity" should not be visible
|
||||
And I am on "Weekly Course 2" course homepage with editing mode on
|
||||
And I open "Weekly wiki name" actions menu
|
||||
And I click on "Move right" "link" in the "Weekly wiki name" activity
|
||||
And I open "Weekly wiki name" actions menu
|
||||
And "Move right" "link" in the "Weekly wiki name" "activity" should not be visible
|
||||
And "Move left" "link" in the "Weekly wiki name" "activity" should be visible
|
||||
|
||||
Scenario Outline: Apply course indentation reset
|
||||
Given I navigate to "Plugins > Course formats > <format> format" in site administration
|
||||
And I wait "5" seconds
|
||||
And "Reset indentation" "link" should exist
|
||||
When I click on "Reset indentation" "link"
|
||||
And I should see "Reset indentation on course page"
|
||||
And "Reset indentation" "button" should exist
|
||||
And I click on "Reset indentation" "button"
|
||||
Then I should see "Indentation reset"
|
||||
And I am on "<format> Course 1" course homepage with editing mode on
|
||||
And I open "<format> forum name" actions menu
|
||||
And "Move right" "link" in the "<format> forum name" "activity" should be visible
|
||||
And "Move left" "link" in the "<format> forum name" "activity" should not be visible
|
||||
And I press the escape key
|
||||
And I open "<format> database name" actions menu
|
||||
And "Move right" "link" in the "<format> database name" "activity" should be visible
|
||||
And "Move left" "link" in the "<format> database name" "activity" should not be visible
|
||||
And I am on "<format> Course 2" course homepage with editing mode on
|
||||
And I open "<format> wiki name" actions menu
|
||||
And "Move right" "link" in the "<format> wiki name" "activity" should be visible
|
||||
And "Move left" "link" in the "<format> wiki name" "activity" should not be visible
|
||||
# Check other course formats had not been reset
|
||||
And I am on "<other> Course 1" course homepage with editing mode on
|
||||
And I open "<other> forum name" actions menu
|
||||
And "Move right" "link" in the "<other> forum name" "activity" should not be visible
|
||||
And "Move left" "link" in the "<other> forum name" "activity" should be visible
|
||||
|
||||
Examples:
|
||||
| format | other |
|
||||
| Topics | Weekly |
|
||||
| Weekly | Topics |
|
||||
|
||||
Scenario Outline: Cancel course indentation reset
|
||||
Given I navigate to "Plugins > Course formats > <format> format" in site administration
|
||||
And "Reset indentation" "link" should exist
|
||||
When I click on "Reset indentation" "link"
|
||||
And I should see "Reset indentation on course page"
|
||||
And "Reset indentation" "button" should exist
|
||||
And "Cancel" "button" should exist
|
||||
And I click on "Cancel" "button"
|
||||
Then I should not see "Indentation reset"
|
||||
And I am on "<format> Course 1" course homepage with editing mode on
|
||||
And I open "<format> forum name" actions menu
|
||||
And "Move right" "link" in the "<format> forum name" "activity" should not be visible
|
||||
And "Move left" "link" in the "<format> forum name" "activity" should be visible
|
||||
And I press the escape key
|
||||
And I open "<format> database name" actions menu
|
||||
And "Move right" "link" in the "<format> database name" "activity" should be visible
|
||||
And "Move left" "link" in the "<format> database name" "activity" should not be visible
|
||||
And I am on "<format> Course 2" course homepage with editing mode on
|
||||
And I open "<format> wiki name" actions menu
|
||||
And "Move right" "link" in the "<format> wiki name" "activity" should not be visible
|
||||
And "Move left" "link" in the "<format> wiki name" "activity" should be visible
|
||||
|
||||
Examples:
|
||||
| format |
|
||||
| Topics |
|
||||
| Weekly |
|
||||
@@ -254,19 +254,16 @@ class renderer extends \tool_brickfield\output\renderer {
|
||||
<tr>
|
||||
<td>
|
||||
<img src="' . $this->image_path() . 'tachometer-alt-solid.svg" width="15" height="15">' .
|
||||
' <td style="line-height: 10px;"> ' .
|
||||
get_string('totalactivitiescount', manager::PLUGINNAME, $data->combodata['total']) .
|
||||
'</td></td>
|
||||
'</td>
|
||||
<td>
|
||||
<img src="' . $this->image_path() . 'check-square-regular.svg" width="15" height="15">' .
|
||||
' <td style="line-height: 10px;"> ' .
|
||||
get_string('passedcount', manager::PLUGINNAME, $data->combodata['passed']) .
|
||||
'</td></td>
|
||||
'</td>
|
||||
<td>
|
||||
<img src="' . $this->image_path() . 'times-circle-regular.svg" width="15" height="15">' .
|
||||
' <td style="line-height: 10px;"> ' .
|
||||
get_string('failedcount', manager::PLUGINNAME, $data->combodata['failed']) .
|
||||
'</td></td>
|
||||
'</td>
|
||||
</tr>
|
||||
</table>';
|
||||
|
||||
@@ -396,8 +393,8 @@ class renderer extends \tool_brickfield\output\renderer {
|
||||
foreach ($data->groupdata as $key => $group) {
|
||||
$checkgroup = area_base::checkgroup_name($key);
|
||||
$icon = $icons[$key];
|
||||
$tabledata[] = ['<img src="'.$icon.'" width="15" height="15">' . ' ' .' <td style="line-height: 10px;"> '.
|
||||
get_string('checktype:' . $checkgroup, manager::PLUGINNAME).'</td>', $group->errorinstances];
|
||||
$tabledata[] = ['<img src="'.$icon.'" width="15" height="15">' . ' ' .
|
||||
get_string('checktype:' . $checkgroup, manager::PLUGINNAME), $group->errorinstances];
|
||||
}
|
||||
|
||||
return $this->render_table(
|
||||
|
||||
@@ -62,8 +62,8 @@ staticDir: "./admin/tool/componentlibrary/content/static"
|
||||
params:
|
||||
moodleroot: "MOODLEROOT"
|
||||
|
||||
current_version: "4.6.0"
|
||||
current_ruby_version: "4.6.0"
|
||||
current_version: "4.6.2"
|
||||
current_ruby_version: "4.6.2"
|
||||
docs_version: "4.6"
|
||||
boostrap_docs: "https://getbootstrap.com/docs/4.6"
|
||||
repo: "https://github.com/twbs/bootstrap"
|
||||
|
||||
@@ -54,4 +54,4 @@ $breadcrumb-divider: none;
|
||||
|
||||
Since breadcrumbs provide a navigation, it's a good idea to add a meaningful label such as `aria-label="breadcrumb"` to describe the type of navigation provided in the `<nav>` element, as well as applying an `aria-current="page"` to the last item of the set to indicate that it represents the current page.
|
||||
|
||||
For more information, see the [WAI-ARIA Authoring Practices for the breadcrumb pattern](https://www.w3.org/TR/wai-aria-practices/#breadcrumb).
|
||||
For more information, see the [ARIA Authoring Practices Guide breadcrumb pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/).
|
||||
|
||||
@@ -123,10 +123,10 @@ Place a `.btn-group` within another `.btn-group` when you want dropdown menus mi
|
||||
<button type="button" class="btn btn-secondary">2</button>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupDrop1" type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupDrop1">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
</div>
|
||||
@@ -155,10 +155,10 @@ Make a set of buttons appear vertically stacked rather than horizontally. **Spli
|
||||
<button type="button" class="btn btn-secondary">Button</button>
|
||||
<button type="button" class="btn btn-secondary">Button</button>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupVerticalDrop1" type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupVerticalDrop1">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
</div>
|
||||
@@ -166,28 +166,28 @@ Make a set of buttons appear vertically stacked rather than horizontally. **Spli
|
||||
<button type="button" class="btn btn-secondary">Button</button>
|
||||
<button type="button" class="btn btn-secondary">Button</button>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupVerticalDrop2" type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupVerticalDrop2">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupVerticalDrop3" type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupVerticalDrop3">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button id="btnGroupVerticalDrop4" type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="btnGroupVerticalDrop4">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
<a class="dropdown-item" href="#">Dropdown link</a>
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,7 @@ Create block level buttons—those that span the full width of a parent—by add
|
||||
|
||||
## Active state
|
||||
|
||||
Buttons will appear pressed (with a darker background, darker border, and inset shadow) when active. **There's no need to add a class to `<button>`s as they use a pseudo-class**. However, you can still force the same active appearance with `.active` (and include the <code>aria-pressed="true"</code> attribute) should you need to replicate the state programmatically.
|
||||
Buttons will appear pressed when active with a darker background, darker border, and, when shadows are enabled, an inset shadow. **There's no need to add a class to `<button>`s as they use a pseudo-class**. However, you can still force the same active appearance with `.active` (and include the <code>aria-pressed="true"</code> attribute) should you need to replicate the state programmatically.
|
||||
|
||||
{{< example >}}
|
||||
<a href="#" class="btn btn-primary btn-lg active" role="button" aria-pressed="true">Primary link</a>
|
||||
@@ -101,19 +101,23 @@ Disabled buttons using the `<a>` element behave a bit different:
|
||||
|
||||
- `<a>`s don't support the `disabled` attribute, so you must add the `.disabled` class to make it visually appear disabled.
|
||||
- Some future-friendly styles are included to disable all `pointer-events` on anchor buttons. In browsers which support that property, you won't see the disabled cursor at all.
|
||||
- Disabled buttons should include the `aria-disabled="true"` attribute to indicate the state of the element to assistive technologies.
|
||||
- Disabled buttons using `<a>` should include the `aria-disabled="true"` attribute to indicate the state of the element to assistive technologies.
|
||||
- Disabled buttons using `<a>` *should not* include the `href` attribute.
|
||||
|
||||
{{< example >}}
|
||||
<a class="btn btn-primary btn-lg disabled" role="button" aria-disabled="true">Primary link</a>
|
||||
<a class="btn btn-secondary btn-lg disabled" role="button" aria-disabled="true">Link</a>
|
||||
{{< /example >}}
|
||||
|
||||
### Link functionality caveat
|
||||
|
||||
To cover cases where you have to keep the `href` attribute on a disabled link, the `.disabled` class uses `pointer-events: none` to try to disable the link functionality of `<a>`s. Note that this CSS property is not yet standardized for HTML, but all modern browsers support it. In addition, even in browsers that do support `pointer-events: none`, keyboard navigation remains unaffected, meaning that sighted keyboard users and users of assistive technologies will still be able to activate these links. So to be safe, in addition to `aria-disabled="true"`, also include a `tabindex="-1"` attribute on these links to prevent them from receiving keyboard focus, and use custom JavaScript to disable their functionality altogether.
|
||||
|
||||
{{< example >}}
|
||||
<a href="#" class="btn btn-primary btn-lg disabled" tabindex="-1" role="button" aria-disabled="true">Primary link</a>
|
||||
<a href="#" class="btn btn-secondary btn-lg disabled" tabindex="-1" role="button" aria-disabled="true">Link</a>
|
||||
{{< /example >}}
|
||||
|
||||
{{< callout warning >}}
|
||||
##### Link functionality caveat
|
||||
|
||||
The `.disabled` class uses `pointer-events: none` to try to disable the link functionality of `<a>`s, but that CSS property is not yet standardized. In addition, even in browsers that do support `pointer-events: none`, keyboard navigation remains unaffected, meaning that sighted keyboard users and users of assistive technologies will still be able to activate these links. So to be safe, add a `tabindex="-1"` attribute on these links (to prevent them from receiving keyboard focus) and use custom JavaScript to disable their functionality.
|
||||
{{< /callout >}}
|
||||
|
||||
## Button plugin
|
||||
|
||||
Do more with buttons. Control button states or create groups of buttons for more components like toolbars.
|
||||
|
||||
@@ -309,7 +309,7 @@ Add some navigation to a card's header (or block) with Bootstrap's [nav componen
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -332,7 +332,7 @@ Add some navigation to a card's header (or block) with Bootstrap's [nav componen
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ Lastly, if you're building our JavaScript from source, it [requires `util.js`]({
|
||||
|
||||
Carousels don't automatically normalize slide dimensions. As such, you may need to use additional utilities or custom styles to appropriately size content. While carousels support previous/next controls and indicators, they're not explicitly required. Add and customize as you see fit.
|
||||
|
||||
**The `.active` class needs to be added to one of the slides** otherwise the carousel will not be visible. Also be sure to set a unique id on the `.carousel` for optional controls, especially if you're using multiple carousels on a single page. Control and indicator elements must have a `data-target` attribute (or `href` for links) that matches the id of the `.carousel` element.
|
||||
**The `.active` class needs to be added to one of the slides** otherwise the carousel will not be visible. Also be sure to set a unique `id` on the `.carousel` for optional controls, especially if you're using multiple carousels on a single page. Control and indicator elements must have a `data-target` attribute (or `href` for links) that matches the `id` of the `.carousel` element.
|
||||
|
||||
### Slides only
|
||||
|
||||
@@ -48,7 +48,7 @@ Here's a carousel with slides only. Note the presence of the `.d-block` and `.w-
|
||||
|
||||
### With controls
|
||||
|
||||
Adding in the previous and next controls:
|
||||
Adding in the previous and next controls. We recommend using `<button>` elements, but you can also use `<a>` elements with `role="button"`.
|
||||
|
||||
{{< example >}}
|
||||
<div id="carouselExampleControls" class="carousel slide" data-ride="carousel">
|
||||
@@ -63,14 +63,14 @@ Adding in the previous and next controls:
|
||||
{{< placeholder width="800" height="400" class="bd-placeholder-img-lg d-block w-100" color="#333" background="#555" text="Third slide" >}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carouselExampleControls" role="button" data-slide="prev">
|
||||
<button class="carousel-control-prev" type="button" data-target="#carouselExampleControls" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carouselExampleControls" role="button" data-slide="next">
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-target="#carouselExampleControls" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -96,14 +96,14 @@ You can also add the indicators to the carousel, alongside the controls, too.
|
||||
{{< placeholder width="800" height="400" class="bd-placeholder-img-lg d-block w-100" color="#333" background="#555" text="Third slide" >}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
|
||||
<button class="carousel-control-prev" type="button" data-target="#carouselExampleIndicators" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-target="#carouselExampleIndicators" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -141,20 +141,20 @@ Add captions to your slides easily with the `.carousel-caption` element within a
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carouselExampleCaptions" role="button" data-slide="prev">
|
||||
<button class="carousel-control-prev" type="button" data-target="#carouselExampleCaptions" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carouselExampleCaptions" role="button" data-slide="next">
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-target="#carouselExampleCaptions" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
### Crossfade
|
||||
|
||||
Add `.carousel-fade` to your carousel to animate slides with a fade transition instead of a slide.
|
||||
Add `.carousel-fade` to your carousel to animate slides with a fade transition instead of a slide. Depending on your carousel content (e.g., text only slides), you may want to add `.bg-body` or some custom CSS to the `.carousel-item`s for proper crossfading.
|
||||
|
||||
{{< example >}}
|
||||
<div id="carouselExampleFade" class="carousel slide carousel-fade" data-ride="carousel">
|
||||
@@ -169,14 +169,14 @@ Add `.carousel-fade` to your carousel to animate slides with a fade transition i
|
||||
{{< placeholder width="800" height="400" class="bd-placeholder-img-lg d-block w-100" color="#333" background="#555" text="Third slide" >}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carouselExampleFade" role="button" data-slide="prev">
|
||||
<button class="carousel-control-prev" type="button" data-target="#carouselExampleFade" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carouselExampleFade" role="button" data-slide="next">
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-target="#carouselExampleFade" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -197,14 +197,14 @@ Add `data-interval=""` to a `.carousel-item` to change the amount of time to del
|
||||
{{< placeholder width="800" height="400" class="bd-placeholder-img-lg d-block w-100" color="#333" background="#555" text="Third slide" >}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carouselExampleInterval" role="button" data-slide="prev">
|
||||
<button class="carousel-control-prev" type="button" data-target="#carouselExampleInterval" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carouselExampleInterval" role="button" data-slide="next">
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-target="#carouselExampleInterval" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -225,14 +225,14 @@ Carousels support swiping left/right on touchscreen devices to move between slid
|
||||
{{< placeholder width="800" height="400" class="bd-placeholder-img-lg d-block w-100" color="#333" background="#555" text="Third slide" >}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="carousel-control-prev" href="#carouselExampleControlsNoTouching" role="button" data-slide="prev">
|
||||
<button class="carousel-control-prev" type="button" data-target="#carouselExampleControlsNoTouching" data-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
<a class="carousel-control-next" href="#carouselExampleControlsNoTouching" role="button" data-slide="next">
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-target="#carouselExampleControlsNoTouching" data-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="sr-only">Next</span>
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
|
||||
@@ -40,6 +40,29 @@ Generally, we recommend using a button with the `data-target` attribute. While n
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
## Horizontal
|
||||
|
||||
The collapse plugin also supports horizontal collapsing. Add the `.width` modifier class to transition the `width` instead of `height` and set a `width` on the immediate child element. Feel free to write your own custom Sass, use inline styles, or use our [width utilities]({{< docsref "/utilities/sizing" >}}).
|
||||
|
||||
{{< callout info >}}
|
||||
Please note that while the example below has a `min-height` set to avoid excessive repaints in our docs, this is not explicitly required. **Only the `width` on the child element is required.**
|
||||
{{< /callout >}}
|
||||
|
||||
{{< example >}}
|
||||
<p>
|
||||
<button class="btn btn-primary" type="button" data-toggle="collapse" data-target="#collapseWidthExample" aria-expanded="false" aria-controls="collapseWidthExample">
|
||||
Toggle width collapse
|
||||
</button>
|
||||
</p>
|
||||
<div style="min-height: 120px;">
|
||||
<div class="collapse width" id="collapseWidthExample">
|
||||
<div class="card card-body" style="width: 320px;">
|
||||
This is some placeholder content for a horizontal collapse. It's hidden by default and shown when triggered.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
## Multiple targets
|
||||
|
||||
A `<button>` or `<a>` can show and hide multiple elements by referencing them with a JQuery selector in its `href` or `data-target` attribute.
|
||||
@@ -127,7 +150,7 @@ Be sure to add `aria-expanded` to the control element. This attribute explicitly
|
||||
|
||||
If your control element is targeting a single collapsible element – i.e. the `data-target` attribute is pointing to an `id` selector – you should add the `aria-controls` attribute to the control element, containing the `id` of the collapsible element. Modern screen readers and similar assistive technologies make use of this attribute to provide users with additional shortcuts to navigate directly to the collapsible element itself.
|
||||
|
||||
Note that Bootstrap's current implementation does not cover the various keyboard interactions described in the [WAI-ARIA Authoring Practices 1.1 accordion pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#accordion) - you will need to include these yourself with custom JavaScript.
|
||||
Note that Bootstrap's current implementation does not cover the various keyboard interactions described in the [ARIA Authoring Practices Guide accordion pattern](https://www.w3.org/WAI/ARIA/apg/patterns/accordion/) - you will need to include these yourself with custom JavaScript.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ If you're building our JavaScript from source, it [requires `util.js`]({{< docsr
|
||||
|
||||
## Accessibility
|
||||
|
||||
The [<abbr title="Web Accessibility Initiative">WAI</abbr> <abbr title="Accessible Rich Internet Applications">ARIA</abbr>](https://www.w3.org/TR/wai-aria/) standard defines an actual [`role="menu"` widget](https://www.w3.org/WAI/PF/aria/roles#menu), but this is specific to application-like menus which trigger actions or functions. <abbr title="Accessible Rich Internet Applications">ARIA</abbr> menus can only contain menu items, checkbox menu items, radio button menu items, radio button groups, and sub-menus.
|
||||
The [<abbr title="Web Accessibility Initiative">WAI</abbr> <abbr title="Accessible Rich Internet Applications">ARIA</abbr>](https://www.w3.org/TR/wai-aria/) standard defines an actual [`role="menu"` widget](https://www.w3.org/TR/wai-aria/#menu), but this is specific to application-like menus which trigger actions or functions. <abbr title="Accessible Rich Internet Applications">ARIA</abbr> menus can only contain menu items, checkbox menu items, radio button menu items, radio button groups, and sub-menus.
|
||||
|
||||
Bootstrap's dropdowns, on the other hand, are designed to be generic and applicable to a variety of situations and markup structures. For instance, it is possible to create dropdowns that contain additional inputs and form controls, such as search fields or login forms. For this reason, Bootstrap does not expect (nor automatically add) any of the `role` and `aria-` attributes required for true <abbr title="Accessible Rich Internet Applications">ARIA</abbr> menus. Authors will have to include these more specific attributes themselves.
|
||||
|
||||
@@ -32,10 +32,10 @@ Any single `.btn` can be turned into a dropdown toggle with some markup changes.
|
||||
|
||||
{{< example >}}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown button
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
@@ -47,11 +47,11 @@ And with `<a>` elements:
|
||||
|
||||
{{< example >}}
|
||||
<div class="dropdown">
|
||||
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="btn btn-secondary dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown link
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
@@ -63,7 +63,7 @@ The best part is you can do this with any button variant, too:
|
||||
|
||||
<div class="bd-example">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Primary</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">Primary</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -73,7 +73,7 @@ The best part is you can do this with any button variant, too:
|
||||
</div>
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Secondary</button>
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">Secondary</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -83,7 +83,7 @@ The best part is you can do this with any button variant, too:
|
||||
</div>
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Success</button>
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-expanded="false">Success</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -93,7 +93,7 @@ The best part is you can do this with any button variant, too:
|
||||
</div>
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Info</button>
|
||||
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-expanded="false">Info</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -103,7 +103,7 @@ The best part is you can do this with any button variant, too:
|
||||
</div>
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-warning dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Warning</button>
|
||||
<button type="button" class="btn btn-warning dropdown-toggle" data-toggle="dropdown" aria-expanded="false">Warning</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -113,7 +113,7 @@ The best part is you can do this with any button variant, too:
|
||||
</div>
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Danger</button>
|
||||
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-expanded="false">Danger</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -127,7 +127,7 @@ The best part is you can do this with any button variant, too:
|
||||
```html
|
||||
<!-- Example single danger button -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Action
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -149,7 +149,7 @@ We use this extra class to reduce the horizontal `padding` on either side of the
|
||||
<div class="bd-example">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary">Primary</button>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -162,7 +162,7 @@ We use this extra class to reduce the horizontal `padding` on either side of the
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary">Secondary</button>
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -175,7 +175,7 @@ We use this extra class to reduce the horizontal `padding` on either side of the
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-success">Success</button>
|
||||
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-success dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -188,7 +188,7 @@ We use this extra class to reduce the horizontal `padding` on either side of the
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-info">Info</button>
|
||||
<button type="button" class="btn btn-info dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-info dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -201,7 +201,7 @@ We use this extra class to reduce the horizontal `padding` on either side of the
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-warning">Warning</button>
|
||||
<button type="button" class="btn btn-warning dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-warning dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -214,7 +214,7 @@ We use this extra class to reduce the horizontal `padding` on either side of the
|
||||
</div><!-- /btn-group -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-danger">Danger</button>
|
||||
<button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -231,7 +231,7 @@ We use this extra class to reduce the horizontal `padding` on either side of the
|
||||
<!-- Example split danger button -->
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-danger">Action</button>
|
||||
<button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-danger dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -250,7 +250,7 @@ Button dropdowns work with buttons of all sizes, including default and split dro
|
||||
|
||||
<div class="bd-example">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary btn-lg dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary btn-lg dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Large button
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -263,7 +263,7 @@ Button dropdowns work with buttons of all sizes, including default and split dro
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-lg btn-secondary">Large split button</button>
|
||||
<button type="button" class="btn btn-lg btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-lg btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -279,7 +279,7 @@ Button dropdowns work with buttons of all sizes, including default and split dro
|
||||
```html
|
||||
<!-- Large button groups (default and split) -->
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary btn-lg dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary btn-lg dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Large button
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -290,7 +290,7 @@ Button dropdowns work with buttons of all sizes, including default and split dro
|
||||
<button class="btn btn-secondary btn-lg" type="button">
|
||||
Large split button
|
||||
</button>
|
||||
<button type="button" class="btn btn-lg btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-lg btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -301,7 +301,7 @@ Button dropdowns work with buttons of all sizes, including default and split dro
|
||||
|
||||
<div class="bd-example">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Small button
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -314,7 +314,7 @@ Button dropdowns work with buttons of all sizes, including default and split dro
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-sm btn-secondary">Small split button</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -330,7 +330,7 @@ Button dropdowns work with buttons of all sizes, including default and split dro
|
||||
```html
|
||||
<!-- Small button groups (default and split) -->
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Small button
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -341,7 +341,7 @@ Button dropdowns work with buttons of all sizes, including default and split dro
|
||||
<button class="btn btn-secondary btn-sm" type="button">
|
||||
Small split button
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -358,7 +358,7 @@ Trigger dropdown menus above elements by adding `.dropup` to the parent element.
|
||||
|
||||
<div class="bd-example">
|
||||
<div class="btn-group dropup">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropup
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -373,7 +373,7 @@ Trigger dropdown menus above elements by adding `.dropup` to the parent element.
|
||||
<button type="button" class="btn btn-secondary">
|
||||
Split dropup
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -389,7 +389,7 @@ Trigger dropdown menus above elements by adding `.dropup` to the parent element.
|
||||
```html
|
||||
<!-- Default dropup button -->
|
||||
<div class="btn-group dropup">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropup
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -402,7 +402,7 @@ Trigger dropdown menus above elements by adding `.dropup` to the parent element.
|
||||
<button type="button" class="btn btn-secondary">
|
||||
Split dropup
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -417,7 +417,7 @@ Trigger dropdown menus at the right of the elements by adding `.dropright` to th
|
||||
|
||||
<div class="bd-example">
|
||||
<div class="btn-group dropright">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropright
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -432,7 +432,7 @@ Trigger dropdown menus at the right of the elements by adding `.dropright` to th
|
||||
<button type="button" class="btn btn-secondary">
|
||||
Split dropright
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropright</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -448,7 +448,7 @@ Trigger dropdown menus at the right of the elements by adding `.dropright` to th
|
||||
```html
|
||||
<!-- Default dropright button -->
|
||||
<div class="btn-group dropright">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropright
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -461,7 +461,7 @@ Trigger dropdown menus at the right of the elements by adding `.dropright` to th
|
||||
<button type="button" class="btn btn-secondary">
|
||||
Split dropright
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropright</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -476,7 +476,7 @@ Trigger dropdown menus at the left of the elements by adding `.dropleft` to the
|
||||
|
||||
<div class="bd-example">
|
||||
<div class="btn-group dropleft">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropleft
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -488,8 +488,8 @@ Trigger dropdown menus at the left of the elements by adding `.dropleft` to the
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<div class="btn-group dropleft" role="group">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<div class="btn-group dropleft">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropleft</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -509,7 +509,7 @@ Trigger dropdown menus at the left of the elements by adding `.dropleft` to the
|
||||
```html
|
||||
<!-- Default dropleft button -->
|
||||
<div class="btn-group dropleft">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropleft
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -519,8 +519,8 @@ Trigger dropdown menus at the left of the elements by adding `.dropleft` to the
|
||||
|
||||
<!-- Split dropleft button -->
|
||||
<div class="btn-group">
|
||||
<div class="btn-group dropleft" role="group">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<div class="btn-group dropleft">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropleft</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -539,10 +539,10 @@ Historically dropdown menu contents *had* to be links, but that's no longer the
|
||||
|
||||
{{< example >}}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenu2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenu2">
|
||||
<div class="dropdown-menu">
|
||||
<button class="dropdown-item" type="button">Action</button>
|
||||
<button class="dropdown-item" type="button">Another action</button>
|
||||
<button class="dropdown-item" type="button">Something else here</button>
|
||||
@@ -580,7 +580,7 @@ Add `.disabled` to items in the dropdown to **style them as disabled**.
|
||||
{{< example >}}
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Regular link</a>
|
||||
<a class="dropdown-item disabled" href="#" tabindex="-1" aria-disabled="true">Disabled link</a>
|
||||
<a class="dropdown-item disabled">Disabled link</a>
|
||||
<a class="dropdown-item" href="#">Another link</a>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
@@ -595,7 +595,7 @@ By default, a dropdown menu is automatically positioned 100% from the top and al
|
||||
|
||||
{{< example >}}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
Right-aligned menu
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
@@ -614,7 +614,7 @@ To align **right** the dropdown menu with the given breakpoint or larger, add `.
|
||||
|
||||
{{< example >}}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" data-display="static" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" data-display="static" aria-expanded="false">
|
||||
Left-aligned but right aligned when large screen
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-lg-right">
|
||||
@@ -629,7 +629,7 @@ To align **left** the dropdown menu with the given breakpoint or larger, add `.d
|
||||
|
||||
{{< example >}}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" data-display="static" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" data-display="static" aria-expanded="false">
|
||||
Right-aligned but left aligned when large screen
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right dropdown-menu-lg-left">
|
||||
@@ -745,10 +745,10 @@ Use `data-offset` or `data-reference` to change the location of the dropdown.
|
||||
{{< example >}}
|
||||
<div class="d-flex">
|
||||
<div class="dropdown mr-1">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" id="dropdownMenuOffset" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-offset="10,20">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-expanded="false" data-offset="10,20">
|
||||
Offset
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuOffset">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
@@ -756,10 +756,10 @@ Use `data-offset` or `data-reference` to change the location of the dropdown.
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-secondary">Reference</button>
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" id="dropdownMenuReference" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-reference="parent">
|
||||
<button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false" data-reference="parent">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuReference">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
@@ -784,10 +784,10 @@ Add `data-toggle="dropdown"` to a link or button to toggle a dropdown.
|
||||
|
||||
```html
|
||||
<div class="dropdown">
|
||||
<button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown trigger
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="dLabel">
|
||||
<div class="dropdown-menu">
|
||||
...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -192,7 +192,7 @@ Multiple add-ons are supported and can be mixed with checkbox and radio input ve
|
||||
{{< example >}}
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</button>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">Dropdown</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -207,7 +207,7 @@ Multiple add-ons are supported and can be mixed with checkbox and radio input ve
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" aria-label="Text input with dropdown button">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</button>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-expanded="false">Dropdown</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -225,7 +225,7 @@ Multiple add-ons are supported and can be mixed with checkbox and radio input ve
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<button type="button" class="btn btn-outline-secondary">Action</button>
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
@@ -243,7 +243,7 @@ Multiple add-ons are supported and can be mixed with checkbox and radio input ve
|
||||
<input type="text" class="form-control" aria-label="Text input with segmented dropdown button">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary">Action</button>
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
@@ -62,7 +62,7 @@ Be sure to **not use the standard `.btn` classes here**.
|
||||
<a href="#" class="list-group-item list-group-item-action">A second link item</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">A third link item</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">A fourth link item</a>
|
||||
<a href="#" class="list-group-item list-group-item-action disabled" tabindex="-1" aria-disabled="true">A disabled link item</a>
|
||||
<a class="list-group-item list-group-item-action disabled">A disabled link item</a>
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
layout: docs
|
||||
title: Navbar
|
||||
description: Documentation and examples for Bootstrap's powerful, responsive navigation header, the navbar. Includes support for branding, navigation, and more, including support for our collapse plugin.
|
||||
description: Documentation and examples for Bootstrap's powerful, responsive navigation header, the navbar. Includes support for branding, navigation, collapse plugin, and more.
|
||||
group: components
|
||||
toc: true
|
||||
---
|
||||
@@ -50,10 +50,10 @@ Here's an example of all the sub-components included in a responsive light-theme
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
@@ -61,7 +61,7 @@ Here's an example of all the sub-components included in a responsive light-theme
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0">
|
||||
@@ -135,7 +135,7 @@ Active states—with `.active`—to indicate the current page can be applied dir
|
||||
<a class="nav-link" href="#">Pricing</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -155,7 +155,7 @@ And because we use classes for our navs, you can avoid the list-based approach e
|
||||
<a class="nav-link active" href="#">Home <span class="sr-only">(current)</span></a>
|
||||
<a class="nav-link" href="#">Features</a>
|
||||
<a class="nav-link" href="#">Pricing</a>
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -181,10 +181,10 @@ You can also use dropdowns in your navbar. Dropdown menus require a wrapping ele
|
||||
<a class="nav-link" href="#">Pricing</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Dropdown link
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
@@ -461,10 +461,10 @@ Here's an example navbar using `.navbar-nav-scroll` with `style="max-height: 100
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarScrollingDropdown" role="button" data-toggle="dropdown" aria-expanded="false">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-expanded="false">
|
||||
Link
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="navbarScrollingDropdown">
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
@@ -472,7 +472,7 @@ Here's an example navbar using `.navbar-nav-scroll` with `style="max-height: 100
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Link</a>
|
||||
<a class="nav-link disabled">Link</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="d-flex">
|
||||
@@ -510,7 +510,7 @@ With no `.navbar-brand` shown at the smallest breakpoint:
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0">
|
||||
@@ -539,7 +539,7 @@ With a brand name shown on the left and toggler on the right:
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0">
|
||||
@@ -568,7 +568,7 @@ With a toggler on the left and brand name on the right:
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0">
|
||||
|
||||
@@ -28,7 +28,7 @@ The base `.nav` component does not include any `.active` state. The following ex
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -40,7 +40,7 @@ Classes are used throughout, so your markup can be super flexible. Use `<ul>`s l
|
||||
<a class="nav-link active" href="#">Active</a>
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</nav>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -66,7 +66,7 @@ Centered with `.justify-content-center`:
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -85,7 +85,7 @@ Right-aligned with `.justify-content-end`:
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -106,7 +106,7 @@ Stack your navigation by changing the flex item direction with the `.flex-column
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -118,7 +118,7 @@ As always, vertical navigation is possible without `<ul>`s, too.
|
||||
<a class="nav-link active" href="#">Active</a>
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</nav>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -138,7 +138,7 @@ Takes the basic nav from above and adds the `.nav-tabs` class to generate a tabb
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -159,7 +159,7 @@ Take that same HTML, but use `.nav-pills` instead:
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -180,7 +180,7 @@ Force your `.nav`'s contents to extend the full available width one of two modif
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -192,7 +192,7 @@ When using a `<nav>`-based navigation, you can safely omit `.nav-item` as only `
|
||||
<a class="nav-link active" href="#">Active</a>
|
||||
<a class="nav-link" href="#">Much longer nav link</a>
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</nav>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -210,7 +210,7 @@ For equal-width elements, use `.nav-justified`. All horizontal space will be occ
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -222,7 +222,7 @@ Similar to the `.nav-fill` example using a `<nav>`-based navigation.
|
||||
<a class="nav-link active" href="#">Active</a>
|
||||
<a class="nav-link" href="#">Much longer nav link</a>
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</nav>
|
||||
|
||||
{{< /example >}}
|
||||
@@ -235,7 +235,7 @@ If you need responsive nav variations, consider using a series of [flexbox utili
|
||||
<a class="flex-sm-fill text-sm-center nav-link active" href="#">Active</a>
|
||||
<a class="flex-sm-fill text-sm-center nav-link" href="#">Longer nav link</a>
|
||||
<a class="flex-sm-fill text-sm-center nav-link" href="#">Link</a>
|
||||
<a class="flex-sm-fill text-sm-center nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="flex-sm-fill text-sm-center nav-link disabled">Disabled</a>
|
||||
</nav>
|
||||
{{< /example >}}
|
||||
|
||||
@@ -243,7 +243,7 @@ If you need responsive nav variations, consider using a series of [flexbox utili
|
||||
|
||||
If you're using navs to provide a navigation bar, be sure to add a `role="navigation"` to the most logical parent container of the `<ul>`, or wrap a `<nav>` element around the whole navigation. Do not add the role to the `<ul>` itself, as this would prevent it from being announced as an actual list by assistive technologies.
|
||||
|
||||
Note that navigation bars, even if visually styled as tabs with the `.nav-tabs` class, should **not** be given `role="tablist"`, `role="tab"` or `role="tabpanel"` attributes. These are only appropriate for dynamic tabbed interfaces, as described in the [<abbr title="Web Accessibility Initiative">WAI</abbr> <abbr title="Accessible Rich Internet Applications">ARIA</abbr> Authoring Practices](https://www.w3.org/TR/wai-aria-practices/#tabpanel). See [JavaScript behavior](#javascript-behavior) for dynamic tabbed interfaces in this section for an example.
|
||||
Note that navigation bars, even if visually styled as tabs with the `.nav-tabs` class, should **not** be given `role="tablist"`, `role="tab"` or `role="tabpanel"` attributes. These are only appropriate for dynamic tabbed interfaces, as described in the [ARIA Authoring Practices Guide tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/). See [JavaScript behavior](#javascript-behavior) for dynamic tabbed interfaces in this section for an example.
|
||||
|
||||
## Using dropdowns
|
||||
|
||||
@@ -257,7 +257,7 @@ Add dropdown menus with a little extra HTML and the [dropdowns JavaScript plugin
|
||||
<a class="nav-link active" href="#">Active</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Dropdown</a>
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-expanded="false">Dropdown</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -270,7 +270,7 @@ Add dropdown menus with a little extra HTML and the [dropdowns JavaScript plugin
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -283,7 +283,7 @@ Add dropdown menus with a little extra HTML and the [dropdowns JavaScript plugin
|
||||
<a class="nav-link active" href="#">Active</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Dropdown</a>
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-expanded="false">Dropdown</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
@@ -296,7 +296,7 @@ Add dropdown menus with a little extra HTML and the [dropdowns JavaScript plugin
|
||||
<a class="nav-link" href="#">Link</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
|
||||
<a class="nav-link disabled">Disabled</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{< /example >}}
|
||||
@@ -307,20 +307,22 @@ Use the tab JavaScript plugin—include it individually or through the compiled
|
||||
|
||||
If you're building our JavaScript from source, it [requires `util.js`]({{< docsref "/getting-started/javascript#util" >}}).
|
||||
|
||||
Dynamic tabbed interfaces, as described in the [<abbr title="Web Accessibility Initiative">WAI</abbr> <abbr title="Accessible Rich Internet Applications">ARIA</abbr> Authoring Practices](https://www.w3.org/TR/wai-aria-practices/#tabpanel), require `role="tablist"`, `role="tab"`, `role="tabpanel"`, and additional `aria-` attributes in order to convey their structure, functionality and current state to users of assistive technologies (such as screen readers).
|
||||
Dynamic tabbed interfaces, as described in the [ARIA Authoring Practices Guide tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/), require `role="tablist"`, `role="tab"`, `role="tabpanel"`, and additional `aria-` attributes in order to convey their structure, functionality and current state to users of assistive technologies (such as screen readers). As a best practice, we recommend using `<button>` elements for the tabs, as these are controls that trigger a dynamic change, rather than links that navigate to a new page or location.
|
||||
|
||||
Note that dynamic tabbed interfaces should <em>not</em> contain dropdown menus, as this causes both usability and accessibility issues. From a usability perspective, the fact that the currently displayed tab's trigger element is not immediately visible (as it's inside the closed dropdown menu) can cause confusion. From an accessibility point of view, there is currently no sensible way to map this sort of construct to a standard WAI ARIA pattern, meaning that it cannot be easily made understandable to users of assistive technologies.
|
||||
{{< callout danger >}}
|
||||
Note that the tab JavaScript plugin **does not** support tabbed interfaces that contain dropdown menus, as these cause both usability and accessibility issues. From a usability perspective, the fact that the currently displayed tab's trigger element is not immediately visible (as it's inside the closed dropdown menu) can cause confusion. From an accessibility point of view, there is currently no sensible way to map this sort of construct to a standard WAI ARIA pattern, meaning that it cannot be easily made understandable to users of assistive technologies.
|
||||
{{< /callout >}}
|
||||
|
||||
<div class="bd-example bd-example-tabs">
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
|
||||
<button class="nav-link active" id="home-tab" data-toggle="tab" data-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true">Home</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
|
||||
<button class="nav-link" id="profile-tab" data-toggle="tab" data-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#contact" role="tab" aria-controls="contact" aria-selected="false">Contact</a>
|
||||
<button class="nav-link" id="contact-tab" data-toggle="tab" data-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false">Contact</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
@@ -339,13 +341,13 @@ Note that dynamic tabbed interfaces should <em>not</em> contain dropdown menus,
|
||||
```html
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
|
||||
<button class="nav-link active" id="home-tab" data-toggle="tab" data-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true">Home</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
|
||||
<button class="nav-link" id="profile-tab" data-toggle="tab" data-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#contact" role="tab" aria-controls="contact" aria-selected="false">Contact</a>
|
||||
<button class="nav-link" id="contact-tab" data-toggle="tab" data-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false">Contact</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
@@ -360,9 +362,9 @@ To help fit your needs, this works with `<ul>`-based markup, as shown above, or
|
||||
<div class="bd-example bd-example-tabs">
|
||||
<nav>
|
||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||
<a class="nav-link active" id="nav-home-tab" data-toggle="tab" href="#nav-home" role="tab" aria-controls="nav-home" aria-selected="true">Home</a>
|
||||
<a class="nav-link" id="nav-profile-tab" data-toggle="tab" href="#nav-profile" role="tab" aria-controls="nav-profile" aria-selected="false">Profile</a>
|
||||
<a class="nav-link" id="nav-contact-tab" data-toggle="tab" href="#nav-contact" role="tab" aria-controls="nav-contact" aria-selected="false">Contact</a>
|
||||
<button class="nav-link active" id="nav-home-tab" data-toggle="tab" data-target="#nav-home" type="button" role="tab" aria-controls="nav-home" aria-selected="true">Home</button>
|
||||
<button class="nav-link" id="nav-profile-tab" data-toggle="tab" data-target="#nav-profile" type="button" role="tab" aria-controls="nav-profile" aria-selected="false">Profile</button>
|
||||
<button class="nav-link" id="nav-contact-tab" data-toggle="tab" data-target="#nav-contact" type="button" role="tab" aria-controls="nav-contact" aria-selected="false">Contact</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
@@ -381,9 +383,9 @@ To help fit your needs, this works with `<ul>`-based markup, as shown above, or
|
||||
```html
|
||||
<nav>
|
||||
<div class="nav nav-tabs" id="nav-tab" role="tablist">
|
||||
<a class="nav-link active" id="nav-home-tab" data-toggle="tab" href="#nav-home" role="tab" aria-controls="nav-home" aria-selected="true">Home</a>
|
||||
<a class="nav-link" id="nav-profile-tab" data-toggle="tab" href="#nav-profile" role="tab" aria-controls="nav-profile" aria-selected="false">Profile</a>
|
||||
<a class="nav-link" id="nav-contact-tab" data-toggle="tab" href="#nav-contact" role="tab" aria-controls="nav-contact" aria-selected="false">Contact</a>
|
||||
<button class="nav-link active" id="nav-home-tab" data-toggle="tab" data-target="#nav-home" type="button" role="tab" aria-controls="nav-home" aria-selected="true">Home</button>
|
||||
<button class="nav-link" id="nav-profile-tab" data-toggle="tab" data-target="#nav-profile" type="button" role="tab" aria-controls="nav-profile" aria-selected="false">Profile</button>
|
||||
<button class="nav-link" id="nav-contact-tab" data-toggle="tab" data-target="#nav-contact" type="button" role="tab" aria-controls="nav-contact" aria-selected="false">Contact</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
@@ -398,13 +400,13 @@ The tabs plugin also works with pills.
|
||||
<div class="bd-example bd-example-tabs">
|
||||
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="pills-home-tab" data-toggle="pill" href="#pills-home" role="tab" aria-controls="pills-home" aria-selected="true">Home</a>
|
||||
<button class="nav-link active" id="pills-home-tab" data-toggle="pill" data-target="#pills-home" type="button" role="tab" aria-controls="pills-home" aria-selected="true">Home</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="pills-profile-tab" data-toggle="pill" href="#pills-profile" role="tab" aria-controls="pills-profile" aria-selected="false">Profile</a>
|
||||
<button class="nav-link" id="pills-profile-tab" data-toggle="pill" data-target="#pills-profile" type="button" role="tab" aria-controls="pills-profile" aria-selected="false">Profile</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="pills-contact-tab" data-toggle="pill" href="#pills-contact" role="tab" aria-controls="pills-contact" aria-selected="false">Contact</a>
|
||||
<button class="nav-link" id="pills-contact-tab" data-toggle="pill" data-target="#pills-contact" type="button" role="tab" aria-controls="pills-contact" aria-selected="false">Contact</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
@@ -423,13 +425,13 @@ The tabs plugin also works with pills.
|
||||
```html
|
||||
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="pills-home-tab" data-toggle="pill" href="#pills-home" role="tab" aria-controls="pills-home" aria-selected="true">Home</a>
|
||||
<button class="nav-link active" id="pills-home-tab" data-toggle="pill" data-target="#pills-home" type="button" role="tab" aria-controls="pills-home" aria-selected="true">Home</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="pills-profile-tab" data-toggle="pill" href="#pills-profile" role="tab" aria-controls="pills-profile" aria-selected="false">Profile</a>
|
||||
<button class="nav-link" id="pills-profile-tab" data-toggle="pill" data-target="#pills-profile" type="button" role="tab" aria-controls="pills-profile" aria-selected="false">Profile</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="pills-contact-tab" data-toggle="pill" href="#pills-contact" role="tab" aria-controls="pills-contact" aria-selected="false">Contact</a>
|
||||
<button class="nav-link" id="pills-contact-tab" data-toggle="pill" data-target="#pills-contact" type="button" role="tab" aria-controls="pills-contact" aria-selected="false">Contact</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
@@ -445,10 +447,10 @@ And with vertical pills.
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
|
||||
<a class="nav-link active" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true">Home</a>
|
||||
<a class="nav-link" id="v-pills-profile-tab" data-toggle="pill" href="#v-pills-profile" role="tab" aria-controls="v-pills-profile" aria-selected="false">Profile</a>
|
||||
<a class="nav-link" id="v-pills-messages-tab" data-toggle="pill" href="#v-pills-messages" role="tab" aria-controls="v-pills-messages" aria-selected="false">Messages</a>
|
||||
<a class="nav-link" id="v-pills-settings-tab" data-toggle="pill" href="#v-pills-settings" role="tab" aria-controls="v-pills-settings" aria-selected="false">Settings</a>
|
||||
<button class="nav-link active" id="v-pills-home-tab" data-toggle="pill" data-target="#v-pills-home" type="button" role="tab" aria-controls="v-pills-home" aria-selected="true">Home</button>
|
||||
<button class="nav-link" id="v-pills-profile-tab" data-toggle="pill" data-target="#v-pills-profile" type="button" role="tab" aria-controls="v-pills-profile" aria-selected="false">Profile</button>
|
||||
<button class="nav-link" id="v-pills-messages-tab" data-toggle="pill" data-target="#v-pills-messages" type="button" role="tab" aria-controls="v-pills-messages" aria-selected="false">Messages</button>
|
||||
<button class="nav-link" id="v-pills-settings-tab" data-toggle="pill" data-target="#v-pills-settings" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false">Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
@@ -474,10 +476,10 @@ And with vertical pills.
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
|
||||
<a class="nav-link active" id="v-pills-home-tab" data-toggle="pill" href="#v-pills-home" role="tab" aria-controls="v-pills-home" aria-selected="true">Home</a>
|
||||
<a class="nav-link" id="v-pills-profile-tab" data-toggle="pill" href="#v-pills-profile" role="tab" aria-controls="v-pills-profile" aria-selected="false">Profile</a>
|
||||
<a class="nav-link" id="v-pills-messages-tab" data-toggle="pill" href="#v-pills-messages" role="tab" aria-controls="v-pills-messages" aria-selected="false">Messages</a>
|
||||
<a class="nav-link" id="v-pills-settings-tab" data-toggle="pill" href="#v-pills-settings" role="tab" aria-controls="v-pills-settings" aria-selected="false">Settings</a>
|
||||
<button class="nav-link active" id="v-pills-home-tab" data-toggle="pill" data-target="#v-pills-home" type="button" role="tab" aria-controls="v-pills-home" aria-selected="true">Home</button>
|
||||
<button class="nav-link" id="v-pills-profile-tab" data-toggle="pill" data-target="#v-pills-profile" type="button" role="tab" aria-controls="v-pills-profile" aria-selected="false">Profile</button>
|
||||
<button class="nav-link" id="v-pills-messages-tab" data-toggle="pill" data-target="#v-pills-messages" type="button" role="tab" aria-controls="v-pills-messages" aria-selected="false">Messages</button>
|
||||
<button class="nav-link" id="v-pills-settings-tab" data-toggle="pill" data-target="#v-pills-settings" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false">Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
@@ -499,16 +501,16 @@ You can activate a tab or pill navigation without writing any JavaScript by simp
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
|
||||
<button class="nav-link active" id="home-tab" data-toggle="tab" data-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true">Home</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
|
||||
<button class="nav-link" id="profile-tab" data-toggle="tab" data-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="messages-tab" data-toggle="tab" href="#messages" role="tab" aria-controls="messages" aria-selected="false">Messages</a>
|
||||
<button class="nav-link" id="messages-tab" data-toggle="tab" data-target="#messages" type="button" role="tab" aria-controls="messages" aria-selected="false">Messages</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false">Settings</a>
|
||||
<button class="nav-link" id="settings-tab" data-toggle="tab" data-target="#settings" type="button" role="tab" aria-controls="settings" aria-selected="false">Settings</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -526,7 +528,7 @@ You can activate a tab or pill navigation without writing any JavaScript by simp
|
||||
Enable tabbable tabs via JavaScript (each tab needs to be activated individually):
|
||||
|
||||
```js
|
||||
$('#myTab a').on('click', function (event) {
|
||||
$('#myTab button').on('click', function (event) {
|
||||
event.preventDefault()
|
||||
$(this).tab('show')
|
||||
})
|
||||
@@ -535,10 +537,10 @@ $('#myTab a').on('click', function (event) {
|
||||
You can activate individual tabs in several ways:
|
||||
|
||||
```js
|
||||
$('#myTab a[href="#profile"]').tab('show') // Select tab by name
|
||||
$('#myTab li:first-child a').tab('show') // Select first tab
|
||||
$('#myTab li:last-child a').tab('show') // Select last tab
|
||||
$('#myTab li:nth-child(3) a').tab('show') // Select third tab
|
||||
$('#myTab button[data-target="#profile"]').tab('show') // Select tab by name
|
||||
$('#myTab li:first-child button').tab('show') // Select first tab
|
||||
$('#myTab li:last-child button').tab('show') // Select last tab
|
||||
$('#myTab li:nth-child(3) button').tab('show') // Select third tab
|
||||
```
|
||||
|
||||
### Fade effect
|
||||
@@ -562,21 +564,21 @@ To make tabs fade in, add `.fade` to each `.tab-pane`. The first tab pane must a
|
||||
|
||||
#### $().tab
|
||||
|
||||
Activates a tab element and content container. Tab should have either a `data-target` or an `href` targeting a container node in the DOM.
|
||||
Activates a tab element and content container. Tab should have either a `data-target` or, if using a link, an `href` attribute targeting a container node in the DOM.
|
||||
|
||||
```html
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true">Home</a>
|
||||
<button class="nav-link active" id="home-tab" data-toggle="tab" data-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true">Home</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="false">Profile</a>
|
||||
<button class="nav-link" id="profile-tab" data-toggle="tab" data-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false">Profile</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="messages-tab" data-toggle="tab" href="#messages" role="tab" aria-controls="messages" aria-selected="false">Messages</a>
|
||||
<button class="nav-link" id="messages-tab" data-toggle="tab" data-target="#messages" type="button" role="tab" aria-controls="messages" aria-selected="false">Messages</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false">Settings</a>
|
||||
<button class="nav-link" id="settings-tab" data-toggle="tab" data-target="#settings" type="button" role="tab" aria-controls="settings" aria-selected="false">Settings</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -589,7 +591,7 @@ Activates a tab element and content container. Tab should have either a `data-ta
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
$('#myTab li:last-child a').tab('show')
|
||||
$('#myTab li:last-child button').tab('show')
|
||||
})
|
||||
</script>
|
||||
```
|
||||
@@ -645,7 +647,7 @@ If no tab was already active, then the `hide.bs.tab` and `hidden.bs.tab` events
|
||||
</table>
|
||||
|
||||
```js
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
|
||||
$('button[data-toggle="tab"]').on('shown.bs.tab', function (event) {
|
||||
event.target // newly activated tab
|
||||
event.relatedTarget // previous active tab
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ While the `.disabled` class uses `pointer-events: none` to _try_ to disable the
|
||||
<nav aria-label="...">
|
||||
<ul class="pagination">
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1" aria-disabled="true">Previous</a>
|
||||
<a class="page-link">Previous</a>
|
||||
</li>
|
||||
<li class="page-item"><a class="page-link" href="#">1</a></li>
|
||||
<li class="page-item active" aria-current="page">
|
||||
@@ -128,7 +128,7 @@ Change the alignment of pagination components with [flexbox utilities]({{< docsr
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1" aria-disabled="true">Previous</a>
|
||||
<a class="page-link">Previous</a>
|
||||
</li>
|
||||
<li class="page-item"><a class="page-link" href="#">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
||||
@@ -144,7 +144,7 @@ Change the alignment of pagination components with [flexbox utilities]({{< docsr
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-end">
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1" aria-disabled="true">Previous</a>
|
||||
<a class="page-link">Previous</a>
|
||||
</li>
|
||||
<li class="page-item"><a class="page-link" href="#">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
||||
|
||||
@@ -22,6 +22,10 @@ Things to know when using the popover plugin:
|
||||
- Popovers must be hidden before their corresponding elements have been removed from the DOM.
|
||||
- Popovers can be triggered thanks to an element inside a shadow DOM.
|
||||
|
||||
{{< callout info >}}
|
||||
{{< partial "callout-info-sanitizer.md" >}}
|
||||
{{< /callout >}}
|
||||
|
||||
{{< callout info >}}
|
||||
{{< partial "callout-info-prefersreducedmotion.md" >}}
|
||||
{{< /callout >}}
|
||||
@@ -268,7 +272,7 @@ Note that for security reasons the `sanitize`, `sanitizeFn` and `whiteList` opti
|
||||
<td>sanitize</td>
|
||||
<td>boolean</td>
|
||||
<td>true</td>
|
||||
<td>Enable or disable the sanitization. If activated <code>'template'</code>, <code>'content'</code> and <code>'title'</code> options will be sanitized.</td>
|
||||
<td>Enable or disable the sanitization. If activated <code>'template'</code>, <code>'content'</code> and <code>'title'</code> options will be sanitized. See the <a href="{{< docsref "/getting-started/javascript#sanitizer" >}}">sanitizer section in our JavaScript documentation</a>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>whiteList</td>
|
||||
|
||||
@@ -33,7 +33,7 @@ Scroll the area below the navbar and watch the active class change. The dropdown
|
||||
<a class="nav-link" href="#mdo">@mdo</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Dropdown</a>
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-expanded="false">Dropdown</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#one">one</a>
|
||||
<a class="dropdown-item" href="#two">two</a>
|
||||
@@ -69,7 +69,7 @@ Scroll the area below the navbar and watch the active class change. The dropdown
|
||||
<a class="nav-link" href="#mdo">@mdo</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Dropdown</a>
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-expanded="false">Dropdown</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="#one">one</a>
|
||||
<a class="dropdown-item" href="#two">two</a>
|
||||
|
||||
@@ -47,7 +47,7 @@ Toasts are as flexible as you need and have very little required markup. At a mi
|
||||
|
||||
### Live
|
||||
|
||||
Click the button the below to show as toast (positioning with our utilities in the lower right corner) that has been hidden by default with `.hide`.
|
||||
Click the button below to show a toast (positioned with our utilities in the lower right corner) that has been hidden by default with `.hide`.
|
||||
|
||||
<div class="position-fixed bottom-0 right-0 p-3" style="z-index: 5; right: 0; bottom: 0;">
|
||||
<div id="liveToast" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" data-delay="2000">
|
||||
@@ -91,7 +91,7 @@ Click the button the below to show as toast (positioning with our utilities in t
|
||||
|
||||
### Translucent
|
||||
|
||||
Toasts are slightly translucent, too, so they blend over whatever they might appear over. For browsers that support the `backdrop-filter` CSS property, we'll also attempt to blur the elements under a toast.
|
||||
Toasts are slightly translucent to blend in with what's below them.
|
||||
|
||||
{{< example class="bg-dark" >}}
|
||||
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
@@ -229,13 +229,13 @@ You can also get fancy with flexbox utilities to align toasts horizontally and/o
|
||||
|
||||
## Accessibility
|
||||
|
||||
Toasts are intended to be small interruptions to your visitors or users, so to help those with screen readers and similar assistive technologies, you should wrap your toasts in an [`aria-live` region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). Changes to live regions (such as injecting/updating a toast component) are automatically announced by screen readers without needing to move the user's focus or otherwise interrupt the user. Additionally, include `aria-atomic="true"` to ensure that the entire toast is always announced as a single (atomic) unit, rather than announcing what was changed (which could lead to problems if you only update part of the toast's content, or if displaying the same toast content at a later point in time). If the information needed is important for the process, e.g. for a list of errors in a form, then use the [alert component]({{< docsref "/components/alerts" >}}) instead of toast.
|
||||
Toasts are intended to be small interruptions to your visitors or users, so to help those with screen readers and similar assistive technologies, you should wrap your toasts in an [`aria-live` region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). Changes to live regions (such as injecting/updating a toast component) are automatically announced by screen readers without needing to move the user's focus or otherwise interrupt the user. Additionally, include `aria-atomic="true"` to ensure that the entire toast is always announced as a single (atomic) unit, rather than just announcing what was changed (which could lead to problems if you only update part of the toast's content, or if displaying the same toast content at a later point in time). If the information needed is important for the process, e.g. for a list of errors in a form, then use the [alert component]({{< docsref "/components/alerts" >}}) instead of toast.
|
||||
|
||||
Note that the live region needs to be present in the markup *before* the toast is generated or updated. If you dynamically generate both at the same time and inject them into the page, they will generally not be announced by assistive technologies.
|
||||
|
||||
You also need to adapt the `role` and `aria-live` level depending on the content. If it's an important message like an error, use `role="alert" aria-live="assertive"`, otherwise use `role="status" aria-live="polite"` attributes.
|
||||
|
||||
As the content you're displaying changes, be sure to update the [`delay` timeout](#options) to ensure people have enough time to read the toast.
|
||||
As the content you're displaying changes, be sure to update the [`delay` timeout](#options) so that users have enough time to read the toast.
|
||||
|
||||
```html
|
||||
<div class="toast" role="alert" aria-live="polite" aria-atomic="true" data-delay="10000">
|
||||
@@ -261,6 +261,8 @@ When using `autohide: false`, you must add a close button to allow users to dism
|
||||
</div>
|
||||
{{< /example >}}
|
||||
|
||||
While technically it's possible to add focusable/actionable controls (such as additional buttons or links) in your toast, you should avoid doing this for autohiding toasts. Even if you give the toast a long [`delay` timeout](#options), keyboard and assistive technology users may find it difficult to reach the toast in time to take action (since toasts don't receive focus when they are displayed). If you absolutely must have further controls, we recommend using a toast with `autohide: false`.
|
||||
|
||||
## JavaScript behavior
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -21,6 +21,10 @@ Things to know when using the tooltip plugin:
|
||||
- Tooltips must be hidden before their corresponding elements have been removed from the DOM.
|
||||
- Tooltips can be triggered thanks to an element inside a shadow DOM.
|
||||
|
||||
{{< callout info >}}
|
||||
{{< partial "callout-info-sanitizer.md" >}}
|
||||
{{< /callout >}}
|
||||
|
||||
{{< callout info >}}
|
||||
{{< partial "callout-info-prefersreducedmotion.md" >}}
|
||||
{{< /callout >}}
|
||||
@@ -269,7 +273,7 @@ Note that for security reasons the `sanitize`, `sanitizeFn` and `whiteList` opti
|
||||
<td>sanitize</td>
|
||||
<td>boolean</td>
|
||||
<td>true</td>
|
||||
<td>Enable or disable the sanitization. If activated <code>'template'</code> and <code>'title'</code> options will be sanitized.</td>
|
||||
<td>Enable or disable the sanitization. If activated <code>'template'</code> and <code>'title'</code> options will be sanitized. See the <a href="{{< docsref "/getting-started/javascript#sanitizer" >}}">sanitizer section in our JavaScript documentation</a>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>whiteList</td>
|
||||
|
||||
@@ -53,7 +53,7 @@ Align images with the [helper float classes]({{< docsref "/utilities/float" >}})
|
||||
If you are using the `<picture>` element to specify multiple `<source>` elements for a specific `<img>`, make sure to add the `.img-*` classes to the `<img>` and not to the `<picture>` tag.
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<picture>
|
||||
<source srcset="..." type="image/svg+xml">
|
||||
<img src="..." class="img-fluid img-thumbnail" alt="...">
|
||||
</picture>
|
||||
|
||||
@@ -52,7 +52,7 @@ $font-family-sans-serif:
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default;
|
||||
```
|
||||
|
||||
Note that because the font stack includes emoji fonts, many common symbol/dingbat unicode characters will be rendered as multi-colored pictographs. Their appearance will vary, depending on the style used in the browser/platform's native emoji font, and they won't be affected by any CSS `color` styles.
|
||||
Note that because the font stack includes emoji fonts, many common symbol/dingbat Unicode characters will be rendered as multi-colored pictographs. Their appearance will vary, depending on the style used in the browser/platform's native emoji font, and they won't be affected by any CSS `color` styles.
|
||||
|
||||
This `font-family` is applied to the `<body>` and automatically inherited globally throughout Bootstrap. To switch the global `font-family`, update `$font-family-base` and recompile Bootstrap.
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ Use text utilities as needed to change the alignment of your blockquote.
|
||||
|
||||
{{< example >}}
|
||||
<blockquote class="blockquote text-center">
|
||||
<p class="mb-0">>A well-known quote, contained in a blockquote element.</p>
|
||||
<p class="mb-0">A well-known quote, contained in a blockquote element.</p>
|
||||
<footer class="blockquote-footer">Someone famous in <cite title="Source Title">Source Title</cite></footer>
|
||||
</blockquote>
|
||||
{{< /example >}}
|
||||
@@ -283,4 +283,4 @@ Align terms and descriptions horizontally by using our grid system's predefined
|
||||
|
||||
As of v4.3.0, Bootstrap ships with the option to enable responsive font sizes, allowing text to scale more naturally across device and viewport sizes. <abbr title="Responsive font sizes">RFS</abbr> can be enabled by changing the `$enable-responsive-font-sizes` Sass variable to `true` and recompiling Bootstrap.
|
||||
|
||||
To support <abbr title="Responsive font sizes">RFS</abbr>, we use a Sass mixin to replace our normal `font-size` properties. Responsive font sizes will be compiled into `calc()` functions with a mix of `rem` and viewport units to enable the responsive scaling behavior. More about <abbr title="Responsive font sizes">RFS</abbr> and its configuration can be found on its [GitHub repository](https://github.com/twbs/rfs/tree/v8.0.4).
|
||||
To support <abbr title="Responsive font sizes">RFS</abbr>, we use a Sass mixin to replace our normal `font-size` properties. Responsive font sizes will be compiled into `calc()` functions with a mix of `rem` and viewport units to enable the responsive scaling behavior. More about <abbr title="Responsive font sizes">RFS</abbr> and its configuration can be found on its [GitHub repository](https://github.com/twbs/rfs/tree/v8.1.0).
|
||||
|
||||
@@ -606,6 +606,7 @@ Responsive variations also exist for `align-content`.
|
||||
- `.align-content{{ .abbr }}-start`
|
||||
- `.align-content{{ .abbr }}-end`
|
||||
- `.align-content{{ .abbr }}-center`
|
||||
- `.align-content{{ .abbr }}-between`
|
||||
- `.align-content{{ .abbr }}-around`
|
||||
- `.align-content{{ .abbr }}-stretch`
|
||||
{{- end -}}
|
||||
|
||||
+48
-124
@@ -10,7 +10,6 @@
|
||||
.bd-content > h4[id] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bd-content > h2[id]::before,
|
||||
.bd-content > h3[id]::before,
|
||||
.bd-content > h4[id]::before {
|
||||
@@ -19,12 +18,10 @@
|
||||
margin-top: -6rem;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.bd-content > .table {
|
||||
max-width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.bd-content > .table {
|
||||
display: block;
|
||||
@@ -34,17 +31,14 @@
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-content > .table th:first-child,
|
||||
.bd-content > .table td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.bd-content > .table th:not(:last-child),
|
||||
.bd-content > .table td:not(:last-child) {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.bd-content > .table td:first-child > code {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -57,18 +51,16 @@
|
||||
.bd-content > h2:not(:first-child) {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.bd-content > h3 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.bd-content > ul li,
|
||||
.bd-content > ol li {
|
||||
margin-bottom: .25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.bd-title {
|
||||
margin-bottom: .5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
@@ -93,13 +85,12 @@
|
||||
background-color: #563d7c;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.skippy:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.skippy-text {
|
||||
padding: .5em;
|
||||
padding: 0.5em;
|
||||
outline: 1px dotted;
|
||||
}
|
||||
|
||||
@@ -114,7 +105,6 @@
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar .dropdown-menu .dropdown-item {
|
||||
white-space: normal;
|
||||
text-transform: capitalize;
|
||||
@@ -136,10 +126,9 @@
|
||||
align-items: center;
|
||||
color: #212529;
|
||||
text-decoration: none;
|
||||
padding: .3rem 0.5rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border-top: solid 1px #dee2e6;
|
||||
}
|
||||
|
||||
.docs-sidebar a.dropdown-toggle::after {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
@@ -165,7 +154,6 @@
|
||||
border-bottom: solid 1px #dee2e6;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-sidebar .active > a {
|
||||
background: #dee2e6;
|
||||
font-weight: bold;
|
||||
@@ -190,46 +178,38 @@
|
||||
padding-top: 1rem;
|
||||
border-left: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
#TableOfContents > ul ul {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
#TableOfContents > ul li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#TableOfContents > ul li a {
|
||||
display: block;
|
||||
padding: .125rem 1.5rem;
|
||||
padding: 0.125rem 1.5rem;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
#TableOfContents > ul li a:hover {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.bd-toc nav {
|
||||
padding-top: .125em;
|
||||
padding-bottom: .125em;
|
||||
padding-top: 0.125em;
|
||||
padding-bottom: 0.125em;
|
||||
border-left: 0.25em solid #e9ecef;
|
||||
}
|
||||
|
||||
.bd-toc nav > ul {
|
||||
padding-left: .5rem;
|
||||
padding-left: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bd-toc nav > ul > li {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bd-toc nav li ul li {
|
||||
margin-bottom: .25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.bd-toc nav a code {
|
||||
font: inherit;
|
||||
}
|
||||
@@ -238,16 +218,13 @@
|
||||
font-size: 0.875rem;
|
||||
color: #63707c;
|
||||
}
|
||||
|
||||
.bd-footer a {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.bd-footer a:hover, .bd-footer a:focus {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.bd-footer p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -256,23 +233,20 @@
|
||||
padding-left: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.bd-footer-links li {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bd-footer-links li + li {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.bd-example-row .row > .col,
|
||||
.bd-example-row .row > [class^="col-"] {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
.bd-example-row .row > [class^=col-] {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
background-color: rgba(86, 61, 124, 0.15);
|
||||
border: 1px solid rgba(86, 61, 124, 0.2);
|
||||
}
|
||||
|
||||
.bd-example-row .row + .row {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -287,9 +261,9 @@
|
||||
border: 1px solid rgba(86, 61, 124, 0.15);
|
||||
}
|
||||
|
||||
.bd-example-responsive-containers [class^="container"] {
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
.bd-example-responsive-containers [class^=container] {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
background-color: rgba(86, 61, 124, 0.15);
|
||||
border: 1px solid rgba(86, 61, 124, 0.2);
|
||||
}
|
||||
@@ -316,18 +290,16 @@
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.example-content-main {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.example-content-main {
|
||||
flex: 0 0 66.666667%;
|
||||
max-width: 66.666667%;
|
||||
flex: 0 0 66.66666667%;
|
||||
max-width: 66.66666667%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,18 +309,16 @@
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.example-content-secondary {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.example-content-secondary {
|
||||
flex: 0 0 33.333333%;
|
||||
max-width: 33.333333%;
|
||||
flex: 0 0 33.33333333%;
|
||||
max-width: 33.33333333%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,44 +327,37 @@
|
||||
padding: 1rem;
|
||||
margin: 1rem -15px 0;
|
||||
border: solid #f8f9fa;
|
||||
border-width: .2rem 0 0;
|
||||
border-width: 0.2rem 0 0;
|
||||
}
|
||||
|
||||
.bd-example::after {
|
||||
display: block;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.bd-example {
|
||||
padding: 1.5rem;
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
border-width: .2rem;
|
||||
border-width: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-example + .highlight,
|
||||
.bd-example + .clipboard + .highlight {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.bd-example + p {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.bd-example > .form-control + .form-control {
|
||||
margin-top: .5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.bd-example > .nav + .nav,
|
||||
.bd-example > .alert + .alert,
|
||||
.bd-example > .navbar + .navbar,
|
||||
.bd-example > .progress + .progress {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.bd-example > .dropdown-menu {
|
||||
position: static;
|
||||
display: block;
|
||||
@@ -402,24 +365,23 @@
|
||||
|
||||
.bd-example > svg + svg,
|
||||
.bd-example > img + img {
|
||||
margin-left: .5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.bd-example > .btn,
|
||||
.bd-example > .btn-group {
|
||||
margin-top: .25rem;
|
||||
margin-bottom: .25rem;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.bd-example > .btn-toolbar + .btn-toolbar {
|
||||
margin-top: .5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.bd-example > .list-group {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.bd-example > [class*="list-group-horizontal"] {
|
||||
.bd-example > [class*=list-group-horizontal] {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -428,12 +390,10 @@
|
||||
position: static;
|
||||
margin: -1rem -1rem 1rem;
|
||||
}
|
||||
|
||||
.bd-example .fixed-bottom {
|
||||
position: static;
|
||||
margin: 1rem -1rem -1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.bd-example .fixed-top,
|
||||
.bd-example .sticky-top {
|
||||
@@ -445,14 +405,13 @@
|
||||
}
|
||||
|
||||
.bd-example .pagination {
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.bd-example-modal {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.bd-example-modal .modal {
|
||||
position: static;
|
||||
display: block;
|
||||
@@ -465,7 +424,7 @@
|
||||
.scrollspy-example {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
margin-top: .5rem;
|
||||
margin-top: 0.5rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -475,15 +434,15 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.bd-example-border-utils [class^="border"] {
|
||||
.bd-example-border-utils [class^=border] {
|
||||
display: inline-block;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: .25rem;
|
||||
margin: 0.25rem;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.bd-example-border-utils-0 [class^="border"] {
|
||||
.bd-example-border-utils-0 [class^=border] {
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
@@ -494,7 +453,6 @@
|
||||
background-color: #f8f9fa;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.highlight {
|
||||
padding: 1.5rem;
|
||||
@@ -505,7 +463,6 @@
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.bd-content .highlight {
|
||||
margin-right: 0;
|
||||
@@ -520,7 +477,6 @@
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.highlight pre code {
|
||||
font-size: inherit;
|
||||
color: #212529;
|
||||
@@ -531,30 +487,25 @@
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border: 1px solid #e9ecef;
|
||||
border-left-width: .25rem;
|
||||
border-left-width: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.bd-callout h4 {
|
||||
margin-bottom: .25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.bd-callout p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bd-callout code {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.bd-callout + .bd-callout {
|
||||
margin-top: -.25rem;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
.bd-callout-info {
|
||||
border-left-color: #5bc0de;
|
||||
}
|
||||
|
||||
.bd-callout-info h4 {
|
||||
color: #5bc0de;
|
||||
}
|
||||
@@ -562,7 +513,6 @@
|
||||
.bd-callout-warning {
|
||||
border-left-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.bd-callout-warning h4 {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
@@ -570,7 +520,6 @@
|
||||
.bd-callout-danger {
|
||||
border-left-color: #d9534f;
|
||||
}
|
||||
|
||||
.bd-callout-danger h4 {
|
||||
color: #d9534f;
|
||||
}
|
||||
@@ -690,11 +639,9 @@
|
||||
display: none;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.bd-clipboard + .highlight {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bd-clipboard {
|
||||
display: block;
|
||||
@@ -703,18 +650,17 @@
|
||||
|
||||
.btn-clipboard {
|
||||
position: absolute;
|
||||
top: .5rem;
|
||||
right: .5rem;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
padding: .25rem .5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75em;
|
||||
color: #343a40;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-clipboard:hover {
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
@@ -740,9 +686,7 @@
|
||||
/* LiteralStringDelimiter */
|
||||
.chroma .dl {
|
||||
color: #4070a0;
|
||||
}
|
||||
|
||||
/* NEW */
|
||||
} /* NEW */
|
||||
/* Comment */
|
||||
.chroma .c {
|
||||
color: #727272;
|
||||
@@ -757,9 +701,7 @@
|
||||
.chroma .ch {
|
||||
font-style: italic;
|
||||
color: #60a0b0;
|
||||
}
|
||||
|
||||
/* NEW */
|
||||
} /* NEW */
|
||||
/* CommentMultiline */
|
||||
.chroma .cm {
|
||||
color: #727272;
|
||||
@@ -773,9 +715,7 @@
|
||||
/* CommentPreprocFile */
|
||||
.chroma .cpf {
|
||||
color: #007020;
|
||||
}
|
||||
|
||||
/* NEW */
|
||||
} /* NEW */
|
||||
/* CommentSpecial */
|
||||
.chroma .cs {
|
||||
color: #727272;
|
||||
@@ -806,9 +746,7 @@
|
||||
/* GenericUnderline */
|
||||
.chroma .gl {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* NEW */
|
||||
} /* NEW */
|
||||
/* GenericOutput */
|
||||
.chroma .go {
|
||||
color: #aaa;
|
||||
@@ -827,9 +765,7 @@
|
||||
/* GenericStrong */
|
||||
.chroma .gs {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* NEW */
|
||||
} /* NEW */
|
||||
/* GenericTraceback */
|
||||
.chroma .gt {
|
||||
color: #9c6;
|
||||
@@ -893,9 +829,7 @@
|
||||
/* LiteralNumberBin */
|
||||
.chroma .mb {
|
||||
color: #40a070;
|
||||
}
|
||||
|
||||
/* NEW */
|
||||
} /* NEW */
|
||||
/* LiteralNumberHex */
|
||||
.chroma .mh {
|
||||
color: #c24f19;
|
||||
@@ -999,9 +933,7 @@
|
||||
/* LiteralStringAffix */
|
||||
.chroma .sa {
|
||||
color: #4070a0;
|
||||
}
|
||||
|
||||
/* NEW */
|
||||
} /* NEW */
|
||||
/* LiteralStringBacktick */
|
||||
.chroma .sb {
|
||||
color: #c30;
|
||||
@@ -1059,12 +991,10 @@
|
||||
content: "$ ";
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chroma .language-bash .m,
|
||||
.chroma .language-sh .m {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.chroma .language-powershell::before {
|
||||
color: #009;
|
||||
content: "PM> ";
|
||||
@@ -1076,13 +1006,11 @@
|
||||
color: rgba(0, 123, 255, 0.5);
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.anchorjs-link {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.anchorjs-link:hover {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
@@ -1092,26 +1020,22 @@
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.path-admin-tool-componentlibrary #page.drawers .main-inner {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.path-admin-tool-componentlibrary #page.drawers div[role="main"] {
|
||||
.path-admin-tool-componentlibrary #page.drawers div[role=main] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.path-admin-tool-componentlibrary #page-header,
|
||||
.path-admin-tool-componentlibrary #maincontent,
|
||||
.path-admin-tool-componentlibrary .secondarynavigation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.path-admin-tool-componentlibrary #region-main {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=docs.css.map */
|
||||
/*# sourceMappingURL=docs.css.map */
|
||||
|
||||
+1
-110
File diff suppressed because one or more lines are too long
@@ -82,7 +82,7 @@
|
||||
.bd-example {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
margin: 1rem (-$grid-gutter-width / 2) 0;
|
||||
margin: 1rem (-$grid-gutter-width * 0.5) 0;
|
||||
border: solid $gray-100;
|
||||
border-width: .2rem 0 0;
|
||||
@include clearfix();
|
||||
@@ -248,8 +248,8 @@
|
||||
}
|
||||
|
||||
.bd-content .highlight {
|
||||
margin-right: (-$grid-gutter-width / 2);
|
||||
margin-left: (-$grid-gutter-width / 2);
|
||||
margin-right: (-$grid-gutter-width * 0.5);
|
||||
margin-left: (-$grid-gutter-width * 0.5);
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-right: 0;
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
- name: primary
|
||||
- name: blue
|
||||
hex: "#007bff"
|
||||
- name: secondary
|
||||
hex: "#868e96"
|
||||
- name: success
|
||||
hex: "#28a745"
|
||||
- name: danger
|
||||
- name: indigo
|
||||
hex: "#6610f2"
|
||||
- name: purple
|
||||
hex: "#6f42c1"
|
||||
- name: pink
|
||||
hex: "#e83e8c"
|
||||
- name: red
|
||||
hex: "#dc3545"
|
||||
- name: warning
|
||||
- name: orange
|
||||
hex: "#fd7e14"
|
||||
- name: yellow
|
||||
hex: "#ffc107"
|
||||
- name: info
|
||||
- name: green
|
||||
hex: "#28a745"
|
||||
- name: teal
|
||||
hex: "#20c997"
|
||||
- name: cyan
|
||||
hex: "#17a2b8"
|
||||
- name: light
|
||||
hex: "#f8f9fa"
|
||||
- name: dark
|
||||
- name: white
|
||||
hex: "#fff"
|
||||
- name: gray
|
||||
hex: "#6c757d"
|
||||
- name: gray-dark
|
||||
hex: "#343a40"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
- name: 500
|
||||
hex: "#adb5bd"
|
||||
- name: 600
|
||||
hex: "#868e96"
|
||||
hex: "#6c757d"
|
||||
- name: 700
|
||||
hex: "#495057"
|
||||
- name: 800
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- name: primary
|
||||
hex: "#007bff"
|
||||
- name: secondary
|
||||
hex: "#868e96"
|
||||
hex: "#6c757d"
|
||||
- name: success
|
||||
hex: "#28a745"
|
||||
- name: danger
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="{{ .Page.Params.description | default .Site.Params.description | markdownify }}">
|
||||
<meta name="author" content="{{ .Site.Params.authors }}">
|
||||
<meta name="generator" content="Hugo {{ hugo.Version }}">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{{- /*
|
||||
Usage: `callout "type"`,
|
||||
where type is one of info (default), danger, warning
|
||||
Usage: `callout "type"`, where `type` is one of info (default), danger, or warning
|
||||
*/ -}}
|
||||
|
||||
{{- $css_class := .Get 0 | default "info" -}}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
{{- /*
|
||||
Usage: `example [args]`
|
||||
Usage: `example args`
|
||||
|
||||
`args` are optional and can be one of the following:
|
||||
id: the `div`'s id - default: ""
|
||||
class: any extra class(es) to be added to the `div` - default ""
|
||||
show_preview: if the preview should be output in the HTML - default: `true`
|
||||
show_markup: if the markup should be output in the HTML - default: `true`
|
||||
`args` are all optional and can be one of the following:
|
||||
* id: the `div`'s id - default: ""
|
||||
* class: any extra class(es) to be added to the `div` - default: ""
|
||||
* show_preview: if the preview should be output in the HTML - default: `true`
|
||||
* show_markup: if the markup should be output in the HTML - default: `true`
|
||||
*/ -}}
|
||||
|
||||
{{- $id := .Get "id" -}}
|
||||
{{- $class := .Get "class" -}}
|
||||
{{- $lang := .Get "lang" | default "html" -}}
|
||||
{{- $show_preview := .Get "show_preview" | default true -}}
|
||||
{{- $show_markup := .Get "show_markup" | default true -}}
|
||||
{{- $input := .Inner -}}
|
||||
{{- $inputrendered := replaceRE `{{#js}}` "<div class='hidden' data-action='runjs'>" $input -}}
|
||||
{{- $inputrendered = replaceRE `{{/js}}` "</div>" $inputrendered -}}
|
||||
|
||||
{{- if eq $show_preview true -}}
|
||||
<div{{ with .Get "id" }} id="{{ . }}"{{ end }} class="bd-example{{ with .Get "class" }} {{ . }}{{ end }}">
|
||||
{{- $inputrendered | safeHTML -}}
|
||||
<div{{ with $id }} id="{{ . }}"{{ end }} class="bd-example{{ with $class }} {{ . }}{{ end }}">
|
||||
{{- $input -}}
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
||||
{{- if eq $show_markup true -}}
|
||||
{{- $content := replaceRE `<svg class="bd\-placeholder\-img(?:\-lg)?(?: *?bd\-placeholder\-img\-lg)? ?(.*?)".*?<\/svg>\n` `<img src="..." class="$1" alt="...">` $input -}}
|
||||
{{- $content = replaceRE `(class=" *?")` "" $content -}}
|
||||
{{- highlight (trim $content "\n") "html" "" -}}
|
||||
{{- $content := replaceRE `<svg class="bd-placeholder-img(?:-lg)?(?: *?bd-placeholder-img-lg)? ?(.*?)".*?<\/svg>\n` `<img src="..." class="$1" alt="...">` $input -}}
|
||||
{{- $content = replaceRE ` (class=" *?")` "" $content -}}
|
||||
{{- highlight (trim $content "\n") $lang "" -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{{- /*
|
||||
Work around wrong escapes in integrity attributes.
|
||||
Original: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/shortcodes/param.html
|
||||
*/ -}}
|
||||
|
||||
{{- $name := .Get 0 -}}
|
||||
{{- with $name -}}
|
||||
{{- $value := $.Page.Param . -}}
|
||||
{{- /* If any parameter ends with `_hash`, mark the string as safe HTML */ -}}
|
||||
{{- if (strings.HasSuffix $name "_hash") -}}
|
||||
{{- $value = $value | safeHTML -}}
|
||||
{{- end -}}
|
||||
{{- with $value }}{{ . }}{{ else }}{{ errorf "Param %q not found: %s" $name $.Position }}{{ end -}}
|
||||
{{- else }}{{ errorf "Missing param key: %s" $.Position }}{{ end -}}
|
||||
@@ -1,21 +1,24 @@
|
||||
{{- /*
|
||||
Usage: `placeholder args`
|
||||
|
||||
args can be one of the following:
|
||||
title: Used in the SVG `title` tag, default "Placeholder"
|
||||
text: The text to show in the image - default: "width x height"
|
||||
class: default: "bd-placeholder-img"
|
||||
color: The text color (foreground) - default: "#dee2e6"
|
||||
background: The background color - default: "#868e96"
|
||||
width: default: 100%
|
||||
height: default: 180px
|
||||
`args` are all optional and can be one of the following:
|
||||
* title: Used in the SVG `title` tag - default: "Placeholder"
|
||||
* text: The text to show in the image - default: "width x height"
|
||||
* class: Class to add to the `svg` - default: "bd-placeholder-img"
|
||||
* color: The text color (foreground) - default: "#dee2e6"
|
||||
* background: The background color - default: "#868e96"
|
||||
* width: default: "100%"
|
||||
* height: default: "180px"
|
||||
*/ -}}
|
||||
|
||||
{{- $grays := $.Site.Data.grays -}}
|
||||
{{- $default_color := (index $grays 2).hex -}}
|
||||
{{- $default_background := (index $grays 5).hex -}}
|
||||
|
||||
{{- $title := .Get "title" | default "Placeholder" -}}
|
||||
{{- $class := .Get "class" -}}
|
||||
{{- $color := .Get "color" | default (index $grays 2).hex -}}
|
||||
{{- $background := .Get "background" | default (index $grays 5).hex -}}
|
||||
{{- $color := .Get "color" | default $default_color -}}
|
||||
{{- $background := .Get "background" | default $default_background -}}
|
||||
{{- $width := .Get "width" | default "100%" -}}
|
||||
{{- $height := .Get "height" | default "180" -}}
|
||||
{{- $text := .Get "text" | default (printf "%sx%s" $width $height) -}}
|
||||
@@ -23,8 +26,8 @@
|
||||
{{- $show_title := not (eq $title "false") -}}
|
||||
{{- $show_text := not (eq $text "false") -}}
|
||||
|
||||
<svg class="bd-placeholder-img{{ with $class }} {{ . }}{{ end }}" width="{{ $width }}" height="{{ $height }}" xmlns="http://www.w3.org/2000/svg"{{ if (or $show_title $show_text) }} aria-label="{{ if $show_title }}{{ $title }}{{ if $show_text }}: {{ end }}{{ end }}{{ if ($show_text) }}{{ $text }}{{ end }}"{{ end }} preserveAspectRatio="xMidYMid slice" role="img">
|
||||
{{- if $show_title -}}<title>{{ $title }}</title>{{- end -}}
|
||||
<svg class="bd-placeholder-img{{ with $class }} {{ . }}{{ end }}" width="{{ $width }}" height="{{ $height }}" xmlns="http://www.w3.org/2000/svg"{{ if (or $show_title $show_text) }} role="img" aria-label="{{ if $show_title }}{{ $title }}{{ if $show_text }}: {{ end }}{{ end }}{{ if ($show_text) }}{{ $text }}{{ end }}"{{ else }} aria-hidden="true"{{ end }} preserveAspectRatio="xMidYMid slice" focusable="false">
|
||||
{{- if $show_title }}<title>{{ $title }}</title>{{ end -}}
|
||||
<rect width="100%" height="100%" fill="{{ $background }}"/>
|
||||
{{- if $show_text -}}<text x="50%" y="50%" fill="{{ $color }}" dy=".3em">{{ $text }}</text>{{- end -}}
|
||||
{{- if $show_text }}<text x="50%" y="50%" fill="{{ $color }}" dy=".3em">{{ $text }}</text>{{ end -}}
|
||||
</svg>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{{- /*
|
||||
Output the current year
|
||||
*/ -}}
|
||||
{{- /* Outputs the current year */ -}}
|
||||
|
||||
{{- now.Format "2006" -}}
|
||||
|
||||
@@ -147,9 +147,7 @@ Feature: Manage plan workflow
|
||||
| name | user | description | status | reviewer |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | active | manager1 |
|
||||
| Test-Plan4 | user1 | Description of plan 4 for user 1 | active | manager1 |
|
||||
And I log in as "manager1"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I follow "User 1"
|
||||
And I am on the "user1" "user > profile" page logged in as "manager1"
|
||||
And I follow "Learning plans"
|
||||
And I should see "List of learning plans"
|
||||
When I click on "Send back to draft" of edit menu in the "Test-Plan3" row
|
||||
@@ -165,9 +163,7 @@ Feature: Manage plan workflow
|
||||
| name | user | description | status | reviewer |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | active | manager1 |
|
||||
| Test-Plan4 | user1 | Description of plan 4 for user 1 | active | manager1 |
|
||||
And I log in as "manager1"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I follow "User 1"
|
||||
And I am on the "user1" "user > profile" page logged in as "manager1"
|
||||
And I follow "Learning plans"
|
||||
And I should see "List of learning plans"
|
||||
When I click on "Complete this learning plan" of edit menu in the "Test-Plan3" row
|
||||
@@ -186,9 +182,7 @@ Feature: Manage plan workflow
|
||||
| name | user | description | status | reviewer |
|
||||
| Test-Plan3 | user1 | Description of plan 3 for user 1 | complete | manager1 |
|
||||
| Test-Plan4 | user1 | Description of plan 4 for user 1 | complete | manager1 |
|
||||
And I log in as "manager1"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I follow "User 1"
|
||||
And I am on the "user1" "user > profile" page logged in as "manager1"
|
||||
And I follow "Learning plans"
|
||||
And I should see "List of learning plans"
|
||||
When I click on "Reopen this learning plan" of edit menu in the "Test-Plan3" row
|
||||
|
||||
@@ -71,8 +71,8 @@
|
||||
</div>
|
||||
<div class="box py-3 modal-footer">
|
||||
<div class="buttons">
|
||||
<input class="btn btn-secondary" type="submit" name="cancel" value="Cancel">
|
||||
<input class="btn btn-primary" type="submit" name="import" value="Continue">
|
||||
<input class="btn btn-secondary" type="submit" name="cancel" value="{{#str}}cancel, core{{/str}}">
|
||||
<input class="btn btn-primary" type="submit" name="import" value="{{#str}}continue, core{{/str}}">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -60,7 +60,7 @@ class user_field_mapping extends persistent {
|
||||
|
||||
// Internal.
|
||||
$choices = $userfieldmapping->get_internalfield_list();
|
||||
$mform->addElement('select', 'internalfield', get_string('userfieldinternalfield', 'tool_oauth2'), $choices);
|
||||
$mform->addElement('selectgroups', 'internalfield', get_string('userfieldinternalfield', 'tool_oauth2'), $choices);
|
||||
$mform->addHelpButton('internalfield', 'userfieldinternalfield', 'tool_oauth2');
|
||||
|
||||
$mform->addElement('hidden', 'action', 'edit');
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
@tool @tool_oauth2 @javascript
|
||||
|
||||
Feature: OAuth2 user profile fields functionality
|
||||
In order to use them later for authentication or repository plugins
|
||||
As an administrator
|
||||
I need to be able to map data fields provided by an Oauth2 provider
|
||||
to custom user profile fields defined by an administrator.
|
||||
|
||||
Background:
|
||||
Given the following "custom profile fields" exist:
|
||||
| datatype | shortname | name | locked |
|
||||
| text | unlocked_field | Unlocked field | 0 |
|
||||
| text | locked_field | Locked field | 1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Server > OAuth 2 services" in site administration
|
||||
|
||||
Scenario: Verify custom user profile field mapping
|
||||
Given I press "Microsoft"
|
||||
And I should see "Create new service: Microsoft"
|
||||
And I set the following fields to these values:
|
||||
| Name | Testing service |
|
||||
| Client ID | thisistheclientid |
|
||||
| Client secret | supersecret |
|
||||
When I press "Save changes"
|
||||
Then I should see "Changes saved"
|
||||
And I should see "Testing service"
|
||||
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
|
||||
|
||||
# Create unlocked field
|
||||
And I click on "Create new user field mapping for issuer \"Testing service\"" "button"
|
||||
And I set the following fields to these values:
|
||||
| External field name | External unlocked |
|
||||
| Internal field name | Unlocked field |
|
||||
And I click on "Save changes" "button"
|
||||
And I should see "unlocked_field"
|
||||
|
||||
# Create locked field
|
||||
And I click on "Create new user field mapping for issuer \"Testing service\"" "button"
|
||||
And I set the following fields to these values:
|
||||
| External field name | External locked |
|
||||
| Internal field name | Locked field |
|
||||
And I click on "Save changes" "button"
|
||||
And I should see "locked_field"
|
||||
@@ -241,9 +241,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I press "Next"
|
||||
And I log out
|
||||
And I log in as "admin"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I follow "Manager"
|
||||
And I am on the "manager" "user > profile" page logged in as "admin"
|
||||
And I follow "Log in as"
|
||||
And I press "Continue"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
|
||||
@@ -563,9 +563,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| user1 | User | 1 | user1@example.com |
|
||||
When I log in as "admin"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I follow "User 1"
|
||||
When I am on the "user1" "user > profile" page logged in as "admin"
|
||||
And I follow "Log in as"
|
||||
Then I should see "You are logged in as User 1"
|
||||
And I press "Continue"
|
||||
@@ -604,8 +602,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
And I follow "User 1"
|
||||
And I am on the "user1" "user > profile" page
|
||||
And I follow "Log in as"
|
||||
Then I should see "You are logged in as User 1"
|
||||
And I press "Continue"
|
||||
|
||||
@@ -111,7 +111,10 @@ class cli_helper {
|
||||
$form = new \admin_uploaduser_form1();
|
||||
[$elements, $defaults] = $form->get_form_for_cli();
|
||||
$options += $this->prepare_form_elements_for_cli($elements, $defaults);
|
||||
$form = new \admin_uploaduser_form2(null, ['columns' => ['type1'], 'data' => []]);
|
||||
// Specify pseudo-column 'type1' to force the form to populate the legacy role mapping selector
|
||||
// but only if user is allowed to assign roles in courses (otherwise form validation will fail).
|
||||
$columns = uu_allowed_roles() ? ['type1'] : [];
|
||||
$form = new \admin_uploaduser_form2(null, ['columns' => $columns, 'data' => []]);
|
||||
[$elements, $defaults] = $form->get_form_for_cli();
|
||||
$options += $this->prepare_form_elements_for_cli($elements, $defaults);
|
||||
return $options;
|
||||
|
||||
@@ -126,7 +126,6 @@ class process {
|
||||
$today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0);
|
||||
$this->today = $today;
|
||||
|
||||
$this->rolecache = uu_allowed_roles_cache(); // Course roles lookup cache.
|
||||
$this->sysrolecache = uu_allowed_sysroles_cache(); // System roles lookup cache.
|
||||
$this->supportedauths = uu_supported_auths(); // Officially supported plugins that are enabled.
|
||||
|
||||
@@ -1209,14 +1208,18 @@ class process {
|
||||
}
|
||||
}
|
||||
|
||||
if (!array_key_exists($courseid, $this->rolecache)) {
|
||||
$this->rolecache[$courseid] = uu_allowed_roles_cache(null, (int)$courseid);
|
||||
}
|
||||
|
||||
if ($courseid == SITEID) {
|
||||
// Technically frontpage does not have enrolments, but only role assignments,
|
||||
// let's not invent new lang strings here for this rarely used feature.
|
||||
|
||||
if (!empty($user->{'role'.$i})) {
|
||||
$rolename = $user->{'role'.$i};
|
||||
if (array_key_exists($rolename, $this->rolecache)) {
|
||||
$roleid = $this->rolecache[$rolename]->id;
|
||||
if (array_key_exists($rolename, $this->rolecache[$courseid]) ) {
|
||||
$roleid = $this->rolecache[$courseid][$rolename]->id;
|
||||
} else {
|
||||
$this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error');
|
||||
continue;
|
||||
@@ -1226,7 +1229,7 @@ class process {
|
||||
|
||||
$a = new \stdClass();
|
||||
$a->course = $shortname;
|
||||
$a->role = $this->rolecache[$roleid]->name;
|
||||
$a->role = $this->rolecache[$courseid][$roleid]->name;
|
||||
$this->upt->track('enrolments', get_string('enrolledincourserole', 'enrol_manual', $a), 'info');
|
||||
}
|
||||
|
||||
@@ -1236,8 +1239,8 @@ class process {
|
||||
$roleid = false;
|
||||
if (!empty($user->{'role'.$i})) {
|
||||
$rolename = $user->{'role'.$i};
|
||||
if (array_key_exists($rolename, $this->rolecache)) {
|
||||
$roleid = $this->rolecache[$rolename]->id;
|
||||
if (array_key_exists($rolename, $this->rolecache[$courseid])) {
|
||||
$roleid = $this->rolecache[$courseid][$rolename]->id;
|
||||
} else {
|
||||
$this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error');
|
||||
continue;
|
||||
@@ -1256,7 +1259,15 @@ class process {
|
||||
}
|
||||
} else {
|
||||
// No role specified, use the default from manual enrol plugin.
|
||||
$roleid = $this->manualcache[$courseid]->roleid;
|
||||
$defaultenrolroleid = (int)$this->manualcache[$courseid]->roleid;
|
||||
// Validate the current user can assign this role.
|
||||
if (array_key_exists($defaultenrolroleid, $this->rolecache[$courseid]) ) {
|
||||
$roleid = $defaultenrolroleid;
|
||||
} else {
|
||||
$role = $DB->get_record('role', ['id' => $defaultenrolroleid]);
|
||||
$this->upt->track('enrolments', get_string('unknownrole', 'error', s($role->shortname)), 'error');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($roleid) {
|
||||
@@ -1299,7 +1310,7 @@ class process {
|
||||
|
||||
$a = new \stdClass();
|
||||
$a->course = $shortname;
|
||||
$a->role = $this->rolecache[$roleid]->name;
|
||||
$a->role = $this->rolecache[$courseid][$roleid]->name;
|
||||
$this->upt->track('enrolments', get_string('enrolledincourserole', 'enrol_manual', $a), 'info');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,23 +384,32 @@ function uu_allowed_roles() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns mapping of roles using short role name as index.
|
||||
* Returns assignable roles for current user using short role name and role ID as index.
|
||||
* This function is no longer called without parameters.
|
||||
*
|
||||
* @param int|null $categoryid Id of the category to get roles for. Null means all roles.
|
||||
* @param int|null $categoryid Id of the category to get roles for.
|
||||
* @param int|null $courseid Id of the course to get roles for.
|
||||
* @return array
|
||||
*/
|
||||
function uu_allowed_roles_cache(?int $categoryid = null): array {
|
||||
if (is_null($categoryid)) {
|
||||
$allowedroles = get_assignable_roles(context_course::instance(SITEID), ROLENAME_SHORT);
|
||||
} else {
|
||||
function uu_allowed_roles_cache(?int $categoryid = null, ?int $courseid = null): array {
|
||||
if (!is_null($categoryid) && !is_null($courseid)) {
|
||||
return [];
|
||||
} else if (is_null($categoryid) && !is_null($courseid)) {
|
||||
$allowedroles = get_assignable_roles(context_course::instance($courseid), ROLENAME_SHORT);
|
||||
} else if (is_null($courseid) && !is_null($categoryid)) {
|
||||
$allowedroles = get_assignable_roles(context_coursecat::instance($categoryid), ROLENAME_SHORT);
|
||||
} else {
|
||||
$allowedroles = get_assignable_roles(context_course::instance(SITEID), ROLENAME_SHORT);
|
||||
}
|
||||
|
||||
$rolecache = [];
|
||||
// A role can be searched for by its ID or by its shortname.
|
||||
foreach ($allowedroles as $rid=>$rname) {
|
||||
$rolecache[$rid] = new stdClass();
|
||||
$rolecache[$rid]->id = $rid;
|
||||
$rolecache[$rid]->name = $rname;
|
||||
if (!is_numeric($rname)) { // only non-numeric shortnames are supported!!!
|
||||
// Since numeric short names are allowed, to avoid replacement of another role, we only accept non-numeric values.
|
||||
if (!is_numeric($rname)) {
|
||||
$rolecache[$rname] = new stdClass();
|
||||
$rolecache[$rname]->id = $rid;
|
||||
$rolecache[$rname]->name = $rname;
|
||||
|
||||
@@ -143,7 +143,8 @@ class cli_test extends \advanced_testcase {
|
||||
// Created users have data in the profile fields.
|
||||
$user1 = \core_user::get_user_by_username('reznort');
|
||||
$profilefields1 = profile_user_record($user1->id);
|
||||
$this->assertEquals((object)['superfield' => 'Loves cats'], $profilefields1);
|
||||
$this->assertObjectHasAttribute('superfield', $profilefields1);
|
||||
$this->assertEquals('Loves cats', $profilefields1->superfield);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
<?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/>.
|
||||
|
||||
namespace tool_uploaduser;
|
||||
|
||||
use advanced_testcase;
|
||||
use context_system;
|
||||
use context_course;
|
||||
use context_coursecat;
|
||||
use stdClass;
|
||||
use tool_uploaduser\cli_helper;
|
||||
use tool_uploaduser\local\text_progress_tracker;
|
||||
|
||||
/**
|
||||
* Class upload_users_test
|
||||
*
|
||||
* @package tool_uploaduser
|
||||
* @copyright 2020 Marina Glancy
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class upload_users_test extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Load required test libraries
|
||||
*/
|
||||
public static function setUpBeforeClass(): void {
|
||||
global $CFG;
|
||||
|
||||
require_once("{$CFG->dirroot}/{$CFG->admin}/tool/uploaduser/locallib.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test upload users, enrol and role assignation
|
||||
* @covers \tool_uploadusers::process
|
||||
*/
|
||||
public function test_user_can_upload_with_course_enrolment(): void {
|
||||
|
||||
$this->resetAfterTest();
|
||||
set_config('passwordpolicy', 0);
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create category and course.
|
||||
$coursecat = $this->getDataGenerator()->create_category();
|
||||
$coursecatcontext = context_coursecat::instance($coursecat->id);
|
||||
$course = $this->getDataGenerator()->create_course(['shortname' => 'course01', 'category' => $coursecat->id]);
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
|
||||
// Create user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Create role with capability to upload CSV files, and assign this role to user.
|
||||
$uploadroleid = create_role('upload role', 'uploadrole', '');
|
||||
set_role_contextlevels($uploadroleid, [CONTEXT_SYSTEM]);
|
||||
$systemcontext = context_system::instance();
|
||||
assign_capability('moodle/site:uploadusers', CAP_ALLOW, $uploadroleid, $systemcontext->id);
|
||||
$this->getDataGenerator()->role_assign($uploadroleid, $user->id, $systemcontext->id);
|
||||
|
||||
// Create role with some of allowed capabilities to enrol users, and assign this role to user.
|
||||
$enrolroleid = create_role('enrol role', 'enrolrole', '');
|
||||
set_role_contextlevels($enrolroleid, [CONTEXT_COURSECAT]);
|
||||
assign_capability('enrol/manual:enrol', CAP_ALLOW, $enrolroleid, $coursecatcontext->id);
|
||||
assign_capability('moodle/course:enrolreview', CAP_ALLOW, $enrolroleid, $coursecatcontext->id);
|
||||
assign_capability('moodle/role:assign', CAP_ALLOW, $enrolroleid, $coursecatcontext->id);
|
||||
$this->getDataGenerator()->role_assign($enrolroleid, $user->id, $coursecatcontext->id);
|
||||
|
||||
// User makes assignments.
|
||||
$studentarch = get_archetype_roles('student');
|
||||
$studentrole = array_shift($studentarch);
|
||||
core_role_set_assign_allowed($enrolroleid, $studentrole->id);
|
||||
|
||||
// Flush accesslib.
|
||||
accesslib_clear_all_caches_for_unit_testing();
|
||||
|
||||
// Process CSV file as user.
|
||||
$csv = <<<EOF
|
||||
username,firstname,lastname,email,course1,role1
|
||||
student1,Student,One,s1@example.com,{$course->shortname},{$studentrole->shortname}
|
||||
student2,Student,Two,s2@example.com,{$course->shortname},teacher
|
||||
EOF;
|
||||
$this->setUser($user);
|
||||
$output = $this->process_csv_upload($csv, ['--uutype=' . UU_USER_ADDNEW]);
|
||||
|
||||
$this->assertStringContainsString('Enrolled in "course01" as "student"', $output);
|
||||
$this->assertStringContainsString('Unknown role "teacher"', $output);
|
||||
|
||||
// Check user creation, enrolment and role assignation.
|
||||
$this->assertEquals(1, count_enrolled_users($coursecontext));
|
||||
|
||||
$usersasstudent = get_role_users($studentrole->id, $coursecontext);
|
||||
$this->assertCount(1, $usersasstudent);
|
||||
$this->assertEquals('student1', reset($usersasstudent)->username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test upload users, enrol and assign default role from manual enrol plugin.
|
||||
* @covers \tool_uploadusers::process
|
||||
*/
|
||||
public function test_user_can_upload_with_course_enrolment_default_role(): void {
|
||||
|
||||
$this->resetAfterTest();
|
||||
set_config('passwordpolicy', 0);
|
||||
$this->setAdminUser();
|
||||
|
||||
// Create category and courses.
|
||||
$coursecat = $this->getDataGenerator()->create_category();
|
||||
$coursecatcontext = context_coursecat::instance($coursecat->id);
|
||||
$course1 = $this->getDataGenerator()->create_course(['shortname' => 'course01', 'category' => $coursecat->id]);
|
||||
$course1context = context_course::instance($course1->id);
|
||||
// Change the default role to 'teacher'.
|
||||
set_config('roleid', 4, 'enrol_manual');
|
||||
$course2 = $this->getDataGenerator()->create_course(['shortname' => 'course02', 'category' => $coursecat->id]);
|
||||
$course2context = context_course::instance($course2->id);
|
||||
|
||||
// Create user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Create role with capability to upload CSV files, and assign this role to user.
|
||||
$uploadroleid = create_role('upload role', 'uploadrole', '');
|
||||
set_role_contextlevels($uploadroleid, [CONTEXT_SYSTEM]);
|
||||
$systemcontext = context_system::instance();
|
||||
assign_capability('moodle/site:uploadusers', CAP_ALLOW, $uploadroleid, $systemcontext->id);
|
||||
$this->getDataGenerator()->role_assign($uploadroleid, $user->id, $systemcontext->id);
|
||||
|
||||
// Create role with some of allowed capabilities to enrol users, and assign this role to user.
|
||||
$enrolroleid = create_role('enrol role', 'enrolrole', '');
|
||||
set_role_contextlevels($enrolroleid, [CONTEXT_COURSECAT]);
|
||||
assign_capability('enrol/manual:enrol', CAP_ALLOW, $enrolroleid, $coursecatcontext->id);
|
||||
assign_capability('moodle/course:enrolreview', CAP_ALLOW, $enrolroleid, $coursecatcontext->id);
|
||||
assign_capability('moodle/role:assign', CAP_ALLOW, $enrolroleid, $coursecatcontext->id);
|
||||
$this->getDataGenerator()->role_assign($enrolroleid, $user->id, $coursecatcontext->id);
|
||||
|
||||
// User makes assignments.
|
||||
$studentarch = get_archetype_roles('student');
|
||||
$studentrole = array_shift($studentarch);
|
||||
core_role_set_assign_allowed($enrolroleid, $studentrole->id);
|
||||
|
||||
// Flush accesslib.
|
||||
accesslib_clear_all_caches_for_unit_testing();
|
||||
|
||||
// Process CSV file (no roles specified) as user.
|
||||
$csv = <<<EOF
|
||||
username,firstname,lastname,email,course1,role1
|
||||
student1,Student,One,s1@example.com,{$course1->shortname},
|
||||
student2,Student,Two,s2@example.com,{$course2->shortname},
|
||||
EOF;
|
||||
$this->setUser($user);
|
||||
$output = $this->process_csv_upload($csv, ['--uutype=' . UU_USER_ADDNEW]);
|
||||
|
||||
$this->assertStringContainsString('Enrolled in "course01" as "student"', $output);
|
||||
// This $user cannot assign teacher role.
|
||||
$this->assertStringContainsString('Unknown role "teacher"', $output);
|
||||
|
||||
// Check user creation, enrolment and role assignation.
|
||||
$this->assertEquals(1, count_enrolled_users($course1context));
|
||||
// This $user cannot enrol anyone as teacher.
|
||||
$this->assertEquals(0, count_enrolled_users($course2context));
|
||||
|
||||
// Test user is enrolled as default-manual-enrol-plugin role.
|
||||
$manualenrolinstance = new stdClass;
|
||||
$enrolinstances = enrol_get_instances($course1->id, true);
|
||||
foreach ($enrolinstances as $courseenrolinstance) {
|
||||
if ($courseenrolinstance->enrol === 'manual') {
|
||||
$manualenrolinstance = $courseenrolinstance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$defaulroleidexpected = $manualenrolinstance->roleid ?? 0;
|
||||
// The default role of course01 is student, id 5.
|
||||
$this->assertEquals(5, $defaulroleidexpected);
|
||||
|
||||
$usersasdefaultrole = get_role_users($defaulroleidexpected, $course1context);
|
||||
$this->assertCount(1, $usersasdefaultrole);
|
||||
$this->assertEquals('student1', reset($usersasdefaultrole)->username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cli_helper and mock $_SERVER['argv']
|
||||
*
|
||||
* @param string $filecontent
|
||||
* @param array $mockargv
|
||||
* @return string
|
||||
*/
|
||||
protected function process_csv_upload(string $filecontent, array $mockargv = []): string {
|
||||
$filepath = make_request_directory() . '/upload.csv';
|
||||
file_put_contents($filepath, $filecontent);
|
||||
$mockargv[] = "--file={$filepath}";
|
||||
|
||||
if (array_key_exists('argv', $_SERVER)) {
|
||||
$oldservervars = $_SERVER['argv'];
|
||||
}
|
||||
|
||||
$_SERVER['argv'] = array_merge([''], $mockargv);
|
||||
$clihelper = new cli_helper(text_progress_tracker::class);
|
||||
|
||||
if (isset($oldservervars)) {
|
||||
$_SERVER['argv'] = $oldservervars;
|
||||
} else {
|
||||
unset($_SERVER['argv']);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$clihelper->process();
|
||||
$output = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -14,6 +14,8 @@ import {eventTypes} from './events';
|
||||
|
||||
let currentTour = null;
|
||||
let tourId = null;
|
||||
let restartTourAndKeepProgress = false;
|
||||
let currentStepNo = null;
|
||||
|
||||
/**
|
||||
* Find the first matching tour.
|
||||
@@ -75,6 +77,20 @@ export const init = async(tourDetails, filters) => {
|
||||
resetTourState(tourId);
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for the resize event.
|
||||
window.addEventListener("resize", () => {
|
||||
// Only listen for the running tour.
|
||||
if (currentTour && currentTour.tourRunning) {
|
||||
clearTimeout(window.resizedFinished);
|
||||
window.resizedFinished = setTimeout(() => {
|
||||
// Wait until the resize event has finished.
|
||||
currentStepNo = currentTour.getCurrentStepNumber();
|
||||
restartTourAndKeepProgress = true;
|
||||
resetTourState(tourId);
|
||||
}, 250);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -187,7 +203,13 @@ const startBootstrapTour = (tourId, template, tourConfig) => {
|
||||
});
|
||||
|
||||
currentTour = new BootstrapTour(tourConfig);
|
||||
return currentTour.startTour();
|
||||
let startAt = 0;
|
||||
if (restartTourAndKeepProgress && currentStepNo) {
|
||||
startAt = currentStepNo;
|
||||
restartTourAndKeepProgress = false;
|
||||
currentStepNo = null;
|
||||
}
|
||||
return currentTour.startTour(startAt);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+34
-18
@@ -258,15 +258,7 @@ class api {
|
||||
$user->password = '';
|
||||
$user->confirmed = 1; // Set the user to confirmed.
|
||||
|
||||
// Map supplied issuer user info to Moodle user fields.
|
||||
$userfieldmapping = new \core\oauth2\user_field_mapping();
|
||||
foreach ($userfieldmapping->get_internalfield_list() as $field) {
|
||||
if (isset($userinfo[$field]) && $userinfo[$field]) {
|
||||
$user->$field = $userinfo[$field];
|
||||
}
|
||||
}
|
||||
|
||||
$user->id = user_create_user($user, false, true);
|
||||
$user = self::save_user($userinfo, $user);
|
||||
|
||||
// The linked account is pre-confirmed.
|
||||
$record = new stdClass();
|
||||
@@ -307,15 +299,7 @@ class api {
|
||||
$user->password = '';
|
||||
$user->confirmed = 0; // The user is not yet confirmed.
|
||||
|
||||
// Map supplied issuer user info to Moodle user fields.
|
||||
$userfieldmapping = new \core\oauth2\user_field_mapping();
|
||||
foreach ($userfieldmapping->get_internalfield_list() as $field) {
|
||||
if (isset($userinfo[$field]) && $userinfo[$field]) {
|
||||
$user->$field = $userinfo[$field];
|
||||
}
|
||||
}
|
||||
|
||||
$user->id = user_create_user($user, false, true);
|
||||
$user = self::save_user($userinfo, $user);
|
||||
|
||||
// The linked account is pre-confirmed.
|
||||
$record = new stdClass();
|
||||
@@ -404,4 +388,36 @@ class api {
|
||||
public static function is_enabled() {
|
||||
return is_enabled_auth('oauth2');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user & update the profile fields
|
||||
*
|
||||
* @param array $userinfo
|
||||
* @param object $user
|
||||
* @return object
|
||||
*/
|
||||
private static function save_user(array $userinfo, object $user): object {
|
||||
// Map supplied issuer user info to Moodle user fields.
|
||||
$userfieldmapping = new \core\oauth2\user_field_mapping();
|
||||
$userfieldlist = $userfieldmapping->get_internalfields();
|
||||
$hasprofilefield = false;
|
||||
foreach ($userfieldlist as $field) {
|
||||
if (isset($userinfo[$field]) && $userinfo[$field]) {
|
||||
$user->$field = $userinfo[$field];
|
||||
|
||||
// Check whether the profile fields exist or not.
|
||||
$hasprofilefield = $hasprofilefield || strpos($field, \core_user\fields::PROFILE_FIELD_PREFIX) === 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new user.
|
||||
$user->id = user_create_user($user, false, true);
|
||||
|
||||
// If profile fields exist then save custom profile fields data.
|
||||
if ($hasprofilefield) {
|
||||
profile_save_data($user);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ class auth extends \auth_plugin_base {
|
||||
public function __construct() {
|
||||
$this->authtype = 'oauth2';
|
||||
$this->config = get_config('auth_oauth2');
|
||||
$this->customfields = $this->get_custom_user_profile_fields();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,23 +310,35 @@ class auth extends \auth_plugin_base {
|
||||
return $userdata;
|
||||
}
|
||||
|
||||
$allfields = array_merge($this->userfields, $this->customfields);
|
||||
|
||||
// Go through each field from the external data.
|
||||
foreach ($externaldata as $fieldname => $value) {
|
||||
if (!in_array($fieldname, $this->userfields)) {
|
||||
if (!in_array($fieldname, $allfields)) {
|
||||
// Skip if this field doesn't belong to the list of fields that can be synced with the OAuth2 issuer.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!property_exists($userdata, $fieldname)) {
|
||||
// Just in case this field is on the list, but not part of the user data. This shouldn't happen though.
|
||||
$userhasfield = property_exists($userdata, $fieldname);
|
||||
// Find out if it is a profile field.
|
||||
$isprofilefield = strpos($fieldname, 'profile_field_') === 0;
|
||||
$profilefieldname = str_replace('profile_field_', '', $fieldname);
|
||||
$userhasprofilefield = $isprofilefield && array_key_exists($profilefieldname, $userdata->profile);
|
||||
|
||||
// Just in case this field is on the list, but not part of the user data. This shouldn't happen though.
|
||||
if (!($userhasfield || $userhasprofilefield)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the old value.
|
||||
$oldvalue = (string)$userdata->$fieldname;
|
||||
$oldvalue = $isprofilefield ? (string) $userdata->profile[$profilefieldname] : (string) $userdata->$fieldname;
|
||||
|
||||
// Get the lock configuration of the field.
|
||||
$lockvalue = $this->config->{'field_lock_' . $fieldname};
|
||||
if (!empty($this->config->{'field_lock_' . $fieldname})) {
|
||||
$lockvalue = $this->config->{'field_lock_' . $fieldname};
|
||||
} else {
|
||||
$lockvalue = 'unlocked';
|
||||
}
|
||||
|
||||
// We should update fields that meet the following criteria:
|
||||
// - Lock value set to 'unlocked'; or 'unlockedifempty', given the current value is empty.
|
||||
@@ -525,6 +538,9 @@ class auth extends \auth_plugin_base {
|
||||
exit();
|
||||
} else {
|
||||
\auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true);
|
||||
// We dont have profile loaded on $moodleuser, so load it.
|
||||
require_once($CFG->dirroot.'/user/profile/lib.php');
|
||||
profile_load_custom_fields($moodleuser);
|
||||
$userinfo = $this->update_user($userinfo, $moodleuser);
|
||||
// No redirect, we will complete this login.
|
||||
}
|
||||
|
||||
@@ -31,5 +31,6 @@ if ($ADMIN->fulltree) {
|
||||
|
||||
$authplugin = get_auth_plugin('oauth2');
|
||||
display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields,
|
||||
get_string('auth_fieldlocks_help', 'auth'), false, false);
|
||||
get_string('auth_fieldlocks_help', 'auth'), false, false,
|
||||
$authplugin->customfields);
|
||||
}
|
||||
|
||||
@@ -18,17 +18,16 @@ Feature: Test if the login form provides the correct feedback
|
||||
|
||||
Scenario: Test login language selector
|
||||
Given remote langimport tests are enabled
|
||||
And I log in as "admin"
|
||||
And I navigate to "Language > Language packs" in site administration
|
||||
And I set the field "Available language packs" to "nl,es"
|
||||
And I press "Install selected language pack(s)"
|
||||
And I trigger cron
|
||||
And I am on homepage
|
||||
And I log out
|
||||
And the following "language packs" exist:
|
||||
| language |
|
||||
| nl |
|
||||
| es |
|
||||
And the following config values are set as admin:
|
||||
| langmenu | 1 |
|
||||
And I follow "Log in"
|
||||
And I open the action menu in "region-main" "region"
|
||||
# The line below contains the unicode character U+200E before and after the brackets, please be very careful editing this line.
|
||||
And I choose "Nederlands (nl)" in the open action menu
|
||||
When I choose "Nederlands (nl)" in the open action menu
|
||||
Then I should see "Gebruikersnaam"
|
||||
|
||||
@_file_upload
|
||||
|
||||
@@ -59,9 +59,10 @@ Feature: The my overview block allows users to hide their courses
|
||||
And I click on "Removed from view" "link" in the "Course overview" "block"
|
||||
And I click on ".coursemenubtn" "css_element" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
|
||||
And I click on "Restore to view" "link" in the "//div[@class='card dashboard-card' and contains(.,'Course 2')]" "xpath_element"
|
||||
And I reload the page
|
||||
And I should not see "Course 2" in the "Course overview" "block"
|
||||
And I click on "Removed from view" "button" in the "Course overview" "block"
|
||||
When I click on "All" "link" in the "Course overview" "block"
|
||||
And I reload the page
|
||||
Then I should see "Course 2" in the "Course overview" "block"
|
||||
|
||||
Scenario: Test show toggle functionality w/ favorites
|
||||
|
||||
@@ -146,6 +146,7 @@ Feature: My overview block pagination
|
||||
| student1 | C12 | student |
|
||||
| student1 | C13 | student |
|
||||
When I am on the "My courses" page logged in as "student1"
|
||||
And I wait until ".block_myoverview [data-control='next']" "css_element" exists
|
||||
And I click on "[data-control='next']" "css_element" in the "Course overview" "block"
|
||||
Then the "class" attribute of ".block_myoverview [data-control='next']" "css_element" should contain "disabled"
|
||||
And I log out
|
||||
|
||||
+5
-4
@@ -51,17 +51,18 @@ class recentlyaccesseditems_item_exporter extends \core\external\exporter {
|
||||
protected function get_other_values(renderer_base $output) {
|
||||
global $CFG;
|
||||
require_once($CFG->libdir.'/modinfolib.php');
|
||||
|
||||
$iconurl = get_fast_modinfo($this->data->courseid)->cms[$this->data->cmid]->get_icon_url();
|
||||
$iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter';
|
||||
return array(
|
||||
'viewurl' => (new moodle_url('/mod/'.$this->data->modname.'/view.php',
|
||||
array('id' => $this->data->cmid)))->out(false),
|
||||
'courseviewurl' => (new moodle_url('/course/view.php', array('id' => $this->data->courseid)))->out(false),
|
||||
'icon' => \html_writer::img(
|
||||
get_fast_modinfo($this->data->courseid)->cms[$this->data->cmid]->get_icon_url(),
|
||||
$iconurl,
|
||||
get_string('pluginname', $this->data->modname),
|
||||
['title' => get_string('pluginname', $this->data->modname), 'class' => 'icon']
|
||||
['title' => get_string('pluginname', $this->data->modname), 'class' => "icon $iconclass"]
|
||||
),
|
||||
'purpose' => plugin_supports('mod', $this->data->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER)
|
||||
'purpose' => plugin_supports('mod', $this->data->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<div class="activityiconcontainer small {{purpose}} courseicon align-self-top align-self-center mx-3 mb-1 mb-sm-0 text-nowrap">
|
||||
{{#icon}}
|
||||
{{#iconurl}}
|
||||
<img alt="{{alttext}}" title="{{alttext}}" src="{{{ iconurl }}}" class="icon ">
|
||||
<img alt="{{alttext}}" title="{{alttext}}" src="{{{ iconurl }}}" class="icon {{iconclass}}">
|
||||
{{/iconurl}}
|
||||
{{^iconurl}}
|
||||
{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}
|
||||
|
||||
+6
-1
@@ -62,12 +62,15 @@ class event_icon_exporter extends exporter {
|
||||
$isgroupevent = ($group && !empty($groupid));
|
||||
$isuserevent = ($user && !empty($userid));
|
||||
$iconurl = '';
|
||||
$iconclass = '';
|
||||
|
||||
if ($isactivityevent) {
|
||||
$key = 'monologo';
|
||||
$component = $coursemodule->get('modname');
|
||||
|
||||
$iconurl = get_fast_modinfo($courseid)->get_cm($coursemodule->get('id'))->get_icon_url()->out(false);
|
||||
$iconurl = get_fast_modinfo($courseid)->get_cm($coursemodule->get('id'))->get_icon_url();
|
||||
$iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter';
|
||||
$iconurl = $iconurl->out(false);
|
||||
if (get_string_manager()->string_exists($event->get_type(), $component)) {
|
||||
$alttext = get_string($event->get_type(), $component);
|
||||
} else {
|
||||
@@ -121,6 +124,7 @@ class event_icon_exporter extends exporter {
|
||||
$data->component = $component;
|
||||
$data->alttext = $alttext;
|
||||
$data->iconurl = $iconurl;
|
||||
$data->iconclass = $iconclass;
|
||||
|
||||
parent::__construct($data, $related);
|
||||
}
|
||||
@@ -136,6 +140,7 @@ class event_icon_exporter extends exporter {
|
||||
'component' => ['type' => PARAM_TEXT],
|
||||
'alttext' => ['type' => PARAM_TEXT],
|
||||
'iconurl' => ['type' => PARAM_TEXT],
|
||||
'iconclass' => ['type' => PARAM_TEXT],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,9 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
|
||||
$whereclause = implode(' AND ', $whereconditions);
|
||||
|
||||
// Build SQL subquery and conditions for filtered events based on priorities.
|
||||
$subquerytimeconditions = array_filter($whereconditions, function($condition) {
|
||||
return (strpos($condition, 'time') !== false);
|
||||
});
|
||||
$subquerywhere = '';
|
||||
$subqueryconditions = [];
|
||||
$subqueryparams = [];
|
||||
@@ -194,7 +197,12 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
|
||||
// Set calendar filters.
|
||||
list($usercourses, $usergroups, $user) = calendar_set_filters($usercourses, true, $userrecord);
|
||||
|
||||
$allusercourses = array_merge($allusercourses, $usercourses);
|
||||
$filteredcourses = is_array($courses) ? $courses : [$courses];
|
||||
$filteredcourses = array_filter($usercourses, function($course) use ($filteredcourses) {
|
||||
return in_array($course, $filteredcourses);
|
||||
});
|
||||
|
||||
$allusercourses = array_merge($allusercourses, $filteredcourses);
|
||||
|
||||
// Flag to indicate whether the query needs to exclude group overrides.
|
||||
$viewgroupsonly = false;
|
||||
@@ -203,7 +211,8 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
|
||||
// Set filter condition for the user's events.
|
||||
// Even though $user is a single scalar, we still use get_in_or_equal() because we are inside a loop.
|
||||
list($inusers, $inuserparams) = $DB->get_in_or_equal($user, SQL_PARAMS_NAMED);
|
||||
$subqueryconditions[] = "(ev.userid $inusers AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)";
|
||||
$condition = "(ev.userid $inusers AND ev.courseid = 0 AND ev.groupid = 0 AND ev.categoryid = 0)";
|
||||
$subqueryconditions[] = $condition;
|
||||
$subqueryparams = array_merge($subqueryparams, $inuserparams);
|
||||
|
||||
foreach ($usercourses as $courseid) {
|
||||
@@ -220,11 +229,11 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
|
||||
// Set filter condition for the user's group events.
|
||||
if ($usergroups === true || $viewgroupsonly) {
|
||||
// Fetch group events, but not group overrides.
|
||||
$subqueryconditions[] = "(ev.groupid != 0 AND ev.eventtype = 'group')";
|
||||
$groupconditions = "(ev.groupid != 0 AND ev.eventtype = 'group')";
|
||||
} else if (!empty($usergroups)) {
|
||||
// Fetch group events and group overrides.
|
||||
list($inusergroups, $inusergroupparams) = $DB->get_in_or_equal($usergroups, SQL_PARAMS_NAMED);
|
||||
$subqueryconditions[] = "(ev.groupid $inusergroups)";
|
||||
$groupconditions = "(ev.groupid $inusergroups)";
|
||||
$subqueryparams = array_merge($subqueryparams, $inusergroupparams);
|
||||
}
|
||||
}
|
||||
@@ -263,8 +272,20 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
|
||||
// Set subquery filter condition for the courses.
|
||||
if (!empty($subquerycourses)) {
|
||||
list($incourses, $incoursesparams) = $DB->get_in_or_equal($subquerycourses, SQL_PARAMS_NAMED);
|
||||
$subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid $incourses AND ev.categoryid = 0)";
|
||||
$subqueryparams = array_merge($subqueryparams, $incoursesparams);
|
||||
if (isset($groupconditions)) {
|
||||
$groupconditions = $groupconditions." OR ";
|
||||
} else {
|
||||
$groupconditions = '';
|
||||
}
|
||||
$condition = "($groupconditions(ev.groupid = 0 AND ev.courseid $incourses AND ev.categoryid = 0))";
|
||||
$subtimesparams = [];
|
||||
if (!empty($subquerytimeconditions)) {
|
||||
$subtimes = $this->subquerytimeconditions("courses", $subquerytimeconditions, $whereparams);
|
||||
$condition .= $subtimes['where'];
|
||||
$subtimesparams = $subtimes['params'];
|
||||
}
|
||||
$subqueryconditions[] = $condition;
|
||||
$subqueryparams = array_merge($subqueryparams, $incoursesparams, $subtimesparams);
|
||||
}
|
||||
|
||||
// Set subquery filter condition for the categories.
|
||||
@@ -272,8 +293,15 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
|
||||
$subqueryconditions[] = "(ev.categoryid != 0 AND ev.eventtype = 'category')";
|
||||
} else if (!empty($categories)) {
|
||||
list($incategories, $incategoriesparams) = $DB->get_in_or_equal($categories, SQL_PARAMS_NAMED);
|
||||
$subqueryconditions[] = "(ev.groupid = 0 AND ev.courseid = 0 AND ev.categoryid $incategories)";
|
||||
$subqueryparams = array_merge($subqueryparams, $incategoriesparams);
|
||||
$condition = "(ev.groupid = 0 AND ev.courseid = 0 AND ev.categoryid $incategories)";
|
||||
$subtimesparams = [];
|
||||
if (!empty($subquerytimeconditions)) {
|
||||
$subtimes = $this->subquerytimeconditions("cats", $subquerytimeconditions, $whereparams);
|
||||
$condition .= $subtimes['where'];
|
||||
$subtimesparams = $subtimes['params'];
|
||||
}
|
||||
$subqueryconditions[] = $condition;
|
||||
$subqueryparams = array_merge($subqueryparams, $incategoriesparams, $subtimesparams);
|
||||
}
|
||||
|
||||
// Build the WHERE condition for the sub-query.
|
||||
@@ -322,4 +350,36 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter
|
||||
|
||||
return $events === false ? [] : $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query fragment and params, with time constraints applied
|
||||
*
|
||||
* @param string $prefix
|
||||
* @param array $conditions
|
||||
* @param array $params
|
||||
* @return array [<where>, <params>]
|
||||
*/
|
||||
protected function subquerytimeconditions(string $prefix, array $conditions, array $params): array {
|
||||
$outwhere = '';
|
||||
$outparams = [];
|
||||
// Most specific to least specific.
|
||||
$timeparams = ['timefromid', 'timefrom3', 'timefrom2', 'timefrom1', 'timefrom', 'timetoid', 'timeto2', 'timeto1', 'timeto'];
|
||||
$whereconditions = [];
|
||||
foreach ($conditions as $condition) {
|
||||
$where = $condition;
|
||||
// This query has been borrowed from the main WHERE clause, so the alias needs to be renamed to match the union.
|
||||
$where = str_replace('e.id', 'ev.id', $where);
|
||||
foreach ($timeparams as $timeparam) {
|
||||
if (isset($params[$timeparam])) {
|
||||
$where = str_replace(":{$timeparam}", ":{$prefix}{$timeparam}", $where);
|
||||
$outparams["{$prefix}{$timeparam}"] = $params[$timeparam];
|
||||
}
|
||||
}
|
||||
$whereconditions[] = $where;
|
||||
}
|
||||
if (count($whereconditions) > 0) {
|
||||
$outwhere = ' AND ' . implode(' AND ', $whereconditions);
|
||||
}
|
||||
return ['where' => $outwhere, 'params' => $outparams];
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -2580,7 +2580,7 @@ function calendar_format_event_location(calendar_event $event): string {
|
||||
function calendar_show_event_type($type, $user = null) {
|
||||
$default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP + CALENDAR_EVENT_USER;
|
||||
|
||||
if (get_user_preferences('calendar_persistflt', 0, $user) === 0) {
|
||||
if ((int)get_user_preferences('calendar_persistflt', 0, $user) === 0) {
|
||||
global $SESSION;
|
||||
if (!isset($SESSION->calendarshoweventtype)) {
|
||||
$SESSION->calendarshoweventtype = $default;
|
||||
@@ -2603,7 +2603,7 @@ function calendar_show_event_type($type, $user = null) {
|
||||
* @param stdClass|int $user moodle user object or id, null means current user
|
||||
*/
|
||||
function calendar_set_event_type_display($type, $display = null, $user = null) {
|
||||
$persist = get_user_preferences('calendar_persistflt', 0, $user);
|
||||
$persist = (int)get_user_preferences('calendar_persistflt', 0, $user);
|
||||
$default = CALENDAR_EVENT_SITE + CALENDAR_EVENT_COURSE + CALENDAR_EVENT_GROUP
|
||||
+ CALENDAR_EVENT_USER + CALENDAR_EVENT_COURSECAT;
|
||||
if ($persist === 0) {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<div class="activityiconcontainer small {{{purpose}}} courseicon mr-3">
|
||||
{{#icon}}
|
||||
{{#iconurl}}
|
||||
<img alt="{{alttext}}" title="{{alttext}}" src="{{{ iconurl }}}" class="icon ">
|
||||
<img alt="{{alttext}}" title="{{alttext}}" src="{{{ iconurl }}}" class="icon {{iconclass}}">
|
||||
{{/iconurl}}
|
||||
{{^iconurl}}
|
||||
{{#pix}} {{key}}, {{component}}, {{alttext}} {{/pix}}
|
||||
|
||||
@@ -8,11 +8,9 @@ Feature: Verify that the day and month names are displayed using UTF-8
|
||||
Given remote langimport tests are enabled
|
||||
|
||||
Scenario Outline: View the calendar for December 2012 with correct UTF-8
|
||||
Given I log in as "admin"
|
||||
And I navigate to "Language > Language packs" in site administration
|
||||
And I set the field "Available language packs" to "<lang>"
|
||||
And I press "Install selected language pack(s)"
|
||||
And I should see "Language pack '<lang>' was successfully installed"
|
||||
Given the following "language pack" exists:
|
||||
| language | <lang> |
|
||||
And I log in as "admin"
|
||||
And I follow "Preferences" in the user menu
|
||||
And I follow "Preferred language"
|
||||
And I set the field "Preferred language" to "<lang>"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
@core @core_calendar @javascript
|
||||
Feature: Perform calendar filter actions
|
||||
In order to test the calendar filters with preselected options
|
||||
As a user
|
||||
I need to filter calendar events by certain criteria
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "categories" exist:
|
||||
| name | idnumber | category |
|
||||
| Year | year | |
|
||||
| Department C1 | department-c1 | year |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format | category |
|
||||
| Course 1 | C1 | topics | department-c1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "groups" exist:
|
||||
| name | course | idnumber |
|
||||
| Group 1 | C1 | G1 |
|
||||
And the following "group members" exist:
|
||||
| user | group |
|
||||
| teacher1 | G1 |
|
||||
And the following "events" exist:
|
||||
| name | eventtype |
|
||||
| Site event | site |
|
||||
And the following "events" exist:
|
||||
| name | eventtype | course |
|
||||
| C1 event | course | C1 |
|
||||
And the following "events" exist:
|
||||
| name | eventtype | category |
|
||||
| Dep1a event | category | department-c1 |
|
||||
And the following "events" exist:
|
||||
| name | eventtype | group | course |
|
||||
| Group1 event | group | G1 | C1 |
|
||||
|
||||
Scenario: Teacher of a Course can see his events and filter them
|
||||
Given I log in as "teacher1"
|
||||
When I follow "Calendar" in the user menu
|
||||
Then I should see "C1 event"
|
||||
And I should see "Dep1a event"
|
||||
And I should see "Group1 event"
|
||||
And I click on "Hide category events" "link"
|
||||
And I should see "C1 event"
|
||||
And I should not see "Dep1a event"
|
||||
And I should see "Group1 event"
|
||||
And I click on "Hide course events" "link"
|
||||
And I should not see "C1 event"
|
||||
And I should not see "Dep1a event"
|
||||
And I should see "Group1 event"
|
||||
|
||||
Scenario: Teacher of a Course can see only non filtered events from user preferences calendar_savedflt
|
||||
Given I log in as "teacher1"
|
||||
And the following "user preferences" exist:
|
||||
| user | preference | value |
|
||||
| teacher1 | calendar_persistflt | 1 |
|
||||
| teacher1 | calendar_savedflt | 13 |
|
||||
When I follow "Calendar" in the user menu
|
||||
Then I should not see "C1 event"
|
||||
And I should see "Dep1a event"
|
||||
And I should not see "Group1 event"
|
||||
|
||||
Scenario: Teacher of a Course can see all events because session is used and not user preference calendar_savedflt
|
||||
Given I log in as "teacher1"
|
||||
And the following "user preferences" exist:
|
||||
| user | preference | value |
|
||||
| teacher1 | calendar_persistflt | 0 |
|
||||
| teacher1 | calendar_savedflt | 13 |
|
||||
When I follow "Calendar" in the user menu
|
||||
Then I should see "C1 event"
|
||||
And I should see "Dep1a event"
|
||||
And I should see "Group1 event"
|
||||
@@ -70,6 +70,10 @@ $CFG->dboptions = array(
|
||||
// can be removed for MySQL (by default it will
|
||||
// use 'utf8mb4_unicode_ci'. This option should
|
||||
// be removed for all other databases.
|
||||
// 'extrainfo' => [], // Extra information for the DB driver, e.g. SQL Server,
|
||||
// has additional configuration according to its environment,
|
||||
// which the administrator can specify to alter and
|
||||
// override any connection options.
|
||||
// 'fetchbuffersize' => 100000, // On PostgreSQL, this option sets a limit
|
||||
// on the number of rows that are fetched into
|
||||
// memory when doing a large recordset query
|
||||
|
||||
@@ -14,26 +14,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* core_contentbank specific renderers
|
||||
*
|
||||
* @package core_contentbank
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_contentbank\output;
|
||||
|
||||
use context_coursecat;
|
||||
use core_contentbank\content;
|
||||
use core_contentbank\contentbank;
|
||||
use renderable;
|
||||
use templatable;
|
||||
use renderer_base;
|
||||
use stdClass;
|
||||
use core_contentbank\content;
|
||||
|
||||
/**
|
||||
* Class containing data for bank content
|
||||
*
|
||||
* @package core_contentbank
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
@@ -138,7 +132,8 @@ class bankcontent implements renderable, templatable {
|
||||
}
|
||||
$options = [];
|
||||
foreach ($this->allowedcategories as $allowedcategory) {
|
||||
$options[$allowedcategory->ctxid] = $allowedcategory->name;
|
||||
$options[$allowedcategory->ctxid] = format_string($allowedcategory->name, true,
|
||||
context_coursecat::instance($allowedcategory->ctxinstance));
|
||||
}
|
||||
if (!empty($options)) {
|
||||
$allowedcontexts['categories'] = [get_string('coursecategories') => $options];
|
||||
@@ -154,14 +149,15 @@ class bankcontent implements renderable, templatable {
|
||||
$allowedcontexts['courses'] = [get_string('courses') => $options];
|
||||
}
|
||||
if (!empty($allowedcontexts)) {
|
||||
$url = new \moodle_url('/contentbank/index.php');
|
||||
$strchoosecontext = get_string('choosecontext', 'core_contentbank');
|
||||
$singleselect = new \single_select(
|
||||
$url,
|
||||
new \moodle_url('/contentbank/index.php'),
|
||||
'contextid',
|
||||
$allowedcontexts,
|
||||
$this->context->id,
|
||||
get_string('choosecontext', 'core_contentbank')
|
||||
$strchoosecontext
|
||||
);
|
||||
$singleselect->set_label($strchoosecontext, ['class' => 'sr-only']);
|
||||
$data->allowedcontexts = $singleselect->export_for_template($output);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Class containing data for a content view.
|
||||
*
|
||||
* @package core_contentbank
|
||||
* @copyright 2020 Victor Deniz <victor@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_contentbank\output;
|
||||
|
||||
use context;
|
||||
use core_contentbank\content;
|
||||
use core_contentbank\contenttype;
|
||||
use moodle_url;
|
||||
@@ -180,8 +174,13 @@ class viewcontent implements renderable, templatable {
|
||||
$data->editcontenturl = $editcontenturl->out(false);
|
||||
}
|
||||
|
||||
$closeurl = new moodle_url('/contentbank/index.php', ['contextid' => $this->content->get_contextid()]);
|
||||
$data->closeurl = $closeurl->out(false);
|
||||
// Close/exit link for those users who can access that context.
|
||||
$context = context::instance_by_id($this->content->get_contextid());
|
||||
if (has_capability('moodle/contentbank:access', $context)) {
|
||||
$closeurl = new moodle_url('/contentbank/index.php', ['contextid' => $context->id]);
|
||||
$data->closeurl = $closeurl->out(false);
|
||||
}
|
||||
|
||||
$data->actionmenu = $this->get_edit_actions_dropdown();
|
||||
$data->heading = $this->content->get_name();
|
||||
if ($this->content->get_visibility() == content::VISIBILITY_UNLISTED) {
|
||||
|
||||
@@ -54,31 +54,28 @@ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
}
|
||||
}}
|
||||
{{#heading}}<h2>{{heading}}</h2>{{/heading}}
|
||||
{{#usercanedit}}
|
||||
<div class="cb-toolbar-container container-fluid my-2">
|
||||
<div class="row">
|
||||
<div class="d-flex">
|
||||
{{#usercanedit}}
|
||||
<div class="d-flex mr-3">
|
||||
<a href="{{editcontenturl}}" class="btn btn-primary" data-action="edit-content">
|
||||
{{#str}}edit{{/str}}
|
||||
</a>
|
||||
</div>
|
||||
{{/usercanedit}}
|
||||
|
||||
{{#actionmenu}}
|
||||
<div class="ml-3">
|
||||
{{> core_contentbank/contentbankmenu }}
|
||||
</div>
|
||||
{{/actionmenu}}
|
||||
{{#actionmenu}}
|
||||
<div>
|
||||
{{> core_contentbank/contentbankmenu }}
|
||||
</div>
|
||||
{{/actionmenu}}
|
||||
|
||||
{{#closeurl}}
|
||||
<div class="ml-auto">
|
||||
<a href="{{closeurl}}" class="btn btn-secondary" data-action="close-content">
|
||||
{{#str}}exit, core_contentbank{{/str}}
|
||||
</a>
|
||||
</div>
|
||||
{{/closeurl}}
|
||||
</div>
|
||||
</div>
|
||||
{{/usercanedit}}
|
||||
{{^usercanedit}}
|
||||
{{#actionmenu}}
|
||||
{{> core_contentbank/contentbankmenu }}
|
||||
{{/actionmenu}}
|
||||
{{/usercanedit}}
|
||||
|
||||
@@ -34,21 +34,20 @@ Feature: Navigate to different contexts in the content bank
|
||||
And I add the "Navigation" block if not present
|
||||
And I expand "Site pages" node
|
||||
When I click on "Content bank" "link"
|
||||
And "contextid" "select" should exist
|
||||
And the "contextid" select box should contain "System"
|
||||
And the "contextid" select box should contain "Cat 1"
|
||||
And the "contextid" select box should contain "Cat 2"
|
||||
And the "contextid" select box should contain "C0"
|
||||
And the "contextid" select box should contain "C1"
|
||||
And the "contextid" select box should contain "C2"
|
||||
And the "Choose course or category" select box should contain "System"
|
||||
And the "Choose course or category" select box should contain "Cat 1"
|
||||
And the "Choose course or category" select box should contain "Cat 2"
|
||||
And the "Choose course or category" select box should contain "C0"
|
||||
And the "Choose course or category" select box should contain "C1"
|
||||
And the "Choose course or category" select box should contain "C2"
|
||||
And I should see "santjordi.h5p"
|
||||
And I should not see "santjordi_rose.h5p"
|
||||
And I should not see "Dragon.h5p"
|
||||
And I set the field "contextid" to "Cat 1"
|
||||
And I set the field "Choose course or category" to "Cat 1"
|
||||
Then I should not see "santjordi.h5p"
|
||||
And I should see "santjordi_rose.h5p"
|
||||
And I should not see "Dragon.h5p"
|
||||
And I set the field "contextid" to "C0"
|
||||
And I set the field "Choose course or category" to "C0"
|
||||
And I should not see "santjordi.h5p"
|
||||
And I should not see "santjordi_rose.h5p"
|
||||
And I should see "Dragon.h5p"
|
||||
@@ -69,18 +68,17 @@ Feature: Navigate to different contexts in the content bank
|
||||
And I add the "Navigation" block if not present
|
||||
And I expand "Site pages" node
|
||||
When I click on "Content bank" "link"
|
||||
And "contextid" "select" should exist
|
||||
And the "contextid" select box should contain "C0"
|
||||
And the "contextid" select box should contain "C1"
|
||||
And the "contextid" select box should not contain "System"
|
||||
And the "contextid" select box should not contain "Cat 1"
|
||||
And the "contextid" select box should not contain "Cat 2"
|
||||
And the "contextid" select box should not contain "C2"
|
||||
And the "Choose course or category" select box should contain "C0"
|
||||
And the "Choose course or category" select box should contain "C1"
|
||||
And the "Choose course or category" select box should not contain "System"
|
||||
And the "Choose course or category" select box should not contain "Cat 1"
|
||||
And the "Choose course or category" select box should not contain "Cat 2"
|
||||
And the "Choose course or category" select box should not contain "C2"
|
||||
And I should see "Dragon.h5p"
|
||||
And I should not see "princess.h5p"
|
||||
And I should not see "santjordi.h5p"
|
||||
And I should not see "santjordi_rose.h5p"
|
||||
And I set the field "contextid" to "C1"
|
||||
And I set the field "Choose course or category" to "C1"
|
||||
Then I should not see "Dragon.h5p"
|
||||
And I should see "princess.h5p"
|
||||
And I should not see "santjordi.h5p"
|
||||
@@ -91,14 +89,13 @@ Feature: Navigate to different contexts in the content bank
|
||||
And I am on the "C0" "Course" page logged in as "teacher"
|
||||
And I expand "Site pages" node
|
||||
When I click on "Content bank" "link"
|
||||
And "contextid" "select" should exist
|
||||
And the "contextid" select box should contain "C0"
|
||||
And the "contextid" select box should contain "C1"
|
||||
And the "contextid" select box should contain "Cat 1"
|
||||
And the "contextid" select box should not contain "System"
|
||||
And the "contextid" select box should not contain "Cat 2"
|
||||
And the "contextid" select box should not contain "C2"
|
||||
And the "Choose course or category" select box should contain "C0"
|
||||
And the "Choose course or category" select box should contain "C1"
|
||||
And the "Choose course or category" select box should contain "Cat 1"
|
||||
And the "Choose course or category" select box should not contain "System"
|
||||
And the "Choose course or category" select box should not contain "Cat 2"
|
||||
And the "Choose course or category" select box should not contain "C2"
|
||||
And I should see "Dragon.h5p"
|
||||
And I set the field "contextid" to "Cat 1"
|
||||
And I set the field "Choose course or category" to "Cat 1"
|
||||
And I should not see "Dragon.h5p"
|
||||
And I should see "santjordi_rose.h5p"
|
||||
|
||||
@@ -11,7 +11,7 @@ Feature: Store the content bank view preference
|
||||
And I log in as "admin"
|
||||
And I follow "Manage private files..."
|
||||
And I upload "h5p/tests/fixtures/filltheblanks.h5p" file to "Files" filemanager
|
||||
And I upload "h5p/tests/fixtures/greeting-card-887.h5p" file to "Files" filemanager
|
||||
And I upload "h5p/tests/fixtures/greeting-card.h5p" file to "Files" filemanager
|
||||
And I click on "Save changes" "button"
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
@@ -35,7 +35,7 @@ Feature: Store the content bank view preference
|
||||
And I click on "Upload" "link"
|
||||
And I click on "Choose a file..." "button"
|
||||
And I click on "Private files" "link" in the ".fp-repo-area" "css_element"
|
||||
And I click on "greeting-card-887.h5p" "link"
|
||||
And I click on "greeting-card.h5p" "link"
|
||||
And I set the field "Save as" to "greetingcard.h5p"
|
||||
And I click on "Select this file" "button"
|
||||
And I click on "Save changes" "button"
|
||||
|
||||
@@ -28,9 +28,12 @@ namespace core_contentbank;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use advanced_testcase;
|
||||
use context_block;
|
||||
use context_course;
|
||||
use context_coursecat;
|
||||
use context_module;
|
||||
use context_system;
|
||||
use context_user;
|
||||
use Exception;
|
||||
|
||||
global $CFG;
|
||||
@@ -364,7 +367,7 @@ class contentbank_test extends advanced_testcase {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
$systemcontext = \context_system::instance();
|
||||
$name = 'greeting-card-887.h5p';
|
||||
$name = 'greeting-card.h5p';
|
||||
|
||||
// Create a dummy H5P file.
|
||||
$dummyh5p = array(
|
||||
@@ -637,72 +640,35 @@ class contentbank_test extends advanced_testcase {
|
||||
/**
|
||||
* Test the behaviour of is_context_allowed().
|
||||
*
|
||||
* @dataProvider context_provider
|
||||
* @param \Closure $getcontext Get the context to check.
|
||||
* @param bool $expectedresult Expected result.
|
||||
*
|
||||
* @covers ::is_context_allowed
|
||||
*/
|
||||
public function test_is_context_allowed(\Closure $getcontext, bool $expectedresult): void {
|
||||
public function test_is_context_allowed(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$cb = new contentbank();
|
||||
$context = $getcontext();
|
||||
$this->assertEquals($expectedresult, $cb->is_context_allowed($context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_is_context_allowed().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function context_provider(): array {
|
||||
// System context.
|
||||
$this->assertTrue($cb->is_context_allowed(context_system::instance()));
|
||||
|
||||
return [
|
||||
'System context' => [
|
||||
function (): \context {
|
||||
return \context_system::instance();
|
||||
},
|
||||
true,
|
||||
],
|
||||
'User context' => [
|
||||
function (): \context {
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
return \context_user::instance($user->id);
|
||||
},
|
||||
false,
|
||||
],
|
||||
'Course category context' => [
|
||||
function (): \context {
|
||||
$coursecat = $this->getDataGenerator()->create_category();
|
||||
return \context_coursecat::instance($coursecat->id);
|
||||
},
|
||||
true,
|
||||
],
|
||||
'Course context' => [
|
||||
function (): \context {
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
return \context_course::instance($course->id);
|
||||
},
|
||||
true,
|
||||
],
|
||||
'Module context' => [
|
||||
function (): \context {
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$module = $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
|
||||
return \context_module::instance($module->cmid);
|
||||
},
|
||||
false,
|
||||
],
|
||||
'Block context' => [
|
||||
function (): \context {
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$block = $this->getDataGenerator()->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
|
||||
return \context_block::instance($block->id);
|
||||
},
|
||||
false,
|
||||
],
|
||||
];
|
||||
// User context.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$this->assertFalse($cb->is_context_allowed(context_user::instance($user->id)));
|
||||
|
||||
// Category context.
|
||||
$category = $this->getDataGenerator()->create_category();
|
||||
$this->assertTrue($cb->is_context_allowed(context_coursecat::instance($category->id)));
|
||||
|
||||
// Course context.
|
||||
$course = $this->getDataGenerator()->create_course(['category' => $category->id]);
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
$this->assertTrue($cb->is_context_allowed($coursecontext));
|
||||
|
||||
// Module context.
|
||||
$module = $this->getDataGenerator()->create_module('page', ['course' => $course->id]);
|
||||
$this->assertFalse($cb->is_context_allowed(context_module::instance($module->cmid)));
|
||||
|
||||
// Block context.
|
||||
$block = $this->getDataGenerator()->create_block('online_users', ['parentcontextid' => $coursecontext->id]);
|
||||
$this->assertFalse($cb->is_context_allowed(context_block::instance($block->id)));
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -60,7 +60,7 @@ define(
|
||||
// Meanwhile, we filter the migrated actions.
|
||||
const componentActions = [
|
||||
'moveSection', 'moveCm', 'addSection', 'deleteSection', 'sectionHide', 'sectionShow',
|
||||
'cmHide', 'cmShow', 'cmStealth',
|
||||
'cmHide', 'cmShow', 'cmStealth', 'cmMoveRight', 'cmMoveLeft',
|
||||
];
|
||||
|
||||
// The course reactive instance.
|
||||
@@ -82,6 +82,7 @@ define(
|
||||
TOGGLE: '.toggle-display,.dropdown-toggle',
|
||||
SECTIONLI: 'li.section',
|
||||
SECTIONACTIONMENU: '.section_action_menu',
|
||||
SECTIONITEM: '[data-for="section_title"]',
|
||||
ADDSECTIONS: '.changenumsections [data-add-sections]',
|
||||
SECTIONBADGES: '[data-region="sectionbadges"]',
|
||||
};
|
||||
@@ -981,7 +982,7 @@ define(
|
||||
|
||||
// The section and activity names are edited using inplace editable.
|
||||
// The "update" jQuery event must be captured in order to update the course state.
|
||||
$('body').on('updated', `${SELECTOR.SECTIONLI} [data-inplaceeditable]`, function(e) {
|
||||
$('body').on('updated', `${SELECTOR.SECTIONLI} ${SELECTOR.SECTIONITEM} [data-inplaceeditable]`, function(e) {
|
||||
if (e.ajaxreturn && e.ajaxreturn.itemid) {
|
||||
const state = courseeditor.state;
|
||||
const section = state.section.get(e.ajaxreturn.itemid);
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace core_course\local\repository;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_component;
|
||||
use core_course\local\entity\content_item;
|
||||
use core_course\local\entity\lang_string_title;
|
||||
use core_course\local\entity\string_title;
|
||||
@@ -195,12 +196,20 @@ class content_item_readonly_repository implements content_item_readonly_reposito
|
||||
$archetype = plugin_supports('mod', $mod->name, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
|
||||
$purpose = plugin_supports('mod', $mod->name, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER);
|
||||
|
||||
$icon = 'monologo';
|
||||
// Quick check for monologo icons.
|
||||
// Plugins that don't have monologo icons will be displayed as is and CSS filter will not be applied.
|
||||
$hasmonologoicons = core_component::has_monologo_icon('mod', $mod->name);
|
||||
$iconclass = '';
|
||||
if (!$hasmonologoicons) {
|
||||
$iconclass = 'nofilter';
|
||||
}
|
||||
$contentitem = new content_item(
|
||||
$mod->id,
|
||||
$mod->name,
|
||||
new lang_string_title("modulename", $mod->name),
|
||||
new \moodle_url('/course/mod.php', ['id' => $course->id, 'add' => $mod->name]),
|
||||
$OUTPUT->pix_icon('monologo', '', $mod->name, ['class' => 'icon activityicon']),
|
||||
$OUTPUT->pix_icon($icon, '', $mod->name, ['class' => "activityicon $iconclass"]),
|
||||
$help,
|
||||
$archetype,
|
||||
'mod_' . $mod->name,
|
||||
|
||||
@@ -81,7 +81,7 @@ class content_notification_task extends adhoc_task {
|
||||
// Get module names in the user's language.
|
||||
$modnames = get_module_types_names();
|
||||
$a = [
|
||||
'coursename' => get_course_display_name_for_list($course),
|
||||
'coursename' => format_string(get_course_display_name_for_list($course), true, ['context' => $modcontext]),
|
||||
'courselink' => (new \moodle_url('/course/view.php', ['id' => $course->id]))->out(false),
|
||||
'modulename' => format_string($cm->name, $modcontext->id),
|
||||
'moduletypename' => $modnames[$cm->modname],
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -8,6 +8,6 @@ define("core_courseformat/local/courseindex/cm",["exports","core_courseformat/lo
|
||||
* @class core_courseformat/local/courseindex/cm
|
||||
* @copyright 2021 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.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem"},this.id=this.element.dataset.id}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm}),window.location.href!=cm.url&&window.location.href!="".concat(course.baseurl,"#").concat(cm.anchor)||(this.reactive.dispatch("setPageItem","cm",this.id),this.element.scrollIntoView({block:"center"})),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),cm.uservisible||this.addEventListener(this.getElement(this.selectors.CM_NAME),"click",this._activityAnchor)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element);try{const{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}catch(error){throw error}}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor))return void setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id)}),50);const course=this.reactive.get("course"),section=this.reactive.get("section",cm.sectionid);if(!section)return;const url="".concat(course.baseurl,"§ion=").concat(section.number,"#").concat(cm.anchor);event.preventDefault(),window.location=url}}return _exports.default=Component,_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_dndcmitem=_interopRequireDefault(_dndcmitem),_templates=_interopRequireDefault(_templates),_prefetch=_interopRequireDefault(_prefetch),_config=_interopRequireDefault(_config);_prefetch.default.prefetchTemplate("core_courseformat/local/courseindex/cmcompletion");class Component extends _dndcmitem.default{create(){this.name="courseindex_cm",this.selectors={CM_NAME:"[data-for='cm_name']",CM_COMPLETION:"[data-for='cm_completion']"},this.classes={CMHIDDEN:"dimmed",LOCKED:"editinprogress",RESTRICTIONS:"restrictions",PAGEITEM:"pageitem",INDENTED:"indented"},this.id=this.element.dataset.id}static init(target,selectors){return new Component({element:document.getElementById(target),selectors:selectors})}stateReady(state){this.configDragDrop(this.id);const cm=state.cm.get(this.id),course=state.course;this._refreshCompletion({state:state,element:cm});const anchor=new URL(window.location.href).hash.replace("#","");(window.location.href==cm.url||window.location.href.includes(course.baseurl)&&anchor==cm.anchor)&&(this.reactive.dispatch("setPageItem","cm",this.id),this.element.scrollIntoView({block:"center"})),_config.default.contextid!=_config.default.courseContextId&&_config.default.contextInstanceId==this.id&&(this.reactive.dispatch("setPageItem","cm",this.id,!0),this.element.scrollIntoView({block:"center"})),cm.uservisible||this.addEventListener(this.getElement(this.selectors.CM_NAME),"click",this._activityAnchor)}getWatchers(){return[{watch:"cm[".concat(this.id,"]:deleted"),handler:this.remove},{watch:"cm[".concat(this.id,"]:updated"),handler:this._refreshCm},{watch:"cm[".concat(this.id,"].completionstate:updated"),handler:this._refreshCompletion},{watch:"course.pageItem:updated",handler:this._refreshPageItem}]}_refreshCm(_ref){var _element$dragging,_element$locked,_element$hascmrestric;let{element:element}=_ref;this.element.classList.toggle(this.classes.CMHIDDEN,!element.visible),this.getElement(this.selectors.CM_NAME).innerHTML=element.name,this.element.classList.toggle(this.classes.DRAGGING,null!==(_element$dragging=element.dragging)&&void 0!==_element$dragging&&_element$dragging),this.element.classList.toggle(this.classes.LOCKED,null!==(_element$locked=element.locked)&&void 0!==_element$locked&&_element$locked),this.element.classList.toggle(this.classes.RESTRICTIONS,null!==(_element$hascmrestric=element.hascmrestrictions)&&void 0!==_element$hascmrestric&&_element$hascmrestric),this.element.classList.toggle(this.classes.INDENTED,element.indent),this.locked=element.locked}_refreshPageItem(_ref2){let{element:element}=_ref2;if(!element.pageItem)return;const isPageId="cm"==element.pageItem.type&&element.pageItem.id==this.id;this.element.classList.toggle(this.classes.PAGEITEM,isPageId),isPageId&&!this.reactive.isEditing&&this.element.scrollIntoView({block:"nearest"})}async _refreshCompletion(_ref3){let{state:state,element:element}=_ref3;if(this.reactive.isEditing||!element.istrackeduser)return;const completionElement=this.getElement(this.selectors.CM_COMPLETION);if(completionElement.dataset.value==element.completionstate)return;const data=this.reactive.getExporter().cmCompletion(state,element);try{const{html:html,js:js}=await _templates.default.renderForPromise("core_courseformat/local/courseindex/cmcompletion",data);_templates.default.replaceNode(completionElement,html,js)}catch(error){throw error}}_activityAnchor(event){const cm=this.reactive.get("cm",this.id);if(document.getElementById(cm.anchor))return void setTimeout((()=>{this.reactive.dispatch("setPageItem","cm",cm.id)}),50);const course=this.reactive.get("course"),section=this.reactive.get("section",cm.sectionid);if(!section)return;const url="".concat(course.baseurl,"§ion=").concat(section.number,"#").concat(cm.anchor);event.preventDefault(),window.location=url}}return _exports.default=Component,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=cm.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -221,6 +221,7 @@ export default class Component extends BaseComponent {
|
||||
// State changes that require to reload some course modules.
|
||||
{watch: `cm.visible:updated`, handler: this._reloadCm},
|
||||
{watch: `cm.stealth:updated`, handler: this._reloadCm},
|
||||
{watch: `cm.indent:updated`, handler: this._reloadCm},
|
||||
// Update section number and title.
|
||||
{watch: `section.number:updated`, handler: this._refreshSectionNumber},
|
||||
// Collapse and expand sections.
|
||||
|
||||
@@ -50,6 +50,8 @@ const directMutations = {
|
||||
cmHide: 'cmHide',
|
||||
cmShow: 'cmShow',
|
||||
cmStealth: 'cmStealth',
|
||||
cmMoveRight: 'cmMoveRight',
|
||||
cmMoveLeft: 'cmMoveLeft',
|
||||
};
|
||||
|
||||
export default class extends BaseComponent {
|
||||
@@ -190,6 +192,8 @@ export default class extends BaseComponent {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const pendingModalReady = new Pending(`courseformat/actions:prepareMoveSectionModal`);
|
||||
|
||||
// The section edit menu to refocus on end.
|
||||
const editTools = this._getClosestActionMenuToogler(target);
|
||||
|
||||
@@ -242,6 +246,8 @@ export default class extends BaseComponent {
|
||||
this.reactive.dispatch('sectionMove', [sectionId], target.dataset.id);
|
||||
this._destroyModal(modal, editTools);
|
||||
});
|
||||
|
||||
pendingModalReady.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,6 +266,8 @@ export default class extends BaseComponent {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const pendingModalReady = new Pending(`courseformat/actions:prepareMoveCmModal`);
|
||||
|
||||
// The section edit menu to refocus on end.
|
||||
const editTools = this._getClosestActionMenuToogler(target);
|
||||
|
||||
@@ -335,6 +343,8 @@ export default class extends BaseComponent {
|
||||
this.reactive.dispatch('cmMove', [cmId], targetSectionId, targetCmId);
|
||||
this._destroyModal(modal, editTools);
|
||||
});
|
||||
|
||||
pendingModalReady.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -282,6 +282,24 @@ export default class {
|
||||
this._setElementsValue(stateManager, 'cm', cmIds, 'completionstate', newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cms to the right: indent = 1.
|
||||
* @param {StateManager} stateManager the current state manager
|
||||
* @param {array} cmIds the list of cm ids
|
||||
*/
|
||||
async cmMoveRight(stateManager, cmIds) {
|
||||
await this._cmBasicAction(stateManager, 'cm_moveright', cmIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move cms to the left: indent = 0.
|
||||
* @param {StateManager} stateManager the current state manager
|
||||
* @param {array} cmIds the list of cm ids
|
||||
*/
|
||||
async cmMoveLeft(stateManager, cmIds) {
|
||||
await this._cmBasicAction(stateManager, 'cm_moveleft', cmIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock or unlock course modules.
|
||||
*
|
||||
|
||||
@@ -52,6 +52,7 @@ export default class Component extends DndCmItem {
|
||||
LOCKED: 'editinprogress',
|
||||
RESTRICTIONS: 'restrictions',
|
||||
PAGEITEM: 'pageitem',
|
||||
INDENTED: 'indented',
|
||||
};
|
||||
// We need our id to watch specific events.
|
||||
this.id = this.element.dataset.id;
|
||||
@@ -85,8 +86,12 @@ export default class Component extends DndCmItem {
|
||||
state,
|
||||
element: cm,
|
||||
});
|
||||
const url = new URL(window.location.href);
|
||||
const anchor = url.hash.replace('#', '');
|
||||
// Check if the current url is the cm url.
|
||||
if (window.location.href == cm.url || window.location.href == `${course.baseurl}#${cm.anchor}`) {
|
||||
if (window.location.href == cm.url
|
||||
|| (window.location.href.includes(course.baseurl) && anchor == cm.anchor)
|
||||
) {
|
||||
this.reactive.dispatch('setPageItem', 'cm', this.id);
|
||||
this.element.scrollIntoView({block: "center"});
|
||||
}
|
||||
@@ -132,6 +137,7 @@ export default class Component extends DndCmItem {
|
||||
this.element.classList.toggle(this.classes.DRAGGING, element.dragging ?? false);
|
||||
this.element.classList.toggle(this.classes.LOCKED, element.locked ?? false);
|
||||
this.element.classList.toggle(this.classes.RESTRICTIONS, element.hascmrestrictions ?? false);
|
||||
this.element.classList.toggle(this.classes.INDENTED, element.indent);
|
||||
this.locked = element.locked;
|
||||
}
|
||||
|
||||
|
||||
@@ -456,14 +456,6 @@ abstract class base {
|
||||
/**
|
||||
* Returns true if this course format uses activity indentation.
|
||||
*
|
||||
* Indentation is not supported by core formats anymore and may be deprecated in the future.
|
||||
* This method will keep a default return "true" for legacy reasons but new formats should override
|
||||
* it with a return false to prevent future deprecations.
|
||||
*
|
||||
* A message in a bottle: if indentation is finally deprecated, both behat steps i_indent_right_activity
|
||||
* and i_indent_left_activity should be removed as well. Right now no core behat uses them but indentation
|
||||
* is not officially deprecated so they are still available for the contrib formats.
|
||||
*
|
||||
* @return bool if the course format uses indentation.
|
||||
*/
|
||||
public function uses_indentation(): bool {
|
||||
|
||||
@@ -102,9 +102,12 @@ class cmname implements named_templatable, renderable {
|
||||
return [];
|
||||
}
|
||||
|
||||
$iconurl = $mod->get_icon_url();
|
||||
$iconclass = $iconurl->get_param('filtericon') ? '' : 'nofilter';
|
||||
$data = [
|
||||
'url' => $mod->url,
|
||||
'icon' => $mod->get_icon_url(),
|
||||
'icon' => $iconurl,
|
||||
'iconclass' => $iconclass,
|
||||
'modname' => $mod->modname,
|
||||
'textclasses' => $displayoptions['textclasses'] ?? '',
|
||||
'purpose' => plugin_supports('mod', $mod->modname, FEATURE_MOD_PURPOSE, MOD_PURPOSE_OTHER),
|
||||
|
||||
@@ -106,6 +106,7 @@ class cmitem implements named_templatable, renderable {
|
||||
'extraclasses' => $mod->extraclasses,
|
||||
'cmformat' => $item->export_for_template($output),
|
||||
'hasinfo' => $hasinfo,
|
||||
'indent' => ($format->uses_indentation()) ? $mod->indent : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@ class cm implements renderable {
|
||||
'sectionnumber' => $section->section,
|
||||
'uservisible' => $cm->uservisible,
|
||||
'hascmrestrictions' => $this->get_has_restrictions(),
|
||||
'module' => $cm->modname,
|
||||
'plugin' => 'mod_' . $cm->modname,
|
||||
'indent' => ($format->uses_indentation()) ? $cm->indent : 0,
|
||||
];
|
||||
|
||||
// Check the user access type to this cm.
|
||||
|
||||
@@ -422,6 +422,79 @@ class stateactions {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move course cms to the right. Indent = 1.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param int[] $ids cm ids
|
||||
* @param int $targetsectionid not used
|
||||
* @param int $targetcmid not used
|
||||
*/
|
||||
public function cm_moveright(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
array $ids = [],
|
||||
?int $targetsectionid = null,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
$this->set_cm_indentation($updates, $course, $ids, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move course cms to the left. Indent = 0.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param int[] $ids cm ids
|
||||
* @param int $targetsectionid not used
|
||||
* @param int $targetcmid not used
|
||||
*/
|
||||
public function cm_moveleft(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
array $ids = [],
|
||||
?int $targetsectionid = null,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
$this->set_cm_indentation($updates, $course, $ids, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to define the cm indentation level.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param int[] $ids cm ids
|
||||
* @param int $indent new value for indentation
|
||||
*/
|
||||
protected function set_cm_indentation(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
array $ids,
|
||||
int $indent
|
||||
): void {
|
||||
global $DB;
|
||||
|
||||
$this->validate_cms($course, $ids, __FUNCTION__);
|
||||
|
||||
// Check capabilities on every activity context.
|
||||
foreach ($ids as $cmid) {
|
||||
$modcontext = context_module::instance($cmid);
|
||||
require_capability('moodle/course:manageactivities', $modcontext);
|
||||
}
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$cms = $this->get_cm_info($modinfo, $ids);
|
||||
list($insql, $inparams) = $DB->get_in_or_equal(array_keys($cms), SQL_PARAMS_NAMED);
|
||||
$DB->set_field_select('course_modules', 'indent', $indent, "id $insql", $inparams);
|
||||
rebuild_course_cache($course->id, false, true);
|
||||
foreach ($cms as $cm) {
|
||||
$modcontext = context_module::instance($cm->id);
|
||||
course_module_updated::create_from_cm($cm, $modcontext)->trigger();
|
||||
$updates->add_cm_put($cm->id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract several cm_info from the course_modinfo.
|
||||
*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user