diff --git a/admin/search.php b/admin/search.php
index 98ec900ed44..648822e60f1 100644
--- a/admin/search.php
+++ b/admin/search.php
@@ -46,6 +46,8 @@ if ($data = data_submitted() and confirm_sesskey() and isset($data->action) and
}
}
+$PAGE->has_secondary_navigation_setter(false);
+
// and finally, if we get here, then there are matching settings and we have to print a form
// to modify them
echo $OUTPUT->header($focus);
@@ -88,7 +90,10 @@ if ($hassiteconfig) {
if ($showsettingslinks) {
$node = $PAGE->settingsnav->find('root', navigation_node::TYPE_SITE_ADMIN);
if ($node) {
- echo $OUTPUT->render_from_template('core/settings_link_page', ['node' => $node]);
+ $moremenu = new \core\navigation\output\more_menu($PAGE->secondarynav, 'nav-tabs');
+ $secondarynavigation = $moremenu->export_for_template($OUTPUT);
+ echo $OUTPUT->render_from_template('core/settings_link_page',
+ ['node' => $node, 'secondarynavigation' => $secondarynavigation]);
}
}
diff --git a/admin/tool/behat/tests/behat/keyboard.feature b/admin/tool/behat/tests/behat/keyboard.feature
index ca9a74f02f3..ca2912ce791 100644
--- a/admin/tool/behat/tests/behat/keyboard.feature
+++ b/admin/tool/behat/tests/behat/keyboard.feature
@@ -34,7 +34,7 @@ Feature: Verify that keyboard steps work as expected
| username | email | firstname | lastname |
| saffronr | saffron.rutledge@example.com | Saffron | Rutledge |
And I log in as "saffronr"
- And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element"
+ And I click on "Saffron Rutledge" "button" in the ".usermenu" "css_element"
When I press the up key
Then the focused element is "Log out" "link"
diff --git a/admin/tool/customlang/tests/behat/customisation_create.feature b/admin/tool/customlang/tests/behat/customisation_create.feature
index 1d139473593..a473c0c58fb 100644
--- a/admin/tool/customlang/tests/behat/customisation_create.feature
+++ b/admin/tool/customlang/tests/behat/customisation_create.feature
@@ -14,20 +14,20 @@ Feature: Within a moodle instance, an administrator should be able to modify lan
And I press "Open language pack for editing"
And I press "Continue"
And I set the field "Show strings of these components" to "moodle.php"
- And I set the field "String identifier" to "administrationsite"
+ And I set the field "String identifier" to "moodledocslink"
And I press "Show strings"
- And I set the field "core/administrationsite" to "Custom string example"
+ And I set the field "core/moodledocslink" to "moodle documents"
@javascript
Scenario: Edit an string but don't save it to lang pack.
When I press "Apply changes and continue editing"
- Then I should see "Site administration" in the "page-header" "region"
- And I should not see "Custom string example" in the "page-header" "region"
+ Then I should see "Help and documentation" in the ".helplink" "css_element"
+ And I should not see "moodle documents" in the ".helplink" "css_element"
@javascript
Scenario: Customize an string as admin and save it to lang pack.
Given I press "Save changes to the language pack"
And I should see "There are 1 modified strings."
When I click on "Continue" "button"
- Then I should see "Custom string example" in the "page-header" "region"
- And I should not see "Site administration" in the "page-header" "region"
+ Then I should see "moodle documents" in the ".helplink" "css_element"
+ And I should not see "Help and documentation" in the ".helplink" "css_element"
diff --git a/admin/tool/customlang/tests/behat/import_files.feature b/admin/tool/customlang/tests/behat/import_files.feature
index 3d93dfec90f..68265a47d62 100644
--- a/admin/tool/customlang/tests/behat/import_files.feature
+++ b/admin/tool/customlang/tests/behat/import_files.feature
@@ -24,7 +24,7 @@ Feature: Within a moodle instance, an administrator should be able to import mod
And I should see "There are 1 modified strings."
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
- And I should see "An amazing import feature" in the "page-header" "region"
+ And I should see "An amazing import feature"
@javascript
Scenario: Try to import a PHP file from a non existent component
@@ -46,6 +46,6 @@ Feature: Within a moodle instance, an administrator should be able to import mod
And I should see "There are 3 modified strings."
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
- And I should see "Uploaded custom string" in the "page-header" "region"
- And I should see "Another Uploaded string" in the "page-header" "region"
- And I should see "An amazing import feature" in the "page-header" "region"
+ And I should see "Uploaded custom string"
+ And I should see "Another Uploaded string"
+ And I should see "An amazing import feature"
diff --git a/admin/tool/customlang/tests/behat/import_mode.feature b/admin/tool/customlang/tests/behat/import_mode.feature
index e977237a5e8..2b0186acdbb 100644
--- a/admin/tool/customlang/tests/behat/import_mode.feature
+++ b/admin/tool/customlang/tests/behat/import_mode.feature
@@ -21,7 +21,7 @@ Feature: Within a moodle instance, an administrator should be able to import lan
And I press "Save changes to the language pack"
And I should see "There are 1 modified strings."
And I click on "Continue" "button"
- And I should see "Custom string example" in the "page-header" "region"
+ And I should see "Custom string example"
@javascript
Scenario: Update only customized strings
@@ -36,12 +36,12 @@ Feature: Within a moodle instance, an administrator should be able to import lan
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 1 modified strings."
- And I should not see "Uploaded custom string" in the "page-header" "region"
+ And I should not see "Uploaded custom string"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
- And I should not see "Custom string example" in the "page-header" "region"
- And I should see "Uploaded custom string" in the "page-header" "region"
- And I should not see "Another Uploaded string" in the "page-header" "region"
+ And I should not see "Custom string example"
+ And I should see "Uploaded custom string"
+ And I should not see "Another Uploaded string"
@javascript
Scenario: Create only new strings
@@ -56,12 +56,12 @@ Feature: Within a moodle instance, an administrator should be able to import lan
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 1 modified strings."
- And I should not see "Uploaded custom string" in the "page-header" "region"
+ And I should not see "Uploaded custom string"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
- And I should see "Custom string example" in the "page-header" "region"
- And I should not see "Uploaded custom string" in the "page-header" "region"
- And I should see "Another Uploaded string" in the "page-header" "region"
+ And I should see "Custom string example"
+ And I should not see "Uploaded custom string"
+ And I should see "Another Uploaded string"
@javascript
Scenario: Import all strings
@@ -76,9 +76,9 @@ Feature: Within a moodle instance, an administrator should be able to import lan
And I should see "String core/nonexistentinvetedstring not found."
And I click on "Continue" "button"
And I should see "There are 2 modified strings."
- And I should not see "Uploaded custom string" in the "page-header" "region"
+ And I should not see "Uploaded custom string"
And I click on "Save strings to language pack" "button"
And I click on "Continue" "button"
- And I should not see "Custom string example" in the "page-header" "region"
- And I should see "Uploaded custom string" in the "page-header" "region"
- And I should see "Another Uploaded string" in the "page-header" "region"
+ And I should not see "Custom string example"
+ And I should see "Uploaded custom string"
+ And I should see "Another Uploaded string"
diff --git a/admin/tool/lp/tests/behat/plan_crud.feature b/admin/tool/lp/tests/behat/plan_crud.feature
index 9f3dc075302..761a569fc87 100644
--- a/admin/tool/lp/tests/behat/plan_crud.feature
+++ b/admin/tool/lp/tests/behat/plan_crud.feature
@@ -56,7 +56,7 @@ Feature: Manage plearning plan
And I set the field "Select cohorts to sync" to "cohort plan"
When I click on "Add cohorts" "button"
Then I should see "2 learning plans were created."
- And I follow "Learning plan templates"
+ And I navigate to "Competencies > Learning plan templates" in site administration
And I click on ".template-userplans" "css_element" in the "Science template cohort" "table_row"
And I should see "Student 1"
And I should see "Student 2"
diff --git a/admin/tool/lp/tests/behat/plan_workflow.feature b/admin/tool/lp/tests/behat/plan_workflow.feature
index 5410f2ab2e9..9d8f2eb39c1 100644
--- a/admin/tool/lp/tests/behat/plan_workflow.feature
+++ b/admin/tool/lp/tests/behat/plan_workflow.feature
@@ -149,7 +149,6 @@ Feature: Manage plan workflow
And I follow "User 1"
And I follow "Learning plans"
And I should see "List of learning plans"
- And I follow "Learning plans"
When I click on "Send back to draft" of edit menu in the "Test-Plan3" row
And I follow "Test-Plan4"
And I follow "Send back to draft"
@@ -168,7 +167,6 @@ Feature: Manage plan workflow
And I follow "User 1"
And I follow "Learning plans"
And I should see "List of learning plans"
- And I follow "Learning plans"
When I click on "Complete this learning plan" of edit menu in the "Test-Plan3" row
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
And I wait until the page is ready
@@ -190,7 +188,6 @@ Feature: Manage plan workflow
And I follow "User 1"
And I follow "Learning plans"
And I should see "List of learning plans"
- And I follow "Learning plans"
When I click on "Reopen this learning plan" of edit menu in the "Test-Plan3" row
And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue"
And I follow "Test-Plan4"
diff --git a/admin/tool/oauth2/tests/behat/basic_settings.feature b/admin/tool/oauth2/tests/behat/basic_settings.feature
index beb669eab94..d2c402c436c 100644
--- a/admin/tool/oauth2/tests/behat/basic_settings.feature
+++ b/admin/tool/oauth2/tests/behat/basic_settings.feature
@@ -24,11 +24,11 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "https://accounts.google.com/.well-known/openid-configuration" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "given_name" "table_row"
And I should see "middlename" in the "middle_name" "table_row"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
@@ -57,10 +57,10 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "givenName" "table_row"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
@@ -89,10 +89,10 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "firstname" in the "first_name" "table_row"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
@@ -126,10 +126,10 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Testing service" "table_row"
And I should see "authorization_endpoint"
And I should not see "discovery_endpoint"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Testing service" "table_row"
And I should see "username" in the "ocs-data-id" "table_row"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Testing service" "table_row"
And I set the following fields to these values:
| Name | Testing service modified |
@@ -159,11 +159,11 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Open Badges" "table_row"
And I should see "https://dc.imsglobal.org/.well-known/badgeconnect.json" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Open Badges" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Open Badges" "table_row"
And I set the following fields to these values:
| Name | IMS Global |
@@ -194,11 +194,11 @@ Feature: Basic OAuth2 functionality
And I click on "Configure endpoints" "link" in the "Google custom" "table_row"
And I should see "https://accounts.google.com/.well-known/openid-configuration" in the "discovery_endpoint" "table_row"
And I should see "authorization_endpoint"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Google custom" "table_row"
And I should see "firstname" in the "given_name" "table_row"
And I should see "middlename" in the "middle_name" "table_row"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Google custom" "table_row"
And I set the following fields to these values:
| Name | Google custom modified |
@@ -227,11 +227,11 @@ Feature: Basic OAuth2 functionality
And I should see "-" in the "Invalid custom service" "table_row"
And I click on "Configure endpoints" "link" in the "Invalid custom service" "table_row"
And I should not see "discovery_endpoint"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Invalid custom service" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Invalid custom service" "table_row"
And I set the following fields to these values:
| Name | Valid custom service |
@@ -268,11 +268,11 @@ Feature: Basic OAuth2 functionality
And I should see "-" in the "Empty custom service" "table_row"
And I click on "Configure endpoints" "link" in the "Empty custom service" "table_row"
And I should not see "discovery_endpoint"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Configure user field mappings" "link" in the "Empty custom service" "table_row"
And I should not see "given_name"
And I should not see "middle_name"
- And I follow "OAuth 2 services"
+ And I navigate to "Server > OAuth 2 services" in site administration
And I click on "Edit" "link" in the "Empty custom service" "table_row"
# Check it works as expected too without slash at the end of the service base URL.
And I set the following fields to these values:
diff --git a/analytics/tests/behat/manage_models.feature b/analytics/tests/behat/manage_models.feature
index d68ed78e11b..d6b56f56695 100644
--- a/analytics/tests/behat/manage_models.feature
+++ b/analytics/tests/behat/manage_models.feature
@@ -93,7 +93,7 @@ Feature: Manage analytics models
And I click on "View" "link"
And I should see "Log extra info"
And I click on "Close" "button"
- And I click on "Analytics models" "link"
+ And I navigate to "Analytics > Analytics models" in site administration
# Execute scheduled analysis
And I open the action menu in "Students at risk of not meeting the course completion conditions" "table_row"
And I choose "Execute scheduled analysis" in the open action menu
diff --git a/badges/criteria.php b/badges/criteria.php
index 0a9f290967b..58f9c0bdaf3 100644
--- a/badges/criteria.php
+++ b/badges/criteria.php
@@ -61,7 +61,7 @@ $PAGE->set_context($context);
$PAGE->set_url($currenturl);
$PAGE->set_heading($badge->name);
$PAGE->set_title($badge->name);
-$PAGE->navbar->add($badge->name);
+$PAGE->navbar->add($badge->name, $currenturl);
$output = $PAGE->get_renderer('core', 'badges');
$msg = optional_param('msg', '', PARAM_TEXT);
@@ -105,4 +105,4 @@ if ($badge->has_criteria()) {
echo $OUTPUT->box(get_string('addcriteriatext', 'badges'));
}
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
diff --git a/badges/tests/behat/add_badge.feature b/badges/tests/behat/add_badge.feature
index bc7695df440..82d5c3b1ff9 100644
--- a/badges/tests/behat/add_badge.feature
+++ b/badges/tests/behat/add_badge.feature
@@ -44,7 +44,7 @@ Feature: Add badges to the system
And I should see "Issuer details"
And I should see "Test Badge Site"
And I should see "testuser@example.com"
- And I follow "Manage badges"
+ And I navigate to "Badges > Manage badges" in site administration
And I should see "Number of badges available: 1"
And I should not see "There are no badges available."
@@ -61,7 +61,7 @@ Feature: Add badges to the system
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
And I press "Create badge"
And I wait until the page is ready
- And I follow "Manage badges"
+ And I navigate to "Badges > Manage badges" in site administration
And I should see "Number of badges available: 1"
And I press "Add a new badge"
And I set the following fields to these values:
@@ -157,7 +157,7 @@ Feature: Add badges to the system
And I should see "Related badges (0)"
And I should see "Alignments (0)"
And I should not see "Create badge"
- And I follow "Manage badges"
+ And I navigate to "Badges > Manage badges" in site administration
And I should see "Number of badges available: 1"
And I should not see "There are no badges available."
# See buttons from the "Site badges" page.
diff --git a/badges/tests/behat/award_badge.feature b/badges/tests/behat/award_badge.feature
index 7324e75c31f..b25de058730 100644
--- a/badges/tests/behat/award_badge.feature
+++ b/badges/tests/behat/award_badge.feature
@@ -21,7 +21,6 @@ Feature: Award badges
And I am on "Course 1" course homepage
# Create course badge 1.
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 1 |
| Description | Course badge 1 description |
@@ -38,7 +37,6 @@ Feature: Award badges
# Badge #2
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 2 |
| Description | Course badge 2 description |
@@ -57,6 +55,7 @@ Feature: Award badges
# Award course badge 1 to student 1.
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
When I press "Award badge"
+ And I follow "Manage badges"
And I follow "Course Badge 1"
And I follow "Recipients (1)"
Then I should see "Recipients (1)"
@@ -149,6 +148,7 @@ Feature: Award badges
And I press "Award badge"
And I set the field "potentialrecipients[]" to "student 1 (student1@example.com)"
And I press "Award badge"
+ And I navigate to "Badges > Manage badges" in site administration
When I follow "Site Badge"
Then I should see "Recipients (2)"
And I log out
@@ -174,7 +174,6 @@ Feature: Award badges
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -191,6 +190,7 @@ Feature: Award badges
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
When I press "Award badge"
+ And I follow "Manage badges"
And I follow "Course Badge"
Then I should see "Recipients (2)"
And I log out
@@ -225,7 +225,6 @@ Feature: Award badges
| id_completion | 1 |
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -279,7 +278,6 @@ Feature: Award badges
And I press "Save changes"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -328,7 +326,6 @@ Feature: Award badges
And I am on "Course 1" course homepage
# Create course badge 1.
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 1 |
| Description | Course badge description |
@@ -347,13 +344,13 @@ Feature: Award badges
# Award course badge 1 to student 1.
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
When I press "Award badge"
+ And I follow "Manage badges"
And I follow "Course Badge 1"
And I follow "Recipients (1)"
Then I should see "Recipients (1)"
# Add course badge 2.
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge 2 |
| Description | Course badge description |
@@ -372,6 +369,7 @@ Feature: Award badges
# Award course badge 2 to student 2.
And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)"
When I press "Award badge"
+ And I follow "Manage badges"
And I follow "Course Badge 2"
And I follow "Recipients (1)"
Then I should see "Recipients (1)"
@@ -409,7 +407,6 @@ Feature: Award badges
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -426,6 +423,7 @@ Feature: Award badges
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
When I press "Award badge"
+ And I follow "Manage badges"
And I follow "Course Badge"
Then I should see "Recipients (2)"
And I follow "Recipients (2)"
@@ -434,5 +432,6 @@ Feature: Award badges
And I press "Revoke badge"
And I set the field "existingrecipients[]" to "Student 1 (student1@example.com)"
When I press "Revoke badge"
+ And I follow "Manage badges"
And I follow "Course Badge"
Then I should see "Recipients (0)"
diff --git a/badges/tests/behat/award_badge_groups.feature b/badges/tests/behat/award_badge_groups.feature
index 76b29b689e3..2596b26dd4c 100644
--- a/badges/tests/behat/award_badge_groups.feature
+++ b/badges/tests/behat/award_badge_groups.feature
@@ -37,7 +37,6 @@ Feature: Award badges with separate groups
And I set the field "Group mode" to "Separate groups"
And I press "Save and display"
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -59,7 +58,6 @@ Feature: Award badges with separate groups
When I log in as "teacher2"
And I am on "Course 1" course homepage
And I navigate to "Badges > Manage badges" in current page administration
- And I follow "Manage badges"
And I follow "Course Badge"
And I press "Award badge"
And I set the field "role" to "Non-editing teacher"
@@ -71,6 +69,7 @@ Feature: Award badges with separate groups
# Non-editing teacher can award the badge
And I set the field "potentialrecipients[]" to "Student 2 (student2@example.com)"
And I press "Award badge"
+ And I follow "Manage badges"
And I follow "Course Badge"
And I should see "Recipients (1)"
And I log out
@@ -85,7 +84,6 @@ Feature: Award badges with separate groups
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Users > Groups" in current page administration
- And I follow "Groups"
And I set the field "groups" to "Class B (2)"
And I press "Add/remove users"
And I set the field "addselect" to "Teacher 2 (teacher2@example.com)"
@@ -94,7 +92,6 @@ Feature: Award badges with separate groups
When I log in as "teacher2"
And I am on "Course 1" course homepage
And I navigate to "Badges > Manage badges" in current page administration
- And I follow "Manage badges"
And I follow "Course Badge"
And I press "Award badge"
And I set the field "role" to "Non-editing teacher"
@@ -112,7 +109,6 @@ Feature: Award badges with separate groups
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Users > Groups" in current page administration
- And I follow "Groups"
And I set the field "groups" to "Class A (2)"
And I press "Add/remove users"
And I set the field "removeselect" to "Teacher 2 (teacher2@example.com)"
@@ -122,7 +118,6 @@ Feature: Award badges with separate groups
When I log in as "teacher2"
And I am on "Course 1" course homepage
And I navigate to "Badges > Manage badges" in current page administration
- And I follow "Manage badges"
And I follow "Course Badge"
And I press "Award badge"
# Teacher 2 shouldn't be able to go further
diff --git a/badges/tests/behat/criteria_activity.feature b/badges/tests/behat/criteria_activity.feature
index 42b16f33c1c..93911ff5715 100644
--- a/badges/tests/behat/criteria_activity.feature
+++ b/badges/tests/behat/criteria_activity.feature
@@ -36,7 +36,6 @@ Feature: Award badges based on activity completion
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
diff --git a/badges/tests/behat/criteria_cohort.feature b/badges/tests/behat/criteria_cohort.feature
index e0578debdf7..32d2625e0af 100644
--- a/badges/tests/behat/criteria_cohort.feature
+++ b/badges/tests/behat/criteria_cohort.feature
@@ -152,6 +152,7 @@ Feature: Award badges based on cohort
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Second User (second@example.com)"
And I press "Award badge"
+ And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (1)"
And I log out
@@ -203,6 +204,7 @@ Feature: Award badges based on cohort
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Second User (second@example.com)"
And I press "Award badge"
+ And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (2)"
And I log out
@@ -259,6 +261,7 @@ Feature: Award badges based on cohort
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Second User (second@example.com)"
And I press "Award badge"
+ And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (2)"
And I log out
@@ -317,6 +320,7 @@ Feature: Award badges based on cohort
And I press "Award badge"
And I set the field "potentialrecipients[]" to "Second User (second@example.com)"
And I press "Award badge"
+ And I navigate to "Badges > Manage badges" in site administration
And I follow "Site Badge"
Then I should see "Recipients (1)"
And I log out
diff --git a/badges/tests/behat/criteria_competency.feature b/badges/tests/behat/criteria_competency.feature
index 0945c09ae58..77bac3fe102 100644
--- a/badges/tests/behat/criteria_competency.feature
+++ b/badges/tests/behat/criteria_competency.feature
@@ -40,7 +40,6 @@ Feature: Award badges based on competency completion
# Add a badge to the course
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
diff --git a/badges/tests/behat/role_visibility.feature b/badges/tests/behat/role_visibility.feature
index 036ff5d060b..4d46e8f7f10 100644
--- a/badges/tests/behat/role_visibility.feature
+++ b/badges/tests/behat/role_visibility.feature
@@ -22,7 +22,6 @@ Feature: Test role visibility for the badge administration page
Given I log in as "manager1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
@@ -37,7 +36,6 @@ Feature: Test role visibility for the badge administration page
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Badges > Add a new badge" in current page administration
- And I follow "Add a new badge"
And I set the following fields to these values:
| Name | Course Badge |
| Description | Course badge description |
diff --git a/badges/tests/behat/view_badge.feature b/badges/tests/behat/view_badge.feature
index fa2e0c6cb3e..5bdc41f2fb3 100644
--- a/badges/tests/behat/view_badge.feature
+++ b/badges/tests/behat/view_badge.feature
@@ -35,6 +35,7 @@ Feature: Display badges
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
And I press "Award badge"
# Check badge details are displayed.
+ And I navigate to "Badges > Manage badges" in site administration
And I follow "Testing system badge"
And I follow "Recipients (1)"
When I click on "View issued badge" "link" in the "Student 1" "table_row"
@@ -59,6 +60,7 @@ Feature: Display badges
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
And I press "Award badge"
# Check badge details are displayed.
+ And I navigate to "Badges > Manage badges" in site administration
And I follow "Testing system badge"
And I follow "Recipients (1)"
When I click on "View issued badge" "link" in the "Student 1" "table_row"
@@ -107,6 +109,7 @@ Feature: Display badges
And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
And I press "Award badge"
# Check "Expires" date is displayed.
+ And I navigate to "Badges > Manage badges" in site administration
And I follow "Testing system badge"
And I follow "Recipients (1)"
And I click on "View issued badge" "link" in the "Student 1" "table_row"
@@ -130,6 +133,7 @@ Feature: Display badges
# Wait 1 second to guarantee the badge is expired.
And I wait "1" seconds
# Check "Expired" date is displayed.
+ And I navigate to "Badges > Manage badges" in site administration
And I follow "Testing system badge"
And I follow "Recipients (1)"
And I click on "View issued badge" "link" in the "Student 1" "table_row"
diff --git a/blocks/classes/local/views/secondary.php b/blocks/classes/local/views/secondary.php
new file mode 100644
index 00000000000..c23e857a5f7
--- /dev/null
+++ b/blocks/classes/local/views/secondary.php
@@ -0,0 +1,32 @@
+.
+
+namespace core_block\local\views;
+
+/**
+ * Class secondary
+ *
+ * @package core_block
+ * @category navigation
+ */
+class secondary extends \core\navigation\views\secondary {
+ /**
+ * Blocks don't require secondary navs as they can be accessed from multiple places and in different contexts.
+ */
+ public function initialise(): void {
+
+ }
+}
diff --git a/blocks/tests/behat/manage_blocks.feature b/blocks/tests/behat/manage_blocks.feature
index e3903a9ce64..90880a3586f 100644
--- a/blocks/tests/behat/manage_blocks.feature
+++ b/blocks/tests/behat/manage_blocks.feature
@@ -59,5 +59,4 @@ Feature: Block appearances
| Visible | No |
And I press "Save changes"
And I press "Turn editing off"
- And I follow "Test book name"
Then I should not see "Comments"
diff --git a/cohort/tests/behat/add_cohort.feature b/cohort/tests/behat/add_cohort.feature
index 0d28abfa8a8..7ba866d8588 100644
--- a/cohort/tests/behat/add_cohort.feature
+++ b/cohort/tests/behat/add_cohort.feature
@@ -33,6 +33,10 @@ Feature: Add cohorts of users
Then the following should exist in the "generaltable" table:
| Name | Cohort ID | Description |
| My new cohort | mynewcohort | My new cohort is cool |
+ And I should see "Test cohort name"
+ And I should see "333"
+ And I should see "Test cohort description"
+ And I should see "Created manually"
@javascript
Scenario: Add users to a cohort selecting them from the system users list
@@ -76,8 +80,7 @@ Feature: Add cohorts of users
@javascript
Scenario: Add users to a cohort using a bulk user action
- When I follow "Accounts"
- And I follow "Bulk user actions"
+ When I navigate to "Users > Accounts > Bulk user actions" in site administration
And I set the field "Available" to "Third User"
And I press "Add to selection"
And I set the field "Available" to "Forth User"
@@ -86,8 +89,7 @@ Feature: Add cohorts of users
And I press "Go"
And I set the field "Cohort" to "Test cohort name [333]"
And I press "Add to cohort"
- And I follow "Accounts"
- And I follow "Cohorts"
+ And I navigate to "Users > Accounts > Cohorts" in site administration
Then I should see "2" in the "#cohorts" "css_element"
And I click on "Assign" "link" in the "Test cohort name" "table_row"
And the "Current users" select box should contain "Third User (third@example.com)"
@@ -96,9 +98,9 @@ Feature: Add cohorts of users
@javascript
Scenario: Edit cohort name in-place
- When I follow "Cohorts"
+ When I navigate to "Users > Accounts > Cohorts" in site administration
And I set the field "Edit cohort name" to "Students cohort"
Then I should not see "Test cohort name"
And I should see "Students cohort"
- And I follow "Cohorts"
+ And I navigate to "Users > Accounts > Cohorts" in site administration
And I should see "Students cohort"
diff --git a/course/tests/behat/course_browsing.feature b/course/tests/behat/course_browsing.feature
index 4993d076e20..586859d2a3c 100644
--- a/course/tests/behat/course_browsing.feature
+++ b/course/tests/behat/course_browsing.feature
@@ -91,7 +91,7 @@ Feature: Restricting access to course lists
And I should see "Biology"
And I should not see "Humanities"
And I click on "Courses" "link" in the "Navigation" "block"
- And "category" "text" should not exist in the ".breadcrumb" "css_element"
+ # And "category" "text" should not exist in the ".breadcrumb" "css_element"
And I should see "Science category"
And I should see "English category"
And I should not see "Other category"
diff --git a/course/tests/behat/course_controls.feature b/course/tests/behat/course_controls.feature
index e51d1807df9..a571bf6ad3d 100644
--- a/course/tests/behat/course_controls.feature
+++ b/course/tests/behat/course_controls.feature
@@ -81,12 +81,12 @@ Feature: Course activity controls works as expected
Examples:
| courseformat | coursedisplay | targetpage | should_see_other_sections | should_see_other_sections_following_block_sections_links | belowpage |
- | topics | 0 | "Course 1" | should | should | "Topic 2" |
+ | topics | 0 | "C1" | should | should | "Topic 2" |
| topics | 1 | "Topic 1" | should not | should not | "Topic 2" |
- | topics | 1 | "Course 1" | should | should not | "Topic 2" |
- | weeks | 0 | "Course 1" | should | should | "8 January - 14 January" |
+ | topics | 1 | "C1" | should | should not | "Topic 2" |
+ | weeks | 0 | "C1" | should | should | "8 January - 14 January" |
| weeks | 1 | "1 January - 7 January" | should not | should not | "8 January - 14 January" |
- | weeks | 1 | "Course 1" | should | should not | "8 January - 14 January" |
+ | weeks | 1 | "C1" | should | should not | "8 January - 14 January" |
Scenario Outline: General activities course controls using topics and weeks formats, and paged mode and not paged mode works as expected
Given the following "users" exist:
@@ -146,9 +146,9 @@ Feature: Course activity controls works as expected
Examples:
| courseformat | coursedisplay | targetpage | should_see_other_sections | should_see_other_sections_following_block_sections_links | belowpage |
- | topics | 0 | "Course 1" | should | should | "Topic 2" |
+ | topics | 0 | "C1" | should | should | "Topic 2" |
| topics | 1 | "Topic 1" | should not | should not | "Topic 2" |
- | topics | 1 | "Course 1" | should | should not | "Topic 2" |
- | weeks | 0 | "Course 1" | should | should | "8 January - 14 January" |
+ | topics | 1 | "C1" | should | should not | "Topic 2" |
+ | weeks | 0 | "C1" | should | should | "8 January - 14 January" |
| weeks | 1 | "1 January - 7 January" | should not | should not | "8 January - 14 January" |
- | weeks | 1 | "Course 1" | should | should not | "8 January - 14 January" |
+ | weeks | 1 | "C1" | should | should not | "8 January - 14 January" |
diff --git a/course/tests/behat/edit_settings.feature b/course/tests/behat/edit_settings.feature
index 4d46d50d61f..59c801af74e 100644
--- a/course/tests/behat/edit_settings.feature
+++ b/course/tests/behat/edit_settings.feature
@@ -23,7 +23,6 @@ Feature: Edit course settings
| Course short name | Edited course shortname |
| Course summary | Edited course summary |
And I press "Save and display"
- And I follow "Edited course fullname"
Then I should not see "Course 1"
And I should not see "C1"
And I should see "Edited course fullname"
diff --git a/grade/grading/tests/behat/behat_grading.php b/grade/grading/tests/behat/behat_grading.php
index 89c6c097556..05a08340297 100644
--- a/grade/grading/tests/behat/behat_grading.php
+++ b/grade/grading/tests/behat/behat_grading.php
@@ -85,7 +85,7 @@ class behat_grading extends behat_base {
// Step to access the user grade page from the grading page.
$gradetext = get_string('gradeverb');
- $this->execute("behat_general::i_click_on_in_the", [$this->escape($activityname), 'link', 'page', 'region']);
+ $this->execute('behat_navigation::go_to_breadcrumb_location', $this->escape($activityname));
$this->execute('behat_navigation::i_navigate_to_in_current_page_administration',
get_string('viewgrading', 'mod_assign'));
diff --git a/lang/en/moodle.php b/lang/en/moodle.php
index bc1d1955d50..005905b13b8 100644
--- a/lang/en/moodle.php
+++ b/lang/en/moodle.php
@@ -1006,6 +1006,7 @@ $string['hits'] = 'Hits';
$string['hitsoncourse'] = 'Hits on {$a->coursename} by {$a->username}';
$string['hitsoncoursetoday'] = 'Today\'s hits on {$a->coursename} by {$a->username}';
$string['home'] = 'Home';
+$string['homeheader'] = 'Home menu';
$string['hour'] = 'hour';
$string['hours'] = 'hours';
$string['howtomakethemes'] = 'How to make new themes';
@@ -1140,6 +1141,7 @@ $string['langltr'] = 'Language direction left-to-right';
$string['langrtl'] = 'Language direction right-to-left';
$string['language'] = 'Language';
$string['languagegood'] = 'This language pack is up-to-date! :-)';
+$string['languageselector'] = 'Language selector';
$string['last'] = 'Last';
$string['lastaccess'] = 'Last access';
$string['lastcourseaccess'] = 'Last access to course';
@@ -1350,6 +1352,7 @@ $string['movefilestohere'] = 'Move files to here';
$string['movefull'] = 'Move {$a} to this location';
$string['movehere'] = 'Move to here';
$string['moveleft'] = 'Move left';
+$string['moremenu'] = 'More';
$string['moveright'] = 'Move right';
$string['movesection'] = 'Move section {$a}';
$string['moveselectedcategoriesto'] = 'Move selected categories to';
@@ -1735,6 +1738,7 @@ $string['rename'] = 'Rename';
$string['renamefileto'] = 'Rename {$a} to';
$string['report'] = 'Report';
$string['reports'] = 'Reports';
+$string['reporttype'] = 'Report type';
$string['repositories'] = 'Repositories';
$string['requestcourse'] = 'Request a course';
$string['requestedby'] = 'Requested by';
@@ -2214,6 +2218,7 @@ $string['userdetails'] = 'User details';
$string['userfiles'] = 'User files';
$string['userlist'] = 'User list';
$string['usermenu'] = 'User menu';
+$string['usermenugoback'] = 'Go back to user menu';
$string['username'] = 'Username';
$string['usernameemail'] = 'Username / email';
$string['usernameemailmatch'] = 'The username and email address do not relate to the same user';
diff --git a/lib/amd/build/menu_navigation.min.js b/lib/amd/build/menu_navigation.min.js
new file mode 100644
index 00000000000..a6cb660bcf6
--- /dev/null
+++ b/lib/amd/build/menu_navigation.min.js
@@ -0,0 +1,2 @@
+define ("core/menu_navigation",["exports","core/key_codes"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;var c={menuitem:"[role=\"menuitem\"]",menu:"[role=\"menu\"]"},d=null,e=function(a,b){if(null!==a){return a}else{return b}},f=function(a){var e=a.srcElement,f=a.currentTarget.firstElementChild,k=j(a.currentTarget);if(e.classList.contains("dropdown-item")){if(a.keyCode===b.arrowRight||a.keyCode===b.arrowLeft){a.preventDefault();if(null!==d){d.parentElement.click()}}if(a.keyCode===b.space||a.keyCode===b.enter){a.preventDefault();Array.prototype.forEach.call(e.closest(".dropdown-menu").children,function(a){a.querySelector(c.menuitem).classList.remove("active");a.setAttribute("aria-current","false")});if(!e.parentElement.classList.contains("dropdown")){e.click()}}}else{if(a.keyCode===b.arrowRight){a.preventDefault();g(e,f)}if(a.keyCode===b.arrowLeft){a.preventDefault();h(e,k)}if(a.keyCode===b.arrowUp||a.keyCode===b.arrowDown){d=e;a.preventDefault()}if(a.keyCode===b.home){a.preventDefault();e.setAttribute("aria-current","false");i(f)}if(a.keyCode===b.end){a.preventDefault();e.currentNode.setAttribute("aria-current","false");i(k)}if(a.keyCode===b.space||a.keyCode===b.enter){a.preventDefault();if(!e.parentElement.classList.contains("dropdown")){e.click()}}}};a.default=function(a){a.removeEventListener("keydown",f);a.addEventListener("keydown",f)};var g=function(a,b){var d=a.parentElement.nextElementSibling,f=e(d,b),g=f.querySelector(c.menuitem);a.setAttribute("aria-current","false");g.setAttribute("aria-current","true");g.focus()},h=function(a,b){var d=a.parentElement.previousElementSibling,f=e(d,b),g=f.querySelector(c.menuitem);a.setAttribute("aria-current","false");g.setAttribute("aria-current","true");g.focus()},i=function(a){a.querySelector(c.menuitem).focus();a.querySelector(c.menuitem).setAttribute("aria-current","true")},j=function(a){var b=a.lastElementChild;if(!b.classList.contains("d-none")){return a.lastElementChild}else{var c=Array.prototype.map.call(a.children,function(a){return a}).reverse(),d=c.filter(function(a){if(!a.classList.contains("d-none")){return a}});if(0!==d.length){return d[0]}else{return a.firstElementChild}}};return a.default});
+//# sourceMappingURL=menu_navigation.min.js.map
diff --git a/lib/amd/build/menu_navigation.min.js.map b/lib/amd/build/menu_navigation.min.js.map
new file mode 100644
index 00000000000..2fc7010a405
--- /dev/null
+++ b/lib/amd/build/menu_navigation.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/menu_navigation.js"],"names":["SELECTORS","openDropdownNode","clickErrorHandler","item","fallback","listenerEvents","e","src","srcElement","firstNode","currentTarget","firstElementChild","lastNode","findUsableLastNode","classList","contains","keyCode","arrowRight","arrowLeft","preventDefault","parentElement","click","space","enter","Array","prototype","forEach","call","closest","children","node","querySelector","menuitem","remove","setAttribute","setFocusNext","setFocusPrev","arrowUp","arrowDown","home","setFocusHomeEnd","end","currentNode","elementRoot","removeEventListener","addEventListener","nextListItem","nextElementSibling","nodeToSelect","menuItem","focus","previousElementSibling","lastElementChild","extractedNodes","map","reverse","nodesToUse","filter","length"],"mappings":"4JA0BMA,CAAAA,CAAS,CAAG,CACd,SAAY,qBADE,CAEd,KAAQ,iBAFM,C,CAKdC,CAAgB,CAAG,I,CASjBC,CAAiB,CAAG,SAACC,CAAD,CAAOC,CAAP,CAAoB,CAC1C,GAAa,IAAT,GAAAD,CAAJ,CAAmB,CACf,MAAOA,CAAAA,CACV,CAFD,IAEO,CACH,MAAOC,CAAAA,CACV,CACJ,C,CAOKC,CAAc,CAAG,SAAAC,CAAC,CAAI,IAClBC,CAAAA,CAAG,CAAGD,CAAC,CAACE,UADU,CAElBC,CAAS,CAAGH,CAAC,CAACI,aAAF,CAAgBC,iBAFV,CAGlBC,CAAQ,CAAGC,CAAkB,CAACP,CAAC,CAACI,aAAH,CAHX,CAOxB,GAAIH,CAAG,CAACO,SAAJ,CAAcC,QAAd,CAAuB,eAAvB,CAAJ,CAA6C,CACzC,GAAIT,CAAC,CAACU,OAAF,GAAcC,YAAd,EACAX,CAAC,CAACU,OAAF,GAAcE,WADlB,CAC6B,CACzBZ,CAAC,CAACa,cAAF,GACA,GAAyB,IAArB,GAAAlB,CAAJ,CAA+B,CAC3BA,CAAgB,CAACmB,aAAjB,CAA+BC,KAA/B,EACH,CACJ,CACD,GAAIf,CAAC,CAACU,OAAF,GAAcM,OAAd,EACAhB,CAAC,CAACU,OAAF,GAAcO,OADlB,CACyB,CACrBjB,CAAC,CAACa,cAAF,GAGAK,KAAK,CAACC,SAAN,CAAgBC,OAAhB,CAAwBC,IAAxB,CAA6BpB,CAAG,CAACqB,OAAJ,CAAY,gBAAZ,EAA8BC,QAA3D,CAAqE,SAAAC,CAAI,CAAI,CACzEA,CAAI,CAACC,aAAL,CAAmB/B,CAAS,CAACgC,QAA7B,EAAuClB,SAAvC,CAAiDmB,MAAjD,CAAwD,QAAxD,EACAH,CAAI,CAACI,YAAL,CAAkB,cAAlB,CAAkC,OAAlC,CACH,CAHD,EAKA,GAAI,CAAC3B,CAAG,CAACa,aAAJ,CAAkBN,SAAlB,CAA4BC,QAA5B,CAAqC,UAArC,CAAL,CAAuD,CACnDR,CAAG,CAACc,KAAJ,EACH,CACJ,CACJ,CAtBD,IAsBO,CACH,GAAIf,CAAC,CAACU,OAAF,GAAcC,YAAlB,CAA8B,CAC1BX,CAAC,CAACa,cAAF,GACAgB,CAAY,CAAC5B,CAAD,CAAME,CAAN,CACf,CACD,GAAIH,CAAC,CAACU,OAAF,GAAcE,WAAlB,CAA6B,CACzBZ,CAAC,CAACa,cAAF,GACAiB,CAAY,CAAC7B,CAAD,CAAMK,CAAN,CACf,CAED,GAAIN,CAAC,CAACU,OAAF,GAAcqB,SAAd,EACA/B,CAAC,CAACU,OAAF,GAAcsB,WADlB,CAC6B,CACzBrC,CAAgB,CAAGM,CAAnB,CACAD,CAAC,CAACa,cAAF,EACH,CACD,GAAIb,CAAC,CAACU,OAAF,GAAcuB,MAAlB,CAAwB,CACpBjC,CAAC,CAACa,cAAF,GACAZ,CAAG,CAAC2B,YAAJ,CAAiB,cAAjB,CAAiC,OAAjC,EACAM,CAAe,CAAC/B,CAAD,CAClB,CACD,GAAIH,CAAC,CAACU,OAAF,GAAcyB,KAAlB,CAAuB,CACnBnC,CAAC,CAACa,cAAF,GACAZ,CAAG,CAACmC,WAAJ,CAAgBR,YAAhB,CAA6B,cAA7B,CAA6C,OAA7C,EACAM,CAAe,CAAC5B,CAAD,CAClB,CACD,GAAIN,CAAC,CAACU,OAAF,GAAcM,OAAd,EACAhB,CAAC,CAACU,OAAF,GAAcO,OADlB,CACyB,CACrBjB,CAAC,CAACa,cAAF,GAEA,GAAI,CAACZ,CAAG,CAACa,aAAJ,CAAkBN,SAAlB,CAA4BC,QAA5B,CAAqC,UAArC,CAAL,CAAuD,CACnDR,CAAG,CAACc,KAAJ,EACH,CACJ,CACJ,CACJ,C,WAOc,SAAAsB,CAAW,CAAI,CAC1BA,CAAW,CAACC,mBAAZ,CAAgC,SAAhC,CAA2CvC,CAA3C,EACAsC,CAAW,CAACE,gBAAZ,CAA6B,SAA7B,CAAwCxC,CAAxC,CACH,C,IAQK8B,CAAAA,CAAY,CAAG,SAACO,CAAD,CAAcjC,CAAd,CAA4B,IACvCqC,CAAAA,CAAY,CAAGJ,CAAW,CAACtB,aAAZ,CAA0B2B,kBADF,CAEvCC,CAAY,CAAG9C,CAAiB,CAAC4C,CAAD,CAAerC,CAAf,CAFO,CAGvCwC,CAAQ,CAAGD,CAAY,CAACjB,aAAb,CAA2B/B,CAAS,CAACgC,QAArC,CAH4B,CAI7CU,CAAW,CAACR,YAAZ,CAAyB,cAAzB,CAAyC,OAAzC,EACAe,CAAQ,CAACf,YAAT,CAAsB,cAAtB,CAAsC,MAAtC,EACAe,CAAQ,CAACC,KAAT,EACH,C,CAQKd,CAAY,CAAG,SAACM,CAAD,CAAc9B,CAAd,CAA2B,IACtCkC,CAAAA,CAAY,CAAGJ,CAAW,CAACtB,aAAZ,CAA0B+B,sBADH,CAEtCH,CAAY,CAAG9C,CAAiB,CAAC4C,CAAD,CAAelC,CAAf,CAFM,CAGtCqC,CAAQ,CAAGD,CAAY,CAACjB,aAAb,CAA2B/B,CAAS,CAACgC,QAArC,CAH2B,CAI5CU,CAAW,CAACR,YAAZ,CAAyB,cAAzB,CAAyC,OAAzC,EACAe,CAAQ,CAACf,YAAT,CAAsB,cAAtB,CAAsC,MAAtC,EACAe,CAAQ,CAACC,KAAT,EACH,C,CAOKV,CAAe,CAAG,SAAAV,CAAI,CAAI,CAC5BA,CAAI,CAACC,aAAL,CAAmB/B,CAAS,CAACgC,QAA7B,EAAuCkB,KAAvC,GACApB,CAAI,CAACC,aAAL,CAAmB/B,CAAS,CAACgC,QAA7B,EAAuCE,YAAvC,CAAoD,cAApD,CAAoE,MAApE,CACH,C,CAQKrB,CAAkB,CAAG,SAAA8B,CAAW,CAAI,CACtC,GAAM/B,CAAAA,CAAQ,CAAG+B,CAAW,CAACS,gBAA7B,CAGA,GAAI,CAACxC,CAAQ,CAACE,SAAT,CAAmBC,QAAnB,CAA4B,QAA5B,CAAL,CAA4C,CACxC,MAAO4B,CAAAA,CAAW,CAACS,gBACtB,CAFD,IAEO,IAEGC,CAAAA,CAAc,CAAG7B,KAAK,CAACC,SAAN,CAAgB6B,GAAhB,CAAoB3B,IAApB,CAAyBgB,CAAW,CAACd,QAArC,CAA+C,SAAAC,CAAI,CAAI,CAC1E,MAAOA,CAAAA,CACV,CAFsB,EAEpByB,OAFoB,EAFpB,CAOGC,CAAU,CAAGH,CAAc,CAACI,MAAf,CAAuB,SAAA3B,CAAI,CAAI,CAC9C,GAAI,CAACA,CAAI,CAAChB,SAAL,CAAeC,QAAf,CAAwB,QAAxB,CAAL,CAAwC,CACpC,MAAOe,CAAAA,CACV,CACJ,CAJkB,CAPhB,CAcH,GAA0B,CAAtB,GAAA0B,CAAU,CAACE,MAAf,CAA6B,CACzB,MAAOF,CAAAA,CAAU,CAAC,CAAD,CACpB,CAFD,IAEO,CACH,MAAOb,CAAAA,CAAW,CAAChC,iBACtB,CACJ,CACJ,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Keyboard initialization for a given html node.\n *\n * @module core/keyboard_navigation\n * @copyright 2021 Moodle\n * @author Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {space, enter, arrowRight, arrowLeft, arrowDown, arrowUp, home, end} from 'core/key_codes';\n\nconst SELECTORS = {\n 'menuitem': '[role=\"menuitem\"]',\n 'menu': '[role=\"menu\"]'\n};\n\nlet openDropdownNode = null;\n\n/**\n * Small helper function to check if a given node is null or not.\n *\n * @param {HTMLElement|null} item The node that we want to compare.\n * @param {HTMLElement} fallback Either the first node or final node that can be focused on.\n * @return {HTMLElement}\n */\nconst clickErrorHandler = (item, fallback) => {\n if (item !== null) {\n return item;\n } else {\n return fallback;\n }\n};\n\n/**\n * Defined event handling so we can remove listeners on nodes on resize etc.\n *\n * @param {event} e The triggering element and key presses etc.\n */\nconst listenerEvents = e => {\n const src = e.srcElement;\n const firstNode = e.currentTarget.firstElementChild;\n const lastNode = findUsableLastNode(e.currentTarget);\n\n // Handling for dropdown escapes.\n // A bulk of the handling is already done by aria.js just add polish.\n if (src.classList.contains('dropdown-item')) {\n if (e.keyCode === arrowRight ||\n e.keyCode === arrowLeft) {\n e.preventDefault();\n if (openDropdownNode !== null) {\n openDropdownNode.parentElement.click();\n }\n }\n if (e.keyCode === space ||\n e.keyCode === enter) {\n e.preventDefault();\n\n // Remove active class from any other dropdown elements.\n Array.prototype.forEach.call(src.closest('.dropdown-menu').children, node => {\n node.querySelector(SELECTORS.menuitem).classList.remove('active');\n node.setAttribute('aria-current', 'false');\n });\n\n if (!src.parentElement.classList.contains('dropdown')) {\n src.click();\n }\n }\n } else {\n if (e.keyCode === arrowRight) {\n e.preventDefault();\n setFocusNext(src, firstNode);\n }\n if (e.keyCode === arrowLeft) {\n e.preventDefault();\n setFocusPrev(src, lastNode);\n }\n // Let aria.js handle the dropdowns.\n if (e.keyCode === arrowUp ||\n e.keyCode === arrowDown) {\n openDropdownNode = src;\n e.preventDefault();\n }\n if (e.keyCode === home) {\n e.preventDefault();\n src.setAttribute('aria-current', 'false');\n setFocusHomeEnd(firstNode);\n }\n if (e.keyCode === end) {\n e.preventDefault();\n src.currentNode.setAttribute('aria-current', 'false');\n setFocusHomeEnd(lastNode);\n }\n if (e.keyCode === space ||\n e.keyCode === enter) {\n e.preventDefault();\n // Aria.js handles dropdowns etc.\n if (!src.parentElement.classList.contains('dropdown')) {\n src.click();\n }\n }\n }\n};\n\n/**\n * The initial entry point that a given module can pass a HTMLElement.\n *\n * @param {HTMLElement} elementRoot The menu to add handlers upon.\n */\nexport default elementRoot => {\n elementRoot.removeEventListener('keydown', listenerEvents);\n elementRoot.addEventListener('keydown', listenerEvents);\n};\n\n/**\n * Handle the focusing to the next element in the dropdown.\n *\n * @param {HTMLElement|null} currentNode The node that we want to take action on.\n * @param {HTMLElement} firstNode The backup node to focus as a last resort.\n */\nconst setFocusNext = (currentNode, firstNode) => {\n const nextListItem = currentNode.parentElement.nextElementSibling;\n const nodeToSelect = clickErrorHandler(nextListItem, firstNode);\n const menuItem = nodeToSelect.querySelector(SELECTORS.menuitem);\n currentNode.setAttribute('aria-current', 'false');\n menuItem.setAttribute('aria-current', 'true');\n menuItem.focus();\n};\n\n/**\n * Handle the focusing to the previous element in the dropdown.\n *\n * @param {HTMLElement|null} currentNode The node that we want to take action on.\n * @param {HTMLElement} lastNode The backup node to focus as a last resort.\n */\nconst setFocusPrev = (currentNode, lastNode) => {\n const nextListItem = currentNode.parentElement.previousElementSibling;\n const nodeToSelect = clickErrorHandler(nextListItem, lastNode);\n const menuItem = nodeToSelect.querySelector(SELECTORS.menuitem);\n currentNode.setAttribute('aria-current', 'false');\n menuItem.setAttribute('aria-current', 'true');\n menuItem.focus();\n};\n\n/**\n * Focus on either the start or end of a nav list.\n *\n * @param {HTMLElement} node The element to focus on.\n */\nconst setFocusHomeEnd = node => {\n node.querySelector(SELECTORS.menuitem).focus();\n node.querySelector(SELECTORS.menuitem).setAttribute('aria-current', 'true');\n};\n\n/**\n * We need to look within the menu to find a last node we can add focus to.\n *\n * @param {HTMLElement} elementRoot Menu to find a final child node within.\n * @return {HTMLElement}\n */\nconst findUsableLastNode = elementRoot => {\n const lastNode = elementRoot.lastElementChild;\n\n // An example is the more menu existing but hidden on the page for the time being.\n if (!lastNode.classList.contains('d-none')) {\n return elementRoot.lastElementChild;\n } else {\n // Cast the HTMLCollection & reverse it.\n const extractedNodes = Array.prototype.map.call(elementRoot.children, node => {\n return node;\n }).reverse();\n\n // Get rid of any nodes we can not set focus on.\n const nodesToUse = extractedNodes.filter((node => {\n if (!node.classList.contains('d-none')) {\n return node;\n }\n }));\n\n // If we find no elements we can set focus on, fall back to the absolute first element.\n if (nodesToUse.length !== 0) {\n return nodesToUse[0];\n } else {\n return elementRoot.firstElementChild;\n }\n }\n};\n"],"file":"menu_navigation.min.js"}
\ No newline at end of file
diff --git a/lib/amd/build/moremenu.min.js b/lib/amd/build/moremenu.min.js
new file mode 100644
index 00000000000..feecc3dd193
--- /dev/null
+++ b/lib/amd/build/moremenu.min.js
@@ -0,0 +1,2 @@
+define ("core/moremenu",["exports","jquery","core/menu_navigation"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=d(b);c=d(c);function d(a){return a&&a.__esModule?a:{default:a}}var f={regions:{moredropdown:"[data-region=\"moredropdown\"]",morebutton:"[data-region=\"morebutton\"]"},classes:{dropdownitem:"dropdown-item",dropdownmoremenu:"dropdownmoremenu",dropdowntoggle:"dropdown-toggle",hidden:"d-none",active:"active",nav:"nav",navlink:"nav-link",observed:"observed"},attributes:{menu:"[role=\"menu\"]"}},g=function(a){var b=a.parentNode.offsetHeight+1,c=a.querySelector(f.regions.moredropdown),d=a.querySelector(f.regions.morebutton);if(a.offsetHeight>b){d.classList.remove(f.classes.hidden);var e=Array.from(a.children).reverse();e.forEach(function(c){if(!c.classList.contains(f.classes.dropdownmoremenu)){if(a.offsetHeight>b){var d=a.removeChild(c);h(a,d,!0)}}})}else{if("children"in c){var j=Array.from(c.children);j.forEach(function(d){if(a.offsetHeightb){g(a)}}a.parentNode.classList.add(f.classes.observed)},h=function(a,b){var c=2.\n\n/**\n * Moves wrapping navigation items into a more menu.\n *\n * @module core/moremenu\n * @copyright 2021 Moodle\n * @author Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport menu_navigation from \"core/menu_navigation\";\n/**\n * Moremenu selectors.\n */\nconst Selectors = {\n regions: {\n moredropdown: '[data-region=\"moredropdown\"]',\n morebutton: '[data-region=\"morebutton\"]'\n },\n classes: {\n dropdownitem: 'dropdown-item',\n dropdownmoremenu: 'dropdownmoremenu',\n dropdowntoggle: 'dropdown-toggle',\n hidden: 'd-none',\n active: 'active',\n nav: 'nav',\n navlink: 'nav-link',\n observed: 'observed',\n },\n attributes: {\n menu: '[role=\"menu\"]'\n }\n};\n\n/**\n * Auto Collapse navigation items that wrap into a dropdown menu.\n *\n * @param {HTMLElement} menu The navbar container.\n */\nconst autoCollapse = menu => {\n\n const maxHeight = menu.parentNode.offsetHeight + 1;\n\n const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);\n const moreButton = menu.querySelector(Selectors.regions.morebutton);\n\n // If the menu items wrap and the menu height is larger than the height of the\n // parent then start pushing navlinks into the moreDropdown.\n if (menu.offsetHeight > maxHeight) {\n\n moreButton.classList.remove(Selectors.classes.hidden);\n\n const menuNodes = Array.from(menu.children).reverse();\n menuNodes.forEach(item => {\n if (!item.classList.contains(Selectors.classes.dropdownmoremenu)) {\n // After moving the menu items into the moreDropdown check again\n // if the menu height is still larger then the height of the parent.\n if (menu.offsetHeight > maxHeight) {\n const lastNode = menu.removeChild(item);\n // Move this node into the more dropdown menu.\n moveIntoMoreDropdown(menu, lastNode, true);\n }\n }\n });\n } else {\n // If the menu height is smaller than the height of the parent, then try returning navlinks to the menu.\n\n if ('children' in moreDropdown) {\n const menuNodes = Array.from(moreDropdown.children);\n menuNodes.forEach(item => {\n // Don't move the node to the more menu if it is explicitly defined that\n // this node should be displayed in the more dropdown menu at all times.\n if (menu.offsetHeight < maxHeight && item.dataset.forceintomoremenu !== 'true') {\n const lastNode = moreDropdown.removeChild(item);\n // Move this node from the more dropdown menu into the main section of the menu.\n moveOutOfMoreDropdown(menu, lastNode);\n }\n });\n\n // If there are no more menuNodes in the dropdown we can hide the moreButton.\n if (menuNodes.length === 0) {\n moreButton.classList.add(Selectors.classes.hidden);\n }\n }\n\n if (menu.offsetHeight > maxHeight) {\n autoCollapse(menu);\n }\n }\n menu.parentNode.classList.add(Selectors.classes.observed);\n};\n\n/**\n * Move a node into the \"more\" dropdown menu.\n *\n * This method forces a given navigation node to be added and displayed within the \"more\" dropdown menu.\n *\n * @param {HTMLElement} menu The navbar moremenu.\n * @param {HTMLElement} navNode The navigation node.\n * @param {boolean} prepend Whether to prepend or append the node to the content in the more dropdown menu.\n */\nconst moveIntoMoreDropdown = (menu, navNode, prepend = false) => {\n const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);\n const dropdownToggle = menu.querySelector('.' + Selectors.classes.dropdowntoggle);\n\n const navLink = navNode.querySelector('.' + Selectors.classes.navlink);\n // If there are navLinks that contain an active link in the moreDropdown\n // make the dropdownToggle in the moreButton active.\n if (navLink.classList.contains(Selectors.classes.active)) {\n dropdownToggle.classList.add(Selectors.classes.active);\n }\n\n // Change the styling of the navLink to a dropdownitem and push it into\n // the moreDropdown.\n navLink.classList.remove(Selectors.classes.navlink);\n navLink.classList.add(Selectors.classes.dropdownitem);\n if (prepend) {\n moreDropdown.prepend(navNode);\n } else {\n moreDropdown.append(navNode);\n }\n};\n\n/**\n * Move a node out of the \"more\" dropdown menu.\n *\n * This method forces a given node from the \"more\" dropdown menu to be displayed in the main section of the menu.\n *\n * @param {HTMLElement} menu The navbar moremenu.\n * @param {HTMLElement} navNode The navigation node.\n */\nconst moveOutOfMoreDropdown = (menu, navNode) => {\n const moreButton = menu.querySelector(Selectors.regions.morebutton);\n const dropdownToggle = menu.querySelector('.' + Selectors.classes.dropdowntoggle);\n const navLink = navNode.querySelector('.' + Selectors.classes.dropdownitem);\n\n // Stop displaying the active state on the dropdownToggle if\n // the active navlink is removed.\n if (navLink.classList.contains(Selectors.classes.active)) {\n dropdownToggle.classList.remove(Selectors.classes.active);\n }\n navLink.classList.remove(Selectors.classes.dropdownitem);\n navLink.classList.add(Selectors.classes.navlink);\n menu.insertBefore(navNode, moreButton);\n};\n\n/**\n * Initialise the more menus.\n *\n * @param {HTMLElement} menu The navbar moremenu.\n */\nexport default menu => {\n menu.firstElementChild.querySelector('[role=\"menuitem\"]').setAttribute('tabindex', '0');\n // Pre-populate the \"more\" dropdown menu with navigation nodes which are set to be displayed in this menu\n // by default at all times.\n if ('children' in menu) {\n const moreButton = menu.querySelector(Selectors.regions.morebutton);\n const menuNodes = Array.from(menu.children);\n menuNodes.forEach((item) => {\n if (!item.classList.contains(Selectors.classes.dropdownmoremenu) &&\n item.dataset.forceintomoremenu === 'true') {\n // Append this node into the more dropdown menu.\n moveIntoMoreDropdown(menu, item, false);\n // After adding the node into the more dropdown menu, make sure that the more dropdown menu button\n // is displayed.\n if (moreButton.classList.contains(Selectors.classes.hidden)) {\n moreButton.classList.remove(Selectors.classes.hidden);\n }\n }\n });\n }\n // Populate the more dropdown menu with additional nodes if necessary, depending on the current screen size.\n autoCollapse(menu);\n menu_navigation(menu);\n\n // When the screen size changes make sure the menu still fits.\n window.addEventListener('resize', () => {\n autoCollapse(menu);\n menu_navigation(menu);\n });\n\n const toggledropdown = e => {\n const innerMenu = e.target.parentNode.querySelector(Selectors.attributes.menu);\n if (innerMenu) {\n innerMenu.classList.toggle('show');\n }\n e.stopPropagation();\n };\n\n // If there are dropdowns in the MoreMenu, add a new\n // event listener to show the contents on click and prevent the\n // moreMenu from closing.\n $('.' + Selectors.classes.dropdownmoremenu).on('show.bs.dropdown', function() {\n const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);\n moreDropdown.querySelectorAll('.dropdown').forEach((dropdown) => {\n dropdown.removeEventListener('click', toggledropdown, true);\n dropdown.addEventListener('click', toggledropdown, true);\n });\n });\n};\n"],"file":"moremenu.min.js"}
\ No newline at end of file
diff --git a/lib/amd/build/usermenu.min.js b/lib/amd/build/usermenu.min.js
new file mode 100644
index 00000000000..fb89aa66b81
--- /dev/null
+++ b/lib/amd/build/usermenu.min.js
@@ -0,0 +1,2 @@
+define ("core/usermenu",["exports","jquery","core/key_codes"],function(a,b,c){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.default=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var d={userMenu:".usermenu",userMenuCarousel:".usermenu #usermenu-carousel",userMenuCarouselItem:".usermenu #usermenu-carousel .carousel-item",userMenuCarouselItemActive:".usermenu #usermenu-carousel .carousel-item.active",userMenuCarouselNavigationLink:".usermenu #usermenu-carousel .carousel-navigation-link"},e=function(){var a=document.querySelector(d.userMenu);(0,b.default)(d.userMenu).on("shown.bs.dropdown",function(){var b=document.querySelector(d.userMenuCarouselItemActive);b.focus();a.querySelectorAll(d.userMenuCarouselItem).forEach(function(a){if(!a.classList.contains("active")){a.style.width=b.offsetWidth+"px";a.style.height=b.offsetHeight+"px"}})});a.addEventListener("click",function(a){if(a.target.matches(d.userMenuCarouselNavigationLink)){f(a)}});a.addEventListener("keydown",function(a){if((a.keyCode===c.space||a.keyCode===c.enter)&&a.target.matches(d.userMenuCarouselNavigationLink)){a.preventDefault();f(a)}});var f=function(c){c.stopPropagation();var e=c.target.dataset.carouselTargetId,f=a.querySelector("#"+e),g=Array.from(f.parentNode.children).indexOf(f);(0,b.default)(d.userMenuCarousel).carousel(g)};(0,b.default)(d.userMenu).on("hide.bs.dropdown",function(){(0,b.default)(d.userMenuCarousel).carousel(0)});(0,b.default)(d.userMenuCarousel).on("slid.bs.carousel",function(){var b=a.querySelector(d.userMenuCarouselItemActive);b.focus()})};a.default={init:function init(){e()}};return a.default});
+//# sourceMappingURL=usermenu.min.js.map
diff --git a/lib/amd/build/usermenu.min.js.map b/lib/amd/build/usermenu.min.js.map
new file mode 100644
index 00000000000..fda9bf8d7f3
--- /dev/null
+++ b/lib/amd/build/usermenu.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../src/usermenu.js"],"names":["selectors","userMenu","userMenuCarousel","userMenuCarouselItem","userMenuCarouselItemActive","userMenuCarouselNavigationLink","registerEventListeners","document","querySelector","on","activeCarouselItem","focus","querySelectorAll","forEach","element","classList","contains","style","width","offsetWidth","height","offsetHeight","addEventListener","e","target","matches","carouselManagement","keyCode","space","enter","preventDefault","stopPropagation","targetedCarouselItemId","dataset","carouselTargetId","targetedCarouselItem","index","Array","from","parentNode","children","indexOf","carousel","init"],"mappings":"6JAwBA,uD,GAMMA,CAAAA,CAAS,CAAG,CACdC,QAAQ,CAAE,WADI,CAEdC,gBAAgB,CAAE,8BAFJ,CAGdC,oBAAoB,CAAE,6CAHR,CAIdC,0BAA0B,CAAE,oDAJd,CAKdC,8BAA8B,CAAE,wDALlB,C,CAWZC,CAAsB,CAAG,UAAM,CACjC,GAAML,CAAAA,CAAQ,CAAGM,QAAQ,CAACC,aAAT,CAAuBR,CAAS,CAACC,QAAjC,CAAjB,CAGA,cAAED,CAAS,CAACC,QAAZ,EAAsBQ,EAAtB,CAAyB,mBAAzB,CAA8C,UAAM,CAChD,GAAMC,CAAAA,CAAkB,CAAGH,QAAQ,CAACC,aAAT,CAAuBR,CAAS,CAACI,0BAAjC,CAA3B,CAEAM,CAAkB,CAACC,KAAnB,GAEAV,CAAQ,CAACW,gBAAT,CAA0BZ,CAAS,CAACG,oBAApC,EAA0DU,OAA1D,CAAkE,SAAAC,CAAO,CAAI,CAKzE,GAAI,CAACA,CAAO,CAACC,SAAR,CAAkBC,QAAlB,CAA2B,QAA3B,CAAL,CAA2C,CACvCF,CAAO,CAACG,KAAR,CAAcC,KAAd,CAAsBR,CAAkB,CAACS,WAAnB,CAAiC,IAAvD,CACAL,CAAO,CAACG,KAAR,CAAcG,MAAd,CAAuBV,CAAkB,CAACW,YAAnB,CAAkC,IAC5D,CACJ,CATD,CAUH,CAfD,EAkBApB,CAAQ,CAACqB,gBAAT,CAA0B,OAA1B,CAAmC,SAACC,CAAD,CAAO,CAGtC,GAAIA,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBzB,CAAS,CAACK,8BAA3B,CAAJ,CAAgE,CAC5DqB,CAAkB,CAACH,CAAD,CACrB,CACJ,CAND,EAQAtB,CAAQ,CAACqB,gBAAT,CAA0B,SAA1B,CAAqC,SAAAC,CAAC,CAAI,CAEtC,GAAI,CAACA,CAAC,CAACI,OAAF,GAAcC,OAAd,EACDL,CAAC,CAACI,OAAF,GAAcE,OADd,GAEAN,CAAC,CAACC,MAAF,CAASC,OAAT,CAAiBzB,CAAS,CAACK,8BAA3B,CAFJ,CAEgE,CAC5DkB,CAAC,CAACO,cAAF,GACAJ,CAAkB,CAACH,CAAD,CACrB,CACJ,CARD,EAeA,GAAMG,CAAAA,CAAkB,CAAG,SAAAH,CAAC,CAAI,CAK5BA,CAAC,CAACQ,eAAF,GAL4B,GAOtBC,CAAAA,CAAsB,CAAGT,CAAC,CAACC,MAAF,CAASS,OAAT,CAAiBC,gBAPpB,CAQtBC,CAAoB,CAAGlC,CAAQ,CAACO,aAAT,CAAuB,IAAMwB,CAA7B,CARD,CAUtBI,CAAK,CAAGC,KAAK,CAACC,IAAN,CAAWH,CAAoB,CAACI,UAArB,CAAgCC,QAA3C,EAAqDC,OAArD,CAA6DN,CAA7D,CAVc,CAY5B,cAAEnC,CAAS,CAACE,gBAAZ,EAA8BwC,QAA9B,CAAuCN,CAAvC,CAEH,CAdD,CAiBA,cAAEpC,CAAS,CAACC,QAAZ,EAAsBQ,EAAtB,CAAyB,kBAAzB,CAA6C,UAAM,CAG/C,cAAET,CAAS,CAACE,gBAAZ,EAA8BwC,QAA9B,CAAuC,CAAvC,CACH,CAJD,EAOA,cAAE1C,CAAS,CAACE,gBAAZ,EAA8BO,EAA9B,CAAiC,kBAAjC,CAAqD,UAAM,CACvD,GAAMC,CAAAA,CAAkB,CAAGT,CAAQ,CAACO,aAAT,CAAuBR,CAAS,CAACI,0BAAjC,CAA3B,CAEAM,CAAkB,CAACC,KAAnB,EACH,CAJD,CAKH,C,WASc,CACXgC,IAAI,CALK,QAAPA,CAAAA,IAAO,EAAM,CACfrC,CAAsB,EACzB,CAEc,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Initializes and handles events in the user menu.\n *\n * @module core/usermenu\n * @copyright 2021 Moodle\n * @author Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {space, enter} from 'core/key_codes';\n\n/**\n * User menu constants.\n */\nconst selectors = {\n userMenu: '.usermenu',\n userMenuCarousel: '.usermenu #usermenu-carousel',\n userMenuCarouselItem: '.usermenu #usermenu-carousel .carousel-item',\n userMenuCarouselItemActive: '.usermenu #usermenu-carousel .carousel-item.active',\n userMenuCarouselNavigationLink: '.usermenu #usermenu-carousel .carousel-navigation-link',\n};\n\n/**\n * Register event listeners.\n */\nconst registerEventListeners = () => {\n const userMenu = document.querySelector(selectors.userMenu);\n\n // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).\n $(selectors.userMenu).on('shown.bs.dropdown', () => {\n const activeCarouselItem = document.querySelector(selectors.userMenuCarouselItemActive);\n // Set the focus on the active carousel item.\n activeCarouselItem.focus();\n\n userMenu.querySelectorAll(selectors.userMenuCarouselItem).forEach(element => {\n // Resize all non-active carousel items to match the height and width of the current active (main)\n // carousel item to avoid sizing inconsistencies. This has to be done once the dropdown menu is fully\n // displayed ('shown.bs.dropdown') as the offsetWidth and offsetHeight cannot be obtained when the\n // element is hidden.\n if (!element.classList.contains('active')) {\n element.style.width = activeCarouselItem.offsetWidth + 'px';\n element.style.height = activeCarouselItem.offsetHeight + 'px';\n }\n });\n });\n\n // Handle click events in the user menu.\n userMenu.addEventListener('click', (e) => {\n\n // Handle click event on the carousel navigation (control) links in the user menu.\n if (e.target.matches(selectors.userMenuCarouselNavigationLink)) {\n carouselManagement(e);\n }\n });\n\n userMenu.addEventListener('keydown', e => {\n // Handle keydown event on the carousel navigation (control) links in the user menu.\n if ((e.keyCode === space ||\n e.keyCode === enter) &&\n e.target.matches(selectors.userMenuCarouselNavigationLink)) {\n e.preventDefault();\n carouselManagement(e);\n }\n });\n\n /**\n * We do the same actions here even if the caller was a click or button press.\n *\n * @param {Event} e The triggering element and key presses etc.\n */\n const carouselManagement = e => {\n // By default the user menu dropdown element closes on a click event. This behaviour is not desirable\n // as we need to be able to navigate through the carousel items (submenus of the user menu) within the\n // user menu. Therefore, we need to prevent the propagation of this event and then manually call the\n // carousel transition.\n e.stopPropagation();\n // The id of the targeted carousel item.\n const targetedCarouselItemId = e.target.dataset.carouselTargetId;\n const targetedCarouselItem = userMenu.querySelector('#' + targetedCarouselItemId);\n // Get the position (index) of the targeted carousel item within the parent container element.\n const index = Array.from(targetedCarouselItem.parentNode.children).indexOf(targetedCarouselItem);\n // Navigate to the targeted carousel item.\n $(selectors.userMenuCarousel).carousel(index);\n\n };\n\n // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).\n $(selectors.userMenu).on('hide.bs.dropdown', () => {\n // Reset the state once the user menu dropdown is closed and return back to the first (main) carousel item\n // if necessary.\n $(selectors.userMenuCarousel).carousel(0);\n });\n\n // Handle the 'slid.bs.carousel' event (Fired when the carousel has completed its slide transition).\n $(selectors.userMenuCarousel).on('slid.bs.carousel', () => {\n const activeCarouselItem = userMenu.querySelector(selectors.userMenuCarouselItemActive);\n // Set the focus on the newly activated carousel item.\n activeCarouselItem.focus();\n });\n};\n\n/**\n * Initialize the user menu.\n */\nconst init = () => {\n registerEventListeners();\n};\n\nexport default {\n init: init,\n};\n"],"file":"usermenu.min.js"}
\ No newline at end of file
diff --git a/lib/amd/src/menu_navigation.js b/lib/amd/src/menu_navigation.js
new file mode 100644
index 00000000000..a6f49f5d84e
--- /dev/null
+++ b/lib/amd/src/menu_navigation.js
@@ -0,0 +1,201 @@
+// 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 .
+
+/**
+ * Keyboard initialization for a given html node.
+ *
+ * @module core/keyboard_navigation
+ * @copyright 2021 Moodle
+ * @author Mathew May
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {space, enter, arrowRight, arrowLeft, arrowDown, arrowUp, home, end} from 'core/key_codes';
+
+const SELECTORS = {
+ 'menuitem': '[role="menuitem"]',
+ 'menu': '[role="menu"]'
+};
+
+let openDropdownNode = null;
+
+/**
+ * Small helper function to check if a given node is null or not.
+ *
+ * @param {HTMLElement|null} item The node that we want to compare.
+ * @param {HTMLElement} fallback Either the first node or final node that can be focused on.
+ * @return {HTMLElement}
+ */
+const clickErrorHandler = (item, fallback) => {
+ if (item !== null) {
+ return item;
+ } else {
+ return fallback;
+ }
+};
+
+/**
+ * Defined event handling so we can remove listeners on nodes on resize etc.
+ *
+ * @param {event} e The triggering element and key presses etc.
+ */
+const listenerEvents = e => {
+ const src = e.srcElement;
+ const firstNode = e.currentTarget.firstElementChild;
+ const lastNode = findUsableLastNode(e.currentTarget);
+
+ // Handling for dropdown escapes.
+ // A bulk of the handling is already done by aria.js just add polish.
+ if (src.classList.contains('dropdown-item')) {
+ if (e.keyCode === arrowRight ||
+ e.keyCode === arrowLeft) {
+ e.preventDefault();
+ if (openDropdownNode !== null) {
+ openDropdownNode.parentElement.click();
+ }
+ }
+ if (e.keyCode === space ||
+ e.keyCode === enter) {
+ e.preventDefault();
+
+ // Remove active class from any other dropdown elements.
+ Array.prototype.forEach.call(src.closest('.dropdown-menu').children, node => {
+ node.querySelector(SELECTORS.menuitem).classList.remove('active');
+ node.setAttribute('aria-current', 'false');
+ });
+
+ if (!src.parentElement.classList.contains('dropdown')) {
+ src.click();
+ }
+ }
+ } else {
+ if (e.keyCode === arrowRight) {
+ e.preventDefault();
+ setFocusNext(src, firstNode);
+ }
+ if (e.keyCode === arrowLeft) {
+ e.preventDefault();
+ setFocusPrev(src, lastNode);
+ }
+ // Let aria.js handle the dropdowns.
+ if (e.keyCode === arrowUp ||
+ e.keyCode === arrowDown) {
+ openDropdownNode = src;
+ e.preventDefault();
+ }
+ if (e.keyCode === home) {
+ e.preventDefault();
+ src.setAttribute('aria-current', 'false');
+ setFocusHomeEnd(firstNode);
+ }
+ if (e.keyCode === end) {
+ e.preventDefault();
+ src.currentNode.setAttribute('aria-current', 'false');
+ setFocusHomeEnd(lastNode);
+ }
+ if (e.keyCode === space ||
+ e.keyCode === enter) {
+ e.preventDefault();
+ // Aria.js handles dropdowns etc.
+ if (!src.parentElement.classList.contains('dropdown')) {
+ src.click();
+ }
+ }
+ }
+};
+
+/**
+ * The initial entry point that a given module can pass a HTMLElement.
+ *
+ * @param {HTMLElement} elementRoot The menu to add handlers upon.
+ */
+export default elementRoot => {
+ elementRoot.removeEventListener('keydown', listenerEvents);
+ elementRoot.addEventListener('keydown', listenerEvents);
+};
+
+/**
+ * Handle the focusing to the next element in the dropdown.
+ *
+ * @param {HTMLElement|null} currentNode The node that we want to take action on.
+ * @param {HTMLElement} firstNode The backup node to focus as a last resort.
+ */
+const setFocusNext = (currentNode, firstNode) => {
+ const nextListItem = currentNode.parentElement.nextElementSibling;
+ const nodeToSelect = clickErrorHandler(nextListItem, firstNode);
+ const menuItem = nodeToSelect.querySelector(SELECTORS.menuitem);
+ currentNode.setAttribute('aria-current', 'false');
+ menuItem.setAttribute('aria-current', 'true');
+ menuItem.focus();
+};
+
+/**
+ * Handle the focusing to the previous element in the dropdown.
+ *
+ * @param {HTMLElement|null} currentNode The node that we want to take action on.
+ * @param {HTMLElement} lastNode The backup node to focus as a last resort.
+ */
+const setFocusPrev = (currentNode, lastNode) => {
+ const nextListItem = currentNode.parentElement.previousElementSibling;
+ const nodeToSelect = clickErrorHandler(nextListItem, lastNode);
+ const menuItem = nodeToSelect.querySelector(SELECTORS.menuitem);
+ currentNode.setAttribute('aria-current', 'false');
+ menuItem.setAttribute('aria-current', 'true');
+ menuItem.focus();
+};
+
+/**
+ * Focus on either the start or end of a nav list.
+ *
+ * @param {HTMLElement} node The element to focus on.
+ */
+const setFocusHomeEnd = node => {
+ node.querySelector(SELECTORS.menuitem).focus();
+ node.querySelector(SELECTORS.menuitem).setAttribute('aria-current', 'true');
+};
+
+/**
+ * We need to look within the menu to find a last node we can add focus to.
+ *
+ * @param {HTMLElement} elementRoot Menu to find a final child node within.
+ * @return {HTMLElement}
+ */
+const findUsableLastNode = elementRoot => {
+ const lastNode = elementRoot.lastElementChild;
+
+ // An example is the more menu existing but hidden on the page for the time being.
+ if (!lastNode.classList.contains('d-none')) {
+ return elementRoot.lastElementChild;
+ } else {
+ // Cast the HTMLCollection & reverse it.
+ const extractedNodes = Array.prototype.map.call(elementRoot.children, node => {
+ return node;
+ }).reverse();
+
+ // Get rid of any nodes we can not set focus on.
+ const nodesToUse = extractedNodes.filter((node => {
+ if (!node.classList.contains('d-none')) {
+ return node;
+ }
+ }));
+
+ // If we find no elements we can set focus on, fall back to the absolute first element.
+ if (nodesToUse.length !== 0) {
+ return nodesToUse[0];
+ } else {
+ return elementRoot.firstElementChild;
+ }
+ }
+};
diff --git a/lib/amd/src/moremenu.js b/lib/amd/src/moremenu.js
new file mode 100644
index 00000000000..425a01fe826
--- /dev/null
+++ b/lib/amd/src/moremenu.js
@@ -0,0 +1,215 @@
+// 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 .
+
+/**
+ * Moves wrapping navigation items into a more menu.
+ *
+ * @module core/moremenu
+ * @copyright 2021 Moodle
+ * @author Bas Brands
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+import menu_navigation from "core/menu_navigation";
+/**
+ * Moremenu selectors.
+ */
+const Selectors = {
+ regions: {
+ moredropdown: '[data-region="moredropdown"]',
+ morebutton: '[data-region="morebutton"]'
+ },
+ classes: {
+ dropdownitem: 'dropdown-item',
+ dropdownmoremenu: 'dropdownmoremenu',
+ dropdowntoggle: 'dropdown-toggle',
+ hidden: 'd-none',
+ active: 'active',
+ nav: 'nav',
+ navlink: 'nav-link',
+ observed: 'observed',
+ },
+ attributes: {
+ menu: '[role="menu"]'
+ }
+};
+
+/**
+ * Auto Collapse navigation items that wrap into a dropdown menu.
+ *
+ * @param {HTMLElement} menu The navbar container.
+ */
+const autoCollapse = menu => {
+
+ const maxHeight = menu.parentNode.offsetHeight + 1;
+
+ const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
+ const moreButton = menu.querySelector(Selectors.regions.morebutton);
+
+ // If the menu items wrap and the menu height is larger than the height of the
+ // parent then start pushing navlinks into the moreDropdown.
+ if (menu.offsetHeight > maxHeight) {
+
+ moreButton.classList.remove(Selectors.classes.hidden);
+
+ const menuNodes = Array.from(menu.children).reverse();
+ menuNodes.forEach(item => {
+ if (!item.classList.contains(Selectors.classes.dropdownmoremenu)) {
+ // After moving the menu items into the moreDropdown check again
+ // if the menu height is still larger then the height of the parent.
+ if (menu.offsetHeight > maxHeight) {
+ const lastNode = menu.removeChild(item);
+ // Move this node into the more dropdown menu.
+ moveIntoMoreDropdown(menu, lastNode, true);
+ }
+ }
+ });
+ } else {
+ // If the menu height is smaller than the height of the parent, then try returning navlinks to the menu.
+
+ if ('children' in moreDropdown) {
+ const menuNodes = Array.from(moreDropdown.children);
+ menuNodes.forEach(item => {
+ // Don't move the node to the more menu if it is explicitly defined that
+ // this node should be displayed in the more dropdown menu at all times.
+ if (menu.offsetHeight < maxHeight && item.dataset.forceintomoremenu !== 'true') {
+ const lastNode = moreDropdown.removeChild(item);
+ // Move this node from the more dropdown menu into the main section of the menu.
+ moveOutOfMoreDropdown(menu, lastNode);
+ }
+ });
+
+ // If there are no more menuNodes in the dropdown we can hide the moreButton.
+ if (menuNodes.length === 0) {
+ moreButton.classList.add(Selectors.classes.hidden);
+ }
+ }
+
+ if (menu.offsetHeight > maxHeight) {
+ autoCollapse(menu);
+ }
+ }
+ menu.parentNode.classList.add(Selectors.classes.observed);
+};
+
+/**
+ * Move a node into the "more" dropdown menu.
+ *
+ * This method forces a given navigation node to be added and displayed within the "more" dropdown menu.
+ *
+ * @param {HTMLElement} menu The navbar moremenu.
+ * @param {HTMLElement} navNode The navigation node.
+ * @param {boolean} prepend Whether to prepend or append the node to the content in the more dropdown menu.
+ */
+const moveIntoMoreDropdown = (menu, navNode, prepend = false) => {
+ const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
+ const dropdownToggle = menu.querySelector('.' + Selectors.classes.dropdowntoggle);
+
+ const navLink = navNode.querySelector('.' + Selectors.classes.navlink);
+ // If there are navLinks that contain an active link in the moreDropdown
+ // make the dropdownToggle in the moreButton active.
+ if (navLink.classList.contains(Selectors.classes.active)) {
+ dropdownToggle.classList.add(Selectors.classes.active);
+ }
+
+ // Change the styling of the navLink to a dropdownitem and push it into
+ // the moreDropdown.
+ navLink.classList.remove(Selectors.classes.navlink);
+ navLink.classList.add(Selectors.classes.dropdownitem);
+ if (prepend) {
+ moreDropdown.prepend(navNode);
+ } else {
+ moreDropdown.append(navNode);
+ }
+};
+
+/**
+ * Move a node out of the "more" dropdown menu.
+ *
+ * This method forces a given node from the "more" dropdown menu to be displayed in the main section of the menu.
+ *
+ * @param {HTMLElement} menu The navbar moremenu.
+ * @param {HTMLElement} navNode The navigation node.
+ */
+const moveOutOfMoreDropdown = (menu, navNode) => {
+ const moreButton = menu.querySelector(Selectors.regions.morebutton);
+ const dropdownToggle = menu.querySelector('.' + Selectors.classes.dropdowntoggle);
+ const navLink = navNode.querySelector('.' + Selectors.classes.dropdownitem);
+
+ // Stop displaying the active state on the dropdownToggle if
+ // the active navlink is removed.
+ if (navLink.classList.contains(Selectors.classes.active)) {
+ dropdownToggle.classList.remove(Selectors.classes.active);
+ }
+ navLink.classList.remove(Selectors.classes.dropdownitem);
+ navLink.classList.add(Selectors.classes.navlink);
+ menu.insertBefore(navNode, moreButton);
+};
+
+/**
+ * Initialise the more menus.
+ *
+ * @param {HTMLElement} menu The navbar moremenu.
+ */
+export default menu => {
+ menu.firstElementChild.querySelector('[role="menuitem"]').setAttribute('tabindex', '0');
+ // Pre-populate the "more" dropdown menu with navigation nodes which are set to be displayed in this menu
+ // by default at all times.
+ if ('children' in menu) {
+ const moreButton = menu.querySelector(Selectors.regions.morebutton);
+ const menuNodes = Array.from(menu.children);
+ menuNodes.forEach((item) => {
+ if (!item.classList.contains(Selectors.classes.dropdownmoremenu) &&
+ item.dataset.forceintomoremenu === 'true') {
+ // Append this node into the more dropdown menu.
+ moveIntoMoreDropdown(menu, item, false);
+ // After adding the node into the more dropdown menu, make sure that the more dropdown menu button
+ // is displayed.
+ if (moreButton.classList.contains(Selectors.classes.hidden)) {
+ moreButton.classList.remove(Selectors.classes.hidden);
+ }
+ }
+ });
+ }
+ // Populate the more dropdown menu with additional nodes if necessary, depending on the current screen size.
+ autoCollapse(menu);
+ menu_navigation(menu);
+
+ // When the screen size changes make sure the menu still fits.
+ window.addEventListener('resize', () => {
+ autoCollapse(menu);
+ menu_navigation(menu);
+ });
+
+ const toggledropdown = e => {
+ const innerMenu = e.target.parentNode.querySelector(Selectors.attributes.menu);
+ if (innerMenu) {
+ innerMenu.classList.toggle('show');
+ }
+ e.stopPropagation();
+ };
+
+ // If there are dropdowns in the MoreMenu, add a new
+ // event listener to show the contents on click and prevent the
+ // moreMenu from closing.
+ $('.' + Selectors.classes.dropdownmoremenu).on('show.bs.dropdown', function() {
+ const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
+ moreDropdown.querySelectorAll('.dropdown').forEach((dropdown) => {
+ dropdown.removeEventListener('click', toggledropdown, true);
+ dropdown.addEventListener('click', toggledropdown, true);
+ });
+ });
+};
diff --git a/lib/amd/src/usermenu.js b/lib/amd/src/usermenu.js
new file mode 100644
index 00000000000..25d05ca2f4d
--- /dev/null
+++ b/lib/amd/src/usermenu.js
@@ -0,0 +1,127 @@
+// 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 .
+
+/**
+ * Initializes and handles events in the user menu.
+ *
+ * @module core/usermenu
+ * @copyright 2021 Moodle
+ * @author Mihail Geshoski
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+import {space, enter} from 'core/key_codes';
+
+/**
+ * User menu constants.
+ */
+const selectors = {
+ userMenu: '.usermenu',
+ userMenuCarousel: '.usermenu #usermenu-carousel',
+ userMenuCarouselItem: '.usermenu #usermenu-carousel .carousel-item',
+ userMenuCarouselItemActive: '.usermenu #usermenu-carousel .carousel-item.active',
+ userMenuCarouselNavigationLink: '.usermenu #usermenu-carousel .carousel-navigation-link',
+};
+
+/**
+ * Register event listeners.
+ */
+const registerEventListeners = () => {
+ const userMenu = document.querySelector(selectors.userMenu);
+
+ // Handle the 'shown.bs.dropdown' event (Fired when the dropdown menu is fully displayed).
+ $(selectors.userMenu).on('shown.bs.dropdown', () => {
+ const activeCarouselItem = document.querySelector(selectors.userMenuCarouselItemActive);
+ // Set the focus on the active carousel item.
+ activeCarouselItem.focus();
+
+ userMenu.querySelectorAll(selectors.userMenuCarouselItem).forEach(element => {
+ // Resize all non-active carousel items to match the height and width of the current active (main)
+ // carousel item to avoid sizing inconsistencies. This has to be done once the dropdown menu is fully
+ // displayed ('shown.bs.dropdown') as the offsetWidth and offsetHeight cannot be obtained when the
+ // element is hidden.
+ if (!element.classList.contains('active')) {
+ element.style.width = activeCarouselItem.offsetWidth + 'px';
+ element.style.height = activeCarouselItem.offsetHeight + 'px';
+ }
+ });
+ });
+
+ // Handle click events in the user menu.
+ userMenu.addEventListener('click', (e) => {
+
+ // Handle click event on the carousel navigation (control) links in the user menu.
+ if (e.target.matches(selectors.userMenuCarouselNavigationLink)) {
+ carouselManagement(e);
+ }
+ });
+
+ userMenu.addEventListener('keydown', e => {
+ // Handle keydown event on the carousel navigation (control) links in the user menu.
+ if ((e.keyCode === space ||
+ e.keyCode === enter) &&
+ e.target.matches(selectors.userMenuCarouselNavigationLink)) {
+ e.preventDefault();
+ carouselManagement(e);
+ }
+ });
+
+ /**
+ * We do the same actions here even if the caller was a click or button press.
+ *
+ * @param {Event} e The triggering element and key presses etc.
+ */
+ const carouselManagement = e => {
+ // By default the user menu dropdown element closes on a click event. This behaviour is not desirable
+ // as we need to be able to navigate through the carousel items (submenus of the user menu) within the
+ // user menu. Therefore, we need to prevent the propagation of this event and then manually call the
+ // carousel transition.
+ e.stopPropagation();
+ // The id of the targeted carousel item.
+ const targetedCarouselItemId = e.target.dataset.carouselTargetId;
+ const targetedCarouselItem = userMenu.querySelector('#' + targetedCarouselItemId);
+ // Get the position (index) of the targeted carousel item within the parent container element.
+ const index = Array.from(targetedCarouselItem.parentNode.children).indexOf(targetedCarouselItem);
+ // Navigate to the targeted carousel item.
+ $(selectors.userMenuCarousel).carousel(index);
+
+ };
+
+ // Handle the 'hide.bs.dropdown' event (Fired when the dropdown menu is being closed).
+ $(selectors.userMenu).on('hide.bs.dropdown', () => {
+ // Reset the state once the user menu dropdown is closed and return back to the first (main) carousel item
+ // if necessary.
+ $(selectors.userMenuCarousel).carousel(0);
+ });
+
+ // Handle the 'slid.bs.carousel' event (Fired when the carousel has completed its slide transition).
+ $(selectors.userMenuCarousel).on('slid.bs.carousel', () => {
+ const activeCarouselItem = userMenu.querySelector(selectors.userMenuCarouselItemActive);
+ // Set the focus on the newly activated carousel item.
+ activeCarouselItem.focus();
+ });
+};
+
+/**
+ * Initialize the user menu.
+ */
+const init = () => {
+ registerEventListeners();
+};
+
+export default {
+ init: init,
+};
diff --git a/lib/blocklib.php b/lib/blocklib.php
index 849f848720f..35a6102635d 100644
--- a/lib/blocklib.php
+++ b/lib/blocklib.php
@@ -1520,6 +1520,7 @@ class block_manager {
$deleteurlparams = $this->page->url->params();
$deletepage->set_url($deleteurlbase, $deleteurlparams);
$deletepage->set_block_actions_done();
+ $deletepage->set_secondarynav($this->get_secondarynav($block));
// At this point we are either going to redirect, or display the form, so
// overwrite global $PAGE ready for this. (Formslib refers to it.)
$PAGE = $deletepage;
@@ -1608,6 +1609,23 @@ class block_manager {
return true;
}
+ /**
+ * Convenience function to check whether a block is implementing a secondary nav class and return it
+ * initialised to the calling function
+ *
+ * @param block_base $block
+ * @return \core\navigation\views\secondary
+ */
+ protected function get_secondarynav(block_base $block): \core\navigation\views\secondary {
+ $class = "core_block\\local\\views\\secondary";
+ if (class_exists("block_{$block->name()}\\local\\views\\secondary")) {
+ $class = "block_{$block->name()}\\local\\views\\secondary";
+ }
+ $secondarynav = new $class($this->page);
+ $secondarynav->initialise();
+ return $secondarynav;
+ }
+
/**
* Handle showing/processing the submission from the block editing form.
* @return boolean true if the form was submitted and the new config saved. Does not
@@ -1635,6 +1653,8 @@ class block_manager {
$editpage->set_course($this->page->course);
//$editpage->set_context($block->context);
$editpage->set_context($this->page->context);
+ $editpage->set_secondarynav($this->get_secondarynav($block));
+
if ($this->page->cm) {
$editpage->set_cm($this->page->cm);
}
diff --git a/lib/classes/navigation/output/more_menu.php b/lib/classes/navigation/output/more_menu.php
new file mode 100644
index 00000000000..7947c88766e
--- /dev/null
+++ b/lib/classes/navigation/output/more_menu.php
@@ -0,0 +1,71 @@
+.
+
+namespace core\navigation\output;
+
+use renderable;
+use renderer_base;
+use templatable;
+use custom_menu;
+
+/**
+ * more menu navigation renderable
+ *
+ * @package core
+ * @category navigation
+ * @copyright 2021 onwards Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class more_menu implements renderable, templatable {
+
+ protected $content;
+ protected $navbarstyle;
+ protected $haschildren;
+
+ /**
+ * Constructor for this class.
+ *
+ * @param object $content Navigation objects.
+ * @param string $navbarstyle class name.
+ * @param bool $haschildren The content has children.
+ */
+ public function __construct(object $content, string $navbarstyle, bool $haschildren = true) {
+ $this->content = $content;
+ $this->navbarstyle = $navbarstyle;
+ $this->haschildren = $haschildren;
+ }
+
+ /**
+ * Return data for rendering a template.
+ *
+ * @param renderer_base $output The output
+ * @return array Data for rendering a template
+ */
+ public function export_for_template(renderer_base $output): array {
+ $data = ['navbarstyle' => $this->navbarstyle];
+ if ($this->haschildren) {
+ if (!isset($this->content->children) || count($this->content->children) == 0) {
+ $data = [];
+ }
+ $data['nodecollection'] = $this->content;
+ } else {
+ $data['nodearray'] = (array) $this->content;
+ }
+
+ return $data;
+ }
+
+}
diff --git a/lib/classes/navigation/output/primary.php b/lib/classes/navigation/output/primary.php
new file mode 100644
index 00000000000..f8103a6c184
--- /dev/null
+++ b/lib/classes/navigation/output/primary.php
@@ -0,0 +1,273 @@
+.
+
+namespace core\navigation\output;
+
+use renderable;
+use renderer_base;
+use templatable;
+use custom_menu;
+
+/**
+ * Primary navigation renderable
+ *
+ * This file combines primary nav, custom menu, lang menu and
+ * usermenu into a standardized format for the frontend
+ *
+ * @package core
+ * @category navigation
+ * @copyright 2021 onwards Peter Dias
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class primary implements renderable, templatable {
+ /** @var moodle_page $page the moodle page that the navigation belongs to */
+ private $page = null;
+
+ /**
+ * primary constructor.
+ * @param \moodle_page $page
+ */
+ public function __construct($page) {
+ $this->page = $page;
+ }
+
+ /**
+ * Combine the various menus into a standardized output.
+ *
+ * @param renderer_base|null $output
+ * @return array
+ */
+ public function export_for_template(?renderer_base $output = null): array {
+ if (!$output) {
+ $output = $this->page->get_renderer('core');
+ }
+
+ $menudata = (object) array_merge($this->get_primary_nav(), $this->get_custom_menu($output));
+ $moremenu = new \core\navigation\output\more_menu($menudata, 'navbar-nav', false);
+
+ return [
+ 'moremenu' => $moremenu->export_for_template($output),
+ 'lang' => !isloggedin() || isguestuser() ? $this->get_lang_menu($output) : [],
+ 'user' => $this->get_user_menu($output),
+ ];
+ }
+
+ /**
+ * Get the primary nav object and standardize the output
+ *
+ * @return array
+ */
+ protected function get_primary_nav(): array {
+ $nodes = [];
+ foreach ($this->page->primarynav->children as $node) {
+ $nodes[] = [
+ 'title' => $node->get_title(),
+ 'url' => $node->action(),
+ 'text' => $node->text,
+ 'icon' => $node->icon,
+ 'isactive' => $node->isactive,
+ ];
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * Custom menu items reside on the same level as the original nodes.
+ * Fetch and convert the nodes to a standardised array.
+ *
+ * @param renderer_base $output
+ * @return array
+ */
+ protected function get_custom_menu(renderer_base $output): array {
+ global $CFG;
+
+ // Early return if a custom menu does not exists.
+ if (empty($CFG->custommenuitems)) {
+ return [];
+ }
+
+ $custommenuitems = $CFG->custommenuitems;
+ $currentlang = current_language();
+ $custommenunodes = custom_menu::convert_text_to_menu_nodes($custommenuitems, $currentlang);
+ $nodes = [];
+ foreach ($custommenunodes as $node) {
+ $nodes[] = $node->export_for_template($output);
+ }
+
+ return $nodes;
+ }
+
+ /**
+ * Get a list of options for the lang picker.
+ *
+ * @param renderer_base $output
+ * @return array
+ */
+ protected function get_lang_menu(renderer_base $output): array {
+ // Early return if a lang menu does not exists.
+ if (empty($output->lang_menu())) {
+ return [];
+ }
+
+ $currentlang = current_language();
+ $langs = get_string_manager()->get_list_of_translations();
+ $nodes = [];
+ $activelanguage = '';
+
+ // Add the lang picker if needed.
+ foreach ($langs as $langtype => $langname) {
+ $isactive = $langtype == $currentlang;
+ $node = [
+ 'title' => $langname,
+ 'text' => $langname,
+ 'link' => true,
+ 'isactive' => $isactive,
+ 'url' => $isactive ? new \moodle_url('#') : new \moodle_url($this->page->url, ['lang' => $langtype]),
+ ];
+
+ $nodes[] = $node;
+
+ if ($isactive) {
+ $activelanguage = $langname;
+ }
+ }
+
+ return [
+ 'title' => $activelanguage,
+ 'items' => $nodes,
+ ];
+ }
+
+ /**
+ * Get/Generate the user menu.
+ *
+ * This is leveraging the data from user_get_user_navigation_info and the logic in $OUTPUT->user_menu()
+ *
+ * @param renderer_base $output
+ * @return array
+ */
+ public function get_user_menu(renderer_base $output): array {
+ global $CFG, $USER, $PAGE;
+ require_once($CFG->dirroot . '/user/lib.php');
+
+ $usermenudata = [];
+ $submenusdata = [];
+ $info = user_get_user_navigation_info($USER, $PAGE);
+ if (isset($info->unauthenticateduser)) {
+ $info->unauthenticateduser['content'] = get_string($info->unauthenticateduser['content']);
+ $info->unauthenticateduser['url'] = get_login_url();
+ return (array) $info;
+ }
+ // Gather all the avatar data to be displayed in the user menu.
+ $usermenudata['avatardata'][] = [
+ 'content' => $info->metadata['useravatar'],
+ 'classes' => 'current'
+ ];
+ $usermenudata['userfullname'] = $info->metadata['realuserfullname'] ?? $info->metadata['userfullname'];
+
+ // Logged in as someone else.
+ if ($info->metadata['asotheruser']) {
+ $usermenudata['avatardata'][] = [
+ 'content' => $info->metadata['realuseravatar'],
+ 'classes' => 'realuser'
+ ];
+ $usermenudata['metadata'][] = [
+ 'content' => get_string('loggedinas', 'moodle', $info->metadata['userfullname']),
+ 'classes' => 'viewingas'
+ ];
+ }
+
+ // Gather all the meta data to be displayed in the user menu.
+ $metadata = [
+ 'asotherrole' => [
+ 'value' => 'rolename',
+ 'class' => 'role role-##GENERATEDCLASS##',
+ ],
+ 'userloginfail' => [
+ 'value' => 'userloginfail',
+ 'class' => 'loginfailures',
+ ],
+ 'asmnetuser' => [
+ 'value' => 'mnetidprovidername',
+ 'class' => 'mnet mnet-##GENERATEDCLASS##',
+ ],
+ ];
+ foreach ($metadata as $key => $value) {
+ if (!empty($info->metadata[$key])) {
+ $content = $info->metadata[$value['value']] ?? '';
+ $generatedclass = strtolower(preg_replace('#[ ]+#', '-', trim($content)));
+ $customclass = str_replace('##GENERATEDCLASS##', $generatedclass, ($value['class'] ?? ''));
+ $usermenudata['metadata'][] = [
+ 'content' => $content,
+ 'classes' => $customclass
+ ];
+ }
+ }
+
+ $modifiedarray = array_map(function($value) {
+ $value->divider = $value->itemtype == 'divider';
+ $value->link = $value->itemtype == 'link';
+ if (isset($value->pix) && !empty($value->pix)) {
+ $value->pixicon = $value->pix;
+ unset($value->pix);
+ }
+ return $value;
+ }, $info->navitems);
+
+ // Include the language menu as a submenu within the user menu.
+ $langmenu = $this->get_lang_menu($output);
+ if (!empty($langmenu)) {
+ $languageitems = $langmenu['items'];
+ // If there are available languages, generate the data for the the language selector submenu.
+ if (!empty($languageitems)) {
+ $langsubmenuid = uniqid();
+ // Generate the data for the link to language selector submenu.
+ $language = (object) [
+ 'itemtype' => 'submenu-link',
+ 'submenuid' => $langsubmenuid,
+ 'title' => get_string('language'),
+ 'pixicon' => 'i/language',
+ 'divider' => false,
+ 'submenulink' => true,
+ ];
+
+ // Place the link before the 'Log out' menu item which is either the last item in the menu or
+ // second to last when 'Switch roles' is available.
+ $menuposition = count($modifiedarray) - 1;
+ if (has_capability('moodle/role:switchroles', $PAGE->context)) {
+ $menuposition = count($modifiedarray) - 2;
+ }
+ array_splice($modifiedarray, $menuposition, 0, [$language]);
+
+ // Generate the data for the language selector submenu.
+ $submenusdata[] = (object)[
+ 'id' => $langsubmenuid,
+ 'title' => get_string('languageselector'),
+ 'items' => $languageitems,
+ ];
+ }
+ }
+
+ // Add dividers after the first item and before the last item.
+ $modifiedarray[0]->divider = true;
+ $modifiedarray[count($modifiedarray) - 2]->divider = true;
+ $usermenudata['items'] = $modifiedarray;
+ $usermenudata['submenus'] = array_values($submenusdata);
+
+ return $usermenudata;
+ }
+}
diff --git a/lib/classes/navigation/views/primary.php b/lib/classes/navigation/views/primary.php
index 67f45c7ebe6..c30b6780da1 100644
--- a/lib/classes/navigation/views/primary.php
+++ b/lib/classes/navigation/views/primary.php
@@ -35,8 +35,10 @@ class primary extends view {
return;
}
$this->id = 'primary_navigation';
- $this->add(get_string('home'), new \moodle_url('/'), self::TYPE_SYSTEM,
+ if (get_home_page() == HOMEPAGE_SITE && isloggedin() && !isguestuser()) {
+ $this->add(get_string('home'), new \moodle_url('/'), self::TYPE_SYSTEM,
null, 'home', new \pix_icon('i/home', ''));
+ }
// Add the dashboard link.
if (isloggedin() && !isguestuser()) { // Makes no sense if you aren't logged in.
diff --git a/lib/classes/navigation/views/secondary.php b/lib/classes/navigation/views/secondary.php
index 918bac3f655..e28655811f6 100644
--- a/lib/classes/navigation/views/secondary.php
+++ b/lib/classes/navigation/views/secondary.php
@@ -32,6 +32,10 @@ use navigation_node;
class secondary extends view {
/** @var string $headertitle The header for this particular menu*/
public $headertitle;
+
+ /** @var int The maximum limit of navigation nodes displayed in the secondary navigation */
+ const MAX_DISPLAYED_NAV_NODES = 5;
+
/**
* Defines the default structure for the secondary nav in a course context.
*
@@ -85,13 +89,13 @@ class secondary extends view {
return [
self::TYPE_SETTING => [
'modedit' => 1,
- 'roleoverride' => 3,
- 'rolecheck' => 3.1,
- 'logreport' => 4,
- "mod_{$this->page->activityname}_useroverrides" => 5, // Overrides are module specific.
- "mod_{$this->page->activityname}_groupoverrides" => 6,
- 'roleassign' => 7,
- 'filtermanage' => 8,
+ "mod_{$this->page->activityname}_useroverrides" => 3, // Overrides are module specific.
+ "mod_{$this->page->activityname}_groupoverrides" => 4,
+ 'roleassign' => 5,
+ 'filtermanage' => 6,
+ 'roleoverride' => 7,
+ 'rolecheck' => 7.1,
+ 'logreport' => 8,
'backup' => 9,
'restore' => 10,
'competencybreakdown' => 11,
@@ -102,6 +106,34 @@ class secondary extends view {
];
}
+ /**
+ * Define the keys of the course secondary nav nodes that should be forced into the "more" menu by default.
+ *
+ * @return array
+ */
+ protected function get_default_course_more_menu_nodes(): array {
+ return [];
+ }
+
+ /**
+ * Define the keys of the module secondary nav nodes that should be forced into the "more" menu by default.
+ *
+ * @return array
+ */
+ protected function get_default_module_more_menu_nodes(): array {
+ return ['roleoverride', 'rolecheck', 'logreport', 'roleassign', 'filtermanage', 'backup', 'restore',
+ 'competencybreakdown'];
+ }
+
+ /**
+ * Define the keys of the admin secondary nav nodes that should be forced into the "more" menu by default.
+ *
+ * @return array
+ */
+ protected function get_default_admin_more_menu_nodes(): array {
+ return [];
+ }
+
/**
* Initialise the view based navigation based on the current context.
*
@@ -119,23 +151,36 @@ class secondary extends view {
$this->id = 'secondary_navigation';
$context = $this->context;
$this->headertitle = get_string('menu');
+ $defaultmoremenunodes = [];
switch ($context->contextlevel) {
case CONTEXT_COURSE:
if ($this->page->course->id != $SITE->id) {
$this->headertitle = get_string('courseheader');
$this->load_course_navigation();
+ $defaultmoremenunodes = $this->get_default_course_more_menu_nodes();
}
break;
case CONTEXT_MODULE:
$this->headertitle = get_string('activityheader');
$this->load_module_navigation();
+ $defaultmoremenunodes = $this->get_default_module_more_menu_nodes();
break;
case CONTEXT_SYSTEM:
+ $this->headertitle = get_string('homeheader');
$this->load_admin_navigation();
+ $defaultmoremenunodes = $this->get_default_admin_more_menu_nodes();
break;
}
+ $this->remove_unwanted_nodes();
+
+ // Don't need to show anything if only the view node is available. Remove it.
+ if ($this->children->count() == 1) {
+ $this->children->remove('modulepage');
+ }
+
+ $this->force_nodes_into_more_menu($defaultmoremenunodes);
// Search and set the active node.
$this->scan_for_active_node($this);
$this->initialised = true;
@@ -157,14 +202,17 @@ class secondary extends view {
$this->add(get_string('coursepage', 'admin'), $url, self::TYPE_COURSE, null, 'coursehome');
$nodes = $this->get_default_course_mapping();
- $nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings']);
- $nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation']);
+ $nodesordered = $this->get_leaf_nodes($settingsnav, $nodes['settings'] ?? []);
+ $nodesordered += $this->get_leaf_nodes($navigation, $nodes['navigation'] ?? []);
$this->add_ordered_nodes($nodesordered);
- // All additional nodes will be available under the 'Course admin' page.
- $text = get_string('courseadministration');
- $url = new \moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
- $this->add($text, $url, null, null, 'courseadmin', new \pix_icon('t/edit', $text));
+ $coursecontext = \context_course::instance($course->id);
+ if (has_capability('moodle/course:update', $coursecontext)) {
+ // All additional nodes will be available under the 'Course admin' page.
+ $text = get_string('courseadministration');
+ $url = new \moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
+ $this->add($text, $url, null, null, 'courseadmin', new \pix_icon('t/edit', $text));
+ }
}
/**
@@ -180,7 +228,12 @@ class secondary extends view {
$nodes = $this->get_default_module_mapping();
if ($mainnode) {
- $this->add(get_string('module', 'course'), $this->page->url, null, null, 'modulepage');
+ $url = new \moodle_url('/mod/' . $this->page->activityname . '/view.php', ['id' => $this->page->cm->id]);
+ $setactive = $url->compare($this->page->url, URL_MATCH_BASE);
+ $node = $this->add(get_string('module', 'course'), $url, null, null, 'modulepage');
+ if ($setactive) {
+ $node->make_active();
+ }
// Add the initial nodes.
$nodesordered = $this->get_leaf_nodes($mainnode, $nodes);
$this->add_ordered_nodes($nodesordered);
@@ -269,4 +322,41 @@ class secondary extends view {
}
}
}
+
+ /**
+ * Force certain secondary navigation nodes to be displayed in the "more" menu.
+ *
+ * @param array $defaultmoremenunodes Array with navigation node keys of the pre-defined nodes that
+ * should be added into the "more" menu by default
+ */
+ protected function force_nodes_into_more_menu(array $defaultmoremenunodes = []) {
+ // Counter of the navigation nodes that are initially displayed in the secondary nav
+ // (excludes the nodes from the "more" menu).
+ $displayednodescount = 0;
+ foreach ($this->children as $child) {
+ // Skip if the navigation node has been already forced into the "more" menu.
+ if ($child->forceintomoremenu) {
+ continue;
+ }
+ // If the navigation node is in the pre-defined list of nodes that should be added by default in the
+ // "more" menu or the maximum limit of displayed navigation nodes has been reached.
+ if (in_array($child->key, $defaultmoremenunodes) || $displayednodescount >= self::MAX_DISPLAYED_NAV_NODES) {
+ // Force the node and its children into the "more" menu.
+ $child->set_force_into_more_menu(true);
+ continue;
+ }
+ $displayednodescount++;
+ }
+ }
+
+ /**
+ * Remove navigation nodes that should not be displayed in the secondary navigation.
+ */
+ protected function remove_unwanted_nodes() {
+ foreach ($this->children as $child) {
+ if (!$child->showinsecondarynavigation) {
+ $child->remove();
+ }
+ }
+ }
}
diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php
index b35e5984e77..29d7f954b4d 100644
--- a/lib/classes/output/icon_system_fontawesome.php
+++ b/lib/classes/output/icon_system_fontawesome.php
@@ -345,6 +345,7 @@ class icon_system_fontawesome extends icon_system_font {
'core:i/warning' => 'fa-exclamation text-warning',
'core:i/window_close' => 'fa-window-close',
'core:i/withsubcat' => 'fa-plus-square',
+ 'core:i/language' => 'fa-language',
'core:m/USD' => 'fa-usd',
'core:t/addcontact' => 'fa-address-card',
'core:t/add' => 'fa-plus',
diff --git a/lib/classes/report_helper.php b/lib/classes/report_helper.php
index 31817f2d7b8..1edd57b0ed6 100644
--- a/lib/classes/report_helper.php
+++ b/lib/classes/report_helper.php
@@ -61,7 +61,7 @@ class report_helper {
if (!empty($menu)) {
$select = new url_select($menu, $activeurl, null, 'choosecoursereport');
- $select->set_label(get_string('report'), ['class' => 'accesshide']);
+ $select->set_label(get_string('reporttype'), ['class' => 'accesshide']);
$select->attributes['style'] = "margin-bottom: 1.5rem";
$select->class .= " mb-4";
echo $OUTPUT->render($select);
diff --git a/lib/navigationlib.php b/lib/navigationlib.php
index 6df89107bcb..e8a8abf75e6 100644
--- a/lib/navigationlib.php
+++ b/lib/navigationlib.php
@@ -149,6 +149,10 @@ class navigation_node implements renderable {
public $requiresajaxloading = false;
/** @var bool If set to true this node will be added to the "flat" navigation */
public $showinflatnavigation = false;
+ /** @var bool If set to true this node will be forced into a "more" menu whenever possible */
+ public $forceintomoremenu = false;
+ /** @var bool If set to true this node will be displayed in the "secondary" navigation when applicable */
+ public $showinsecondarynavigation = true;
/**
* Constructs a new navigation_node
@@ -840,6 +844,30 @@ class navigation_node implements renderable {
return $this->action;
}
+ /**
+ * Sets whether the node and its children should be added into a "more" menu whenever possible.
+ *
+ * @param bool $forceintomoremenu
+ */
+ public function set_force_into_more_menu(bool $forceintomoremenu = false) {
+ $this->forceintomoremenu = $forceintomoremenu;
+ foreach ($this->children as $child) {
+ $child->set_force_into_more_menu($forceintomoremenu);
+ }
+ }
+
+ /**
+ * Sets whether the node and its children should be displayed in the "secondary" navigation when applicable.
+ *
+ * @param bool $show
+ */
+ public function set_show_in_secondary_navigation(bool $show = true) {
+ $this->showinsecondarynavigation = $show;
+ foreach ($this->children as $child) {
+ $child->set_show_in_secondary_navigation($show);
+ }
+ }
+
/**
* Add the menu item to handle locking and unlocking of a conext.
*
diff --git a/lib/outputcomponents.php b/lib/outputcomponents.php
index 52c291eaa68..599b479fbe3 100644
--- a/lib/outputcomponents.php
+++ b/lib/outputcomponents.php
@@ -3981,6 +3981,10 @@ class context_header implements renderable {
* page => page object. Don't include if the image is an external image.
*/
public $additionalbuttons;
+ /**
+ * @var string $prefix A string that is before the title.
+ */
+ public $prefix;
/**
* Constructor.
@@ -3989,8 +3993,9 @@ class context_header implements renderable {
* @param int $headinglevel Main heading 'h' tag level.
* @param string|null $imagedata HTML code for the picture in the page header.
* @param string $additionalbuttons Buttons for the header e.g. Messaging button for the user header.
+ * @param string $prefix Text that precedes the heading.
*/
- public function __construct($heading = null, $headinglevel = 1, $imagedata = null, $additionalbuttons = null) {
+ public function __construct($heading = null, $headinglevel = 1, $imagedata = null, $additionalbuttons = null, $prefix = null) {
$this->heading = $heading;
$this->headinglevel = $headinglevel;
@@ -4000,6 +4005,7 @@ class context_header implements renderable {
if (isset($this->additionalbuttons)) {
$this->format_button_images();
}
+ $this->prefix = $prefix;
}
/**
diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php
index 69677c274f6..8deb104e90c 100644
--- a/lib/outputrenderers.php
+++ b/lib/outputrenderers.php
@@ -3321,41 +3321,26 @@ EOD;
$loginpage = $this->is_login_page();
$loginurl = get_login_url();
- // If not logged in, show the typical not-logged-in string.
- if (!isloggedin()) {
- $returnstr = get_string('loggedinnot', 'moodle');
- if (!$loginpage) {
- $returnstr .= " (" . get_string('login') . ')';
- }
- return html_writer::div(
- html_writer::span(
- $returnstr,
- 'login nav-link'
- ),
- $usermenuclasses
- );
-
- }
-
- // If logged in as a guest user, show a string to that effect.
- if (isguestuser()) {
- $returnstr = get_string('loggedinasguest');
- if (!$loginpage && $withlinks) {
- $returnstr .= " (".get_string('login').')';
- }
-
- return html_writer::div(
- html_writer::span(
- $returnstr,
- 'login nav-link'
- ),
- $usermenuclasses
- );
- }
// Get some navigation opts.
$opts = user_get_user_navigation_info($user, $this->page);
+ if (!empty($opts->unauthenticateduser)) {
+ $returnstr = get_string($opts->unauthenticateduser['content'], 'moodle');
+ // If not logged in, show the typical not-logged-in string.
+ if (!$loginpage && (!$opts->unauthenticateduser['guest'] || $withlinks)) {
+ $returnstr .= " (" . get_string('login') . ')';
+ }
+
+ return html_writer::div(
+ html_writer::span(
+ $returnstr,
+ 'login nav-link'
+ ),
+ $usermenuclasses
+ );
+ }
+
$avatarclasses = "avatars";
$avatarcontents = html_writer::span($opts->metadata['useravatar'], 'avatar current');
$usertextcontents = $opts->metadata['userfullname'];
@@ -4293,10 +4278,14 @@ EOD;
// Image data.
if (isset($contextheader->imagedata)) {
// Header specific image.
- $html .= html_writer::div($contextheader->imagedata, 'page-header-image');
+ $html .= html_writer::div($contextheader->imagedata, 'page-header-image icon-size-7');
}
// Headings.
+ if (isset($contextheader->prefix)) {
+ $prefix = html_writer::div($contextheader->prefix, 'text-muted');
+ $heading = $prefix . $heading;
+ }
$html .= html_writer::tag('div', $heading, array('class' => 'page-header-headings'));
// Buttons.
diff --git a/lib/pagelib.php b/lib/pagelib.php
index 2f08c914f6d..02000fdb6a9 100644
--- a/lib/pagelib.php
+++ b/lib/pagelib.php
@@ -28,6 +28,7 @@
defined('MOODLE_INTERNAL') || die();
use core\navigation\views\primary;
use core\navigation\views\secondary;
+use core\navigation\output\primary as primaryoutput;
/**
* $PAGE is a central store of information about the current page we are
@@ -84,6 +85,7 @@ use core\navigation\views\secondary;
* @property-read secondary $secondarynav The secondary navigation object
* used to display the secondarynav in boost
* @property-read primary $primarynav The primary navigation object used to display the primary nav in boost
+ * @property-read primaryoutput $primarynavcombined The primary navigation object used to display the primary nav in boost
* @property-read global_navigation $navigation The navigation structure for this page.
* @property-read xhtml_container_stack $opencontainers Tracks XHTML tags on this page that have been opened but not closed.
* mainly for internal use by the rendering code.
@@ -311,6 +313,12 @@ class moodle_page {
*/
protected $_primarynav = null;
+ /**
+ * @var primaryoutput Contains the combined nav nodes that will appear
+ * in the primary navigation. Includes - primarynav, langmenu, usermenu
+ */
+ protected $_primarynavcombined = null;
+
/**
* @var navbar Contains the navbar structure.
*/
@@ -380,6 +388,11 @@ class moodle_page {
*/
protected $_regionmainsettingsinheader = false;
+ /**
+ * @var bool Should the secondary menu be rendered.
+ */
+ protected $_hassecondarynavigation = true;
+
/**
* Force the settings menu to be displayed on this page. This will only force the
* settings menu on an activity / resource page that is being displayed on a theme that
@@ -806,7 +819,13 @@ class moodle_page {
*/
protected function magic_get_secondarynav() {
if ($this->_secondarynav === null) {
- $this->_secondarynav = new secondary($this);
+ $class = 'core\navigation\views\secondary';
+ // Try and load a custom class first.
+ if (class_exists("mod_{$this->activityname}\\local\\views\\secondary")) {
+ $class = "mod_{$this->activityname}\\local\\views\\secondary";
+ }
+
+ $this->_secondarynav = new $class($this);
$this->_secondarynav->initialise();
}
return $this->_secondarynav;
@@ -824,6 +843,17 @@ class moodle_page {
return $this->_primarynav;
}
+ /**
+ * Returns the primary navigation object
+ * @return primary
+ */
+ protected function magic_get_primarynavcombined() {
+ if ($this->_primarynavcombined === null) {
+ $this->_primarynavcombined = new primaryoutput($this);
+ }
+ return $this->_primarynavcombined;
+ }
+
/**
* Returns request IP address.
*
@@ -1240,6 +1270,14 @@ class moodle_page {
}
}
+ /**
+ * Force set secondary_nav. Useful in cases where we dealing with non course modules. e.g. blocks, tools.
+ * @param secondary $nav
+ */
+ public function set_secondarynav(secondary $nav) {
+ $this->_secondarynav = $nav;
+ }
+
/**
* Adds a CSS class to the body tag of the page.
*
@@ -2133,4 +2171,22 @@ class moodle_page {
public function include_region_main_settings_in_header_actions() : bool {
return $this->_regionmainsettingsinheader;
}
+
+ /**
+ * Set the flag to indicate if the secondary navigation should be rendered.
+ *
+ * @param bool $value If the secondary navigation should be rendered.
+ */
+ public function has_secondary_navigation_setter(bool $value) : void {
+ $this->_hassecondarynavigation = $value;
+ }
+
+ /**
+ * Check if the secondary navigation should be rendered.
+ *
+ * @return bool
+ */
+ public function has_secondary_navigation() : bool {
+ return $this->_hassecondarynavigation;
+ }
}
diff --git a/lib/templates/full_header.mustache b/lib/templates/full_header.mustache
index d0453e2b744..f4fc8d6f8e8 100644
--- a/lib/templates/full_header.mustache
+++ b/lib/templates/full_header.mustache
@@ -32,6 +32,19 @@
+
+ {{#hasnavbar}}
+
+ {{{navbar}}}
+
+ {{/hasnavbar}}
+
+ {{{pageheadingbutton}}}
+
+
+ {{{courseheader}}}
+
+
{{#contextheader}}
@@ -50,19 +63,6 @@
{{/headeractions}}
-
- {{#hasnavbar}}
-
- {{{navbar}}}
-
- {{/hasnavbar}}
-
- {{{pageheadingbutton}}}
-
-
- {{{courseheader}}}
-
-
diff --git a/lib/templates/moremenu.mustache b/lib/templates/moremenu.mustache
new file mode 100644
index 00000000000..52662983ba9
--- /dev/null
+++ b/lib/templates/moremenu.mustache
@@ -0,0 +1,70 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/moremenu
+
+ The More menu.
+
+ Example context (json):
+ {
+ "nodecollection": {
+ "children": [
+ {
+ "text": "Home",
+ "action": "/index.php?redirect=0",
+ "active": "true"
+ },
+ {
+ "text": "Dashboard",
+ "action": "/my"
+ },
+ {
+ "text": "Courses",
+ "action": "/course"
+ },
+ {
+ "text": "Site Administration",
+ "action": "/admin/search.php"
+ }
+ ]
+ }
+ }
+}}
+
+{{#js}}
+require(['core/moremenu'], function(moremenu) {
+ moremenu(document.querySelector('#moremenu-{{ uniqid }}-{{navbarstyle}}'));
+});
+{{/js}}
diff --git a/lib/templates/moremenu_children.mustache b/lib/templates/moremenu_children.mustache
new file mode 100644
index 00000000000..e404bf975bf
--- /dev/null
+++ b/lib/templates/moremenu_children.mustache
@@ -0,0 +1,63 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/moremenu_children
+
+ The More menu children
+
+ Example context (json):
+ {
+ "divider": "",
+ "haschildren": "",
+ "uniqid": "Unique string",
+ "text": "Moodle community",
+ "children": "",
+ "title": "Moodle community",
+ "url": "https://moodle.org"
+ }
+}}
+{{#haschildren}}
+
diff --git a/lib/templates/user_action_menu_items.mustache b/lib/templates/user_action_menu_items.mustache
new file mode 100644
index 00000000000..ff8942af6fd
--- /dev/null
+++ b/lib/templates/user_action_menu_items.mustache
@@ -0,0 +1,84 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/user_action_menu_items
+
+ Template for user action menu items.
+
+ Context variables required for this template:
+ * items - The different items to be rendered
+ * link - If a link is provided render it.
+ * title - The text to be shown for the link.
+ * url - The href for the link.
+ * pixicon - (Optional) The Moodle icon to use
+ * imgsrc - (Optional) If provided, uses this as source for an image tag. Note: pixicon is preferred.
+ * submenulink - If a submenu link is provided render it.
+ * submenuid - The id of the targeted submenu.
+ * title - The text to be shown for the link.
+ * pixicon - (Optional) The Moodle icon to use.
+ * imgsrc - (Optional) If provided, uses this as source for an image tag. Note: pixicon is preferred.
+ * divider - Whether a divider is to be displayed or not
+
+ Example context (json):
+ {
+ "items": [
+ {
+ "link": {
+ "title": "Github user",
+ "url": "https://raw.githubusercontent.com/",
+ "pixicon": "t/dashboard",
+ "imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png"
+ },
+ "divider": 1
+ },
+ {
+ "submenulink": {
+ "title": "Title",
+ "submenuid": "86cebd87",
+ "pixicon": "t/dashboard",
+ "imgsrc": "https://raw.githubusercontent.com/moodle/moodle/master/pix/t/check.png"
+ },
+ "divider": 1
+ }
+ ]
+ }
+}}
+{{#items}}
+ {{#link}}
+
+ {{#pixicon}}
+ {{#pix}}{{pixicon}}{{/pix}}
+ {{/pixicon}}
+ {{^pixicon}}
+ {{#imgsrc}}{{/imgsrc}}
+ {{/pixicon}}
+ {{title}}
+
+ {{/link}}
+ {{#submenulink}}
+
+ {{#pixicon}}
+ {{#pix}}{{pixicon}}{{/pix}}
+ {{/pixicon}}
+ {{^pixicon}}
+ {{#imgsrc}}{{/imgsrc}}
+ {{/pixicon}}
+ {{title}}
+
+ {{/submenulink}}
+ {{#divider}}{{/divider}}
+{{/items}}
diff --git a/lib/templates/user_action_menu_submenu_items.mustache b/lib/templates/user_action_menu_submenu_items.mustache
new file mode 100644
index 00000000000..18896c4b900
--- /dev/null
+++ b/lib/templates/user_action_menu_submenu_items.mustache
@@ -0,0 +1,48 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/user_action_menu_submenus
+
+ Template for the submenus in the user action menu.
+
+ Context variables required for this template:
+ * items - The submenu items
+ * link - If a link is provided render it.
+ * title - The title added to the link.
+ * text - The text to be shown for the link.
+ * url - The href for the link.
+ * isactive - (Optional) Whether the item is currently active (has been selected).
+
+ Example context (json):
+ {
+ "items": {
+ "link": {
+ "title": "Submenu item 1",
+ "text": "Submenu item 1",
+ "url": "https://example.com/",
+ "isactive": 0
+ }
+ }
+ }
+}}
+{{#items}}
+ {{#link}}
+
+ {{text}}
+
+ {{/link}}
+{{/items}}
diff --git a/lib/templates/user_menu.mustache b/lib/templates/user_menu.mustache
new file mode 100644
index 00000000000..8738a56c9b9
--- /dev/null
+++ b/lib/templates/user_menu.mustache
@@ -0,0 +1,105 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/user_menu
+
+ Action link template.
+
+ Context variables required for this template:
+ * unauthenticateduseruser - (Optional) Items to be displayed if an an unautheticated user is accessing the site
+ * content - The content to be displayed in the header.
+ * url - The login url
+ * items - Array of user menu items used in user_action_menu_items. Required if the above not provided.
+ * metadata - Array of additional metadata to be displayed in the dropdown button.
+ * avatardata - Array of avatars to be displayed. Usually only the current user's avatar. If viewing as another user,
+ includes that user's avatar.
+ * userfullname - The name of the logged in user
+ * submenus - Array of submenus within the user menu.
+ * id - The id of the submenu.
+ * title - The title of the submenu.
+ * items - Array of the submenu items used in core/user_action_menu_submenu_items.
+
+ Example context (json):
+ {
+ "unauthenticateduser": {
+ "content": "You are not logged in",
+ "url": "https://yourmoodlesite/login/index.php"
+ },
+ "items": [],
+ "metadata": [],
+ "avatardata": [],
+ "userfullname": "Admin User",
+ "submenus": [
+ {
+ "id": "86cebd87",
+ "title": "Submenu title",
+ "items": []
+ }
+ ]
+ }
+}}
+
+{{#js}}
+ require(['core/usermenu'], function(UserMenu) {
+ UserMenu.init();
+ });
+{{/js}}
diff --git a/lib/templates/user_menu_metadata.mustache b/lib/templates/user_menu_metadata.mustache
new file mode 100644
index 00000000000..b88a0cc8720
--- /dev/null
+++ b/lib/templates/user_menu_metadata.mustache
@@ -0,0 +1,63 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/user_menu_metadata
+
+ User menu metadata template.
+
+ Context variables required for this template:
+ * metadata - Array of additional metadata to be displayed in the dropdown button.
+ * classes - Additional classes to be applied to the content
+ * content - The content to be displayed. May/may not have html within it.
+ * avatardata - Array of avatars to be displayed. Usually only the current user's avatar. If viewing as another user,
+ includes that user's avatar.
+ * classes - Additional classes to be applied to the content
+ * content - The content to be displayed. May/may not have html within it.
+ * userfullname - The name of the logged in user
+
+ Example context (json):
+ {
+ "metadata": [
+ {
+ "classes": "loginfailures",
+ "content": "1 failed login attempts"
+ }
+ ],
+ "avatardata": [
+ {
+ "classes": "current",
+ "content": ""
+ }
+ ],
+ "userfullname": "Admin User"
+ }
+}}
+
+ {{userfullname}}
+ {{#metadata}}
+
+ {{{content}}}
+
+ {{/metadata}}
+
+
+{{#avatardata}}
+
+ {{{content}}}
+
+{{/avatardata}}
+
diff --git a/lib/tests/behat/behat_navigation.php b/lib/tests/behat/behat_navigation.php
index 053010c6b0e..d71b8e96313 100644
--- a/lib/tests/behat/behat_navigation.php
+++ b/lib/tests/behat/behat_navigation.php
@@ -41,6 +41,20 @@ use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
*/
class behat_navigation extends behat_base {
+ /**
+ * Checks whether a navigation node is active within the block navigation.
+ *
+ * @Given i should see :name is active in navigation
+ *
+ * @throws ElementNotFoundException
+ * @param string $element The name of the nav elemnent to look for.
+ * @return void
+ */
+ public function i_should_see_is_active_in_navigation($element) {
+ $this->execute("behat_general::assert_element_contains_text",
+ [$element, '.block_navigation .active_tree_node', 'css_element']);
+ }
+
/**
* Helper function to get a navigation nodes text element given its text from within the navigation block.
*
@@ -151,7 +165,7 @@ class behat_navigation extends behat_base {
if ($this->running_javascript()) {
// The user menu must be expanded when JS is enabled.
- $xpath = "//div[contains(concat(' ', @class, ' '), ' usermenu ')]//a[contains(concat(' ', @class, ' '), ' dropdown-toggle ')]";
+ $xpath = "//div[contains(concat(' ', @class, ' '), ' usermenu ')]//button[contains(concat(' ', @class, ' '), ' dropdown-toggle ')]";
$this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
}
@@ -925,7 +939,14 @@ class behat_navigation extends behat_base {
if ($parentnodes) {
$tabname = behat_context_helper::escape($parentnodes[0]);
$tabxpath = '//ul[@role=\'tablist\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
- if ($node = $this->getSession()->getPage()->find('xpath', $tabxpath)) {
+ $menubarxpath = '//ul[@role=\'menubar\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
+ $linkname = behat_context_helper::escape(get_string('moremenu'));
+ $menubarmorexpath = '//ul[@role=\'menubar\']/li/a[contains(normalize-space(.), ' . $linkname . ')]';
+ $tabnode = $this->getSession()->getPage()->find('xpath', $tabxpath);
+ $menunode = $this->getSession()->getPage()->find('xpath', $menubarxpath);
+ $menubuttons = $this->getSession()->getPage()->findAll('xpath', $menubarmorexpath);
+ if ($tabnode || $menunode) {
+ $node = is_object($tabnode) ? $tabnode : $menunode;
if ($this->running_javascript()) {
$this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
// Click on the tab and add 'active' tab to the xpath.
@@ -936,6 +957,22 @@ class behat_navigation extends behat_base {
$xpath .= '//div[@id = ' . $tabid . ']';
}
array_shift($parentnodes);
+ } else if (count($menubuttons) > 0) {
+ try {
+ $menubuttons[0]->isVisible();
+ try {
+ $this->execute('behat_general::i_click_on', [$menubuttons[1], 'NodeElement']);
+ } catch (Exception $e) {
+ $this->execute('behat_general::i_click_on', [$menubuttons[0], 'NodeElement']);
+ }
+ $moreitemxpath = '//ul[@data-region=\'moredropdown\']/li/a[contains(normalize-space(.), ' . $tabname . ')]';
+ if ($morenode = $this->getSession()->getPage()->find('xpath', $moreitemxpath)) {
+ $this->execute('behat_general::i_click_on', [$morenode, 'NodeElement']);
+ $xpath .= '//div[contains(@class,\'active\')]';
+ array_shift($parentnodes);
+ }
+ } catch (Exception $e) {
+ }
}
}
@@ -1065,4 +1102,95 @@ class behat_navigation extends behat_base {
}
$this->execute('behat_general::i_visit', [$url]);
}
+
+
+ /**
+ * First checks to see if we are on this page via the breadcrumb. If not we then attempt to follow the link name given.
+ *
+ * @param string $pagename Name of the breadcrumb item to check and follow.
+ */
+ public function go_to_breadcrumb_location(string $pagename): void {
+ $link = $this->getSession()->getPage()->find(
+ 'xpath',
+ "//nav[@aria-label='Navigation bar']/ol/li[last()][contains(normalize-space(.), '" . $pagename . "')]"
+ );
+ if (!$link) {
+ $this->execute("behat_general::click_link", $pagename);
+ }
+ }
+
+ /**
+ * Checks whether an item exists in the user menu.
+ *
+ * @Given :itemtext :selectortype should exist in the user menu
+ * @Given :itemtext :selectortype should :not exist in the user menu
+ *
+ * @throws ElementNotFoundException
+ * @param string $itemtext The menu item to find
+ * @param string $selectortype The selector type
+ * @param string|null $not Instructs to checks whether the element does not exist in the user menu, if defined
+ * @return void
+ */
+ public function should_exist_in_user_menu($itemtext, $selectortype, $not = null) {
+ $callfunction = is_null($not) ? 'should_exist_in_the' : 'should_not_exist_in_the';
+ $this->execute("behat_general::{$callfunction}",
+ [$itemtext, $selectortype, $this->get_user_menu_xpath(), 'xpath_element']);
+ }
+
+ /**
+ * Checks whether an item exists in a given user submenu.
+ *
+ * @Given :itemtext :selectortype should exist in the :submenuname user submenu
+ * @Given :itemtext :selectortype should :not exist in the :submenuname user submenu
+ *
+ * @throws ElementNotFoundException
+ * @param string $itemtext The submenu item to find
+ * @param string $selectortype The selector type
+ * @param string $submenuname The name of the submenu
+ * @param string|null $not Instructs to checks whether the element does not exist in the user menu, if defined
+ * @return void
+ */
+ public function should_exist_in_user_submenu($itemtext, $selectortype, $submenuname, $not = null) {
+ $callfunction = is_null($not) ? 'should_exist_in_the' : 'should_not_exist_in_the';
+ $this->execute("behat_general::{$callfunction}",
+ [$itemtext, $selectortype, $this->get_user_submenu_xpath($submenuname), 'xpath_element']);
+ }
+
+ /**
+ * Checks whether a given user submenu is visible.
+ *
+ * @Then /^I should see "(?P[^"]*)" user submenu$/
+ *
+ * @throws ElementNotFoundException
+ * @throws ExpectationException
+ * @param string $submenuname The name of the submenu
+ * @return void
+ */
+ public function i_should_see_user_submenu($submenuname) {
+ $this->execute('behat_general::should_be_visible',
+ array($this->get_user_submenu_xpath($submenuname), 'xpath_element'));
+ }
+
+ /**
+ * Return the xpath for the user menu element.
+ *
+ * @return string The xpath
+ */
+ protected function get_user_menu_xpath() {
+ return "//div[contains(concat(' ', @class, ' '), ' usermenu ')]" .
+ "//div[contains(concat(' ', @class, ' '), ' dropdown-menu ')]" .
+ "//div[@id='carousel-item-main']";
+ }
+
+ /**
+ * Return the xpath for a given user submenu element.
+ *
+ * @param string $submenuname The name of the submenu
+ * @return string The xpath
+ */
+ protected function get_user_submenu_xpath($submenuname) {
+ return "//div[contains(concat(' ', @class, ' '), ' usermenu ')]" .
+ "//div[contains(concat(' ', @class, ' '), ' dropdown-menu ')]" .
+ "//div[contains(concat(' ', @class, ' '), ' submenu ')][@aria-label='" . $submenuname . "']";
+ }
}
diff --git a/lib/tests/navigation/output/primary_test.php b/lib/tests/navigation/output/primary_test.php
new file mode 100644
index 00000000000..2c4031b3078
--- /dev/null
+++ b/lib/tests/navigation/output/primary_test.php
@@ -0,0 +1,413 @@
+.
+
+namespace core\navigation\output;
+
+use ReflectionMethod;
+
+/**
+ * Primary navigation renderable test
+ *
+ * @package core
+ * @category navigation
+ * @copyright 2021 onwards Peter Dias
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class primary_test extends \advanced_testcase {
+ /**
+ * Basic setup to make sure the nav objects gets generated without any issues.
+ */
+ public function setUp(): void {
+ global $PAGE;
+ $this->resetAfterTest();
+ $pagecourse = $this->getDataGenerator()->create_course();
+ $assign = $this->getDataGenerator()->create_module('assign', ['course' => $pagecourse->id]);
+ $cm = get_coursemodule_from_id('assign', $assign->cmid);
+ $contextrecord = \context_module::instance($cm->id);
+ $pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->instance]);
+ $PAGE->set_cm($cm);
+ $PAGE->set_url($pageurl);
+ $PAGE->set_course($pagecourse);
+ $PAGE->set_context($contextrecord);
+ }
+
+ /**
+ * Test the primary export to confirm we are getting the nodes
+ *
+ * @dataProvider test_primary_export_provider
+ * @param bool $withcustom Setup with custom menu
+ * @param bool $withlang Setup with langs
+ * @param string $userloggedin The type of user ('admin' or 'guest') if creating setup with logged in user,
+ * otherwise consider the user as non-logged in
+ * @param array $expecteditems An array of nodes expected with content in them.
+ */
+ public function test_primary_export(bool $withcustom, bool $withlang, string $userloggedin, array $expecteditems) {
+ global $PAGE, $CFG;
+ if ($withcustom) {
+ $CFG->custommenuitems = "Course search|/course/search.php
+ Google|https://google.com.au/
+ Netflix|https://netflix.com/au";
+ }
+ if ($userloggedin === 'admin') {
+ $this->setAdminUser();
+ } else if ($userloggedin === 'guest') {
+ $this->setGuestUser();
+ } else {
+ $this->setUser(0);
+ }
+
+ // Mimic multiple langs installed. To trigger responses 'get_list_of_translations'.
+ // Note: The text/title of the nodes generated will be 'English(fr), English(de)' but we don't care about this.
+ // We are testing whether the nodes gets generated when the lang menu is available.
+ if ($withlang) {
+ mkdir("$CFG->dataroot/lang/de", 0777, true);
+ mkdir("$CFG->dataroot/lang/fr", 0777, true);
+ // Ensure the new langs are picked up and not taken from the cache.
+ $stringmanager = get_string_manager();
+ $stringmanager->reset_caches(true);
+ }
+
+ $primary = new primary($PAGE);
+ $renderer = $PAGE->get_renderer('core');
+ $data = array_filter($primary->export_for_template($renderer));
+
+ // Assert that the number of returned menu items equals the expected result.
+ $this->assertCount(count($expecteditems), $data);
+ // Assert that returned menu items match the expected items.
+ foreach ($data as $menutype => $value) {
+ $this->assertTrue(in_array($menutype, $expecteditems));
+ }
+ // When the user is logged in (excluding guest access), assert that lang menu is included as a part of the
+ // user menu when multiple languages are installed.
+ if (isloggedin() && !isguestuser()) {
+ // Look for a language menu item within the user menu items.
+ $usermenulang = array_filter($data['user']['items'], function($usermenuitem) {
+ return $usermenuitem->title === get_string('language');
+ });
+ if ($withlang) { // If multiple languages are installed.
+ // Assert that the language menu exists within the user menu.
+ $this->assertNotEmpty($usermenulang);
+ } else { // If the aren't any additional installed languages.
+ $this->assertEmpty($usermenulang);
+ }
+ } else { // Otherwise assert that the user menu does not contain any items.
+ $this->assertArrayNotHasKey('items', $data['user']);
+ }
+ }
+
+ /**
+ * Provider for the test_primary_export function.
+ *
+ * @return array
+ */
+ public function test_primary_export_provider(): array {
+ return [
+ "Export the menu data when: custom menu exists; multiple langs installed; user is not logged in." => [
+ true, true, '', ['moremenu', 'lang', 'user']
+ ],
+ "Export the menu data when: custom menu exists; langs not installed; user is not logged in." => [
+ true, false, '', ['moremenu', 'user']
+ ],
+ "Export the menu data when: custom menu exists; multiple langs installed; logged in as admin." => [
+ true, true, 'admin', ['moremenu', 'user']
+ ],
+ "Export the menu data when: custom menu exists; langs not installed; logged in as admin." => [
+ true, false, 'admin', ['moremenu', 'user']
+ ],
+ "Export the menu data when: custom menu exists; multiple langs installed; logged in as guest." => [
+ true, true, 'guest', ['moremenu', 'lang', 'user']
+ ],
+ "Export the menu data when: custom menu exists; langs not installed; logged in as guest." => [
+ true, false, 'guest', ['moremenu', 'user']
+ ],
+ "Export the menu data when: custom menu does not exist; multiple langs installed; logged in as guest." => [
+ false, true, 'guest', ['moremenu', 'lang', 'user']
+ ],
+ "Export the menu data when: custom menu does not exist; multiple langs installed; logged in as admin." => [
+ false, true, 'admin', ['moremenu', 'user']
+ ],
+ "Export the menu data when: custom menu does not exist; langs not installed; user is not logged in." => [
+ false, false, '', ['moremenu', 'user']
+ ],
+ ];
+ }
+
+ /**
+ * Test the get_lang_menu
+ *
+ * @dataProvider get_lang_menu_provider
+ * @param bool $withadditionallangs
+ * @param string $language
+ * @param array $expected
+ */
+ public function test_get_lang_menu(bool $withadditionallangs, string $language, array $expected) {
+ global $CFG, $PAGE;
+
+ // Mimic multiple langs installed. To trigger responses 'get_list_of_translations'.
+ // Note: The text/title of the nodes generated will be 'English(fr), English(de)' but we don't care about this.
+ // We are testing whether the nodes gets generated when the lang menu is available.
+ if ($withadditionallangs) {
+ mkdir("$CFG->dataroot/lang/de", 0777, true);
+ mkdir("$CFG->dataroot/lang/fr", 0777, true);
+ // Ensure the new langs are picked up and not taken from the cache.
+ $stringmanager = get_string_manager();
+ $stringmanager->reset_caches(true);
+ }
+
+ force_current_language($language);
+
+ $output = new primary($PAGE);
+ $method = new ReflectionMethod('core\navigation\output\primary', 'get_lang_menu');
+ $method->setAccessible(true);
+ $renderer = $PAGE->get_renderer('core');
+
+ $response = $method->invoke($output, $renderer);
+
+ if ($withadditionallangs) { // If there are multiple languages installed.
+ // Assert that the title of the language menu matches the expected one.
+ $this->assertEquals($expected['title'], $response['title']);
+ // Assert that the number of language menu items matches the number of the expected items.
+ $this->assertEquals(count($expected['items']), count($response['items']));
+ foreach ($expected['items'] as $expecteditem) {
+ // We need to manually generate the url key and its value in the expected item array as this cannot
+ // be done in the data provider due to the change of the state of $PAGE.
+ $expecteditem['url'] = $expecteditem['isactive'] ? new \moodle_url('#') :
+ new \moodle_url($PAGE->url, ['lang' => $expecteditem['lang']]);
+ // The lang value is only used to generate the url, so this key can be removed.
+ unset($expecteditem['lang']);
+
+ // Assert that the given expected item exists in the returned items.
+ $this->assertTrue(in_array($expecteditem, $response['items']));
+ }
+ } else { // No multiple languages.
+ $this->assertEquals($expected, $response);
+ }
+ }
+
+ /**
+ * Provider for test_get_lang_menu
+ *
+ * @return array
+ */
+ public function get_lang_menu_provider(): array {
+ return [
+ 'Lang menu with only the current language' => [
+ false, 'en', []
+ ],
+ 'Lang menu with only multiple languages installed' => [
+ true, 'en', [
+ 'title' => 'English (en)',
+ 'items' => [
+ [
+ 'title' => 'English (en)',
+ 'text' => 'English (en)',
+ 'link' => true,
+ 'isactive' => true,
+ 'lang' => 'en'
+ ],
+ [
+ 'title' => 'English (de)',
+ 'text' => 'English (de)',
+ 'link' => true,
+ 'isactive' => false,
+ 'lang' => 'de'
+ ],
+
+ [
+ 'title' => 'English (fr)',
+ 'text' => 'English (fr)',
+ 'link' => true,
+ 'isactive' => false,
+ 'lang' => 'fr'
+ ],
+ ],
+ ],
+ ],
+ 'Lang menu with only multiple languages installed and other than EN set active.' => [
+ true, 'de', [
+ 'title' => 'English (de)',
+ 'items' => [
+ [
+ 'title' => 'English (en)',
+ 'text' => 'English (en)',
+ 'link' => true,
+ 'isactive' => false,
+ 'lang' => 'en'
+ ],
+ [
+ 'title' => 'English (de)',
+ 'text' => 'English (de)',
+ 'link' => true,
+ 'isactive' => true,
+ 'lang' => 'de'
+ ],
+ [
+ 'title' => 'English (fr)',
+ 'text' => 'English (fr)',
+ 'link' => true,
+ 'isactive' => false,
+ 'lang' => 'fr'
+ ],
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Test the custom menu getter to confirm the nodes gets generated and are returned correctly.
+ *
+ * @dataProvider custom_menu_provider
+ * @param string $config
+ * @param array $expected
+ */
+ public function test_get_custom_menu(string $config, array $expected) {
+ global $CFG, $PAGE;
+ $CFG->custommenuitems = $config;
+ $output = new primary($PAGE);
+ $method = new ReflectionMethod('core\navigation\output\primary', 'get_custom_menu');
+ $method->setAccessible(true);
+ $renderer = $PAGE->get_renderer('core');
+ $this->assertEquals($expected, $method->invoke($output, $renderer));
+ }
+
+ /**
+ * Provider for test_get_custom_menu
+ *
+ * @return array
+ */
+ public function custom_menu_provider(): array {
+ return [
+ 'Simple custom menu' => [
+ "Course search|/course/search.php
+ Google|https://google.com.au/
+ Netflix|https://netflix.com/au", [
+ (object) [
+ 'text' => 'Course search',
+ 'url' => 'https://www.example.com/moodle/course/search.php',
+ 'title' => '',
+ 'sort' => 1,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ (object) [
+ 'text' => 'Google',
+ 'url' => 'https://google.com.au/',
+ 'title' => '',
+ 'sort' => 2,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ (object) [
+ 'text' => 'Netflix',
+ 'url' => 'https://netflix.com/au',
+ 'title' => '',
+ 'sort' => 3,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ ]
+ ],
+ 'Complex, nested custom menu' => [
+ "Moodle community|http://moodle.org
+ -Moodle free support|http://moodle.org/support
+ -Moodle development|http://moodle.org/development
+ --Moodle Tracker|http://tracker.moodle.org
+ --Moodle Docs|https://docs.moodle.org
+ -Moodle News|http://moodle.org/news
+ Moodle company
+ -Moodle commercial hosting|http://moodle.com/hosting
+ -Moodle commercial support|http://moodle.com/support", [
+ (object) [
+ 'text' => 'Moodle community',
+ 'url' => 'http://moodle.org',
+ 'title' => '',
+ 'sort' => 1,
+ 'children' => [
+ (object) [
+ 'text' => 'Moodle free support',
+ 'url' => 'http://moodle.org/support',
+ 'title' => '',
+ 'sort' => 2,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ (object) [
+ 'text' => 'Moodle development',
+ 'url' => 'http://moodle.org/development',
+ 'title' => '',
+ 'sort' => 3,
+ 'children' => [
+ (object) [
+ 'text' => 'Moodle Tracker',
+ 'url' => 'http://tracker.moodle.org',
+ 'title' => '',
+ 'sort' => 4,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ (object) [
+ 'text' => 'Moodle Docs',
+ 'url' => 'https://docs.moodle.org',
+ 'title' => '',
+ 'sort' => 5,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ ],
+ 'haschildren' => true,
+ ],
+ (object) [
+ 'text' => 'Moodle News',
+ 'url' => 'http://moodle.org/news',
+ 'title' => '',
+ 'sort' => 6,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ ],
+ 'haschildren' => true,
+ ],
+ (object) [
+ 'text' => 'Moodle company',
+ 'url' => null,
+ 'title' => '',
+ 'sort' => 7,
+ 'children' => [
+ (object) [
+ 'text' => 'Moodle commercial hosting',
+ 'url' => 'http://moodle.com/hosting',
+ 'title' => '',
+ 'sort' => 8,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ (object) [
+ 'text' => 'Moodle commercial support',
+ 'url' => 'http://moodle.com/support',
+ 'title' => '',
+ 'sort' => 9,
+ 'children' => [],
+ 'haschildren' => false,
+ ],
+ ],
+ 'haschildren' => true,
+ ],
+ ]
+ ]
+ ];
+ }
+}
diff --git a/lib/tests/navigation/views/primary_test.php b/lib/tests/navigation/views/primary_test.php
index 41f8e9efa9d..2da990d8afa 100644
--- a/lib/tests/navigation/views/primary_test.php
+++ b/lib/tests/navigation/views/primary_test.php
@@ -58,9 +58,9 @@ class primary_test extends \advanced_testcase {
*/
public function test_setting_initialise_provider() {
return [
- 'Testing as a guest user' => ['guest', ['home', 'courses']],
- 'Testing as an admin' => ['admin', ['home', 'myhome', 'courses', 'siteadminnode']],
- 'Testing as a regular user' => ['user', ['home', 'myhome', 'courses']]
+ 'Testing as a guest user' => ['guest', ['courses']],
+ 'Testing as an admin' => ['admin', ['myhome', 'courses', 'siteadminnode']],
+ 'Testing as a regular user' => ['user', ['myhome', 'courses']]
];
}
}
diff --git a/lib/tests/navigation/views/secondary_test.php b/lib/tests/navigation/views/secondary_test.php
index fa5ef8d85a8..fa511af14b5 100644
--- a/lib/tests/navigation/views/secondary_test.php
+++ b/lib/tests/navigation/views/secondary_test.php
@@ -106,7 +106,7 @@ class secondary_test extends \advanced_testcase {
$assign = $this->getDataGenerator()->create_module('assign', ['course' => $pagecourse->id]);
$cm = get_coursemodule_from_id('assign', $assign->cmid);
$contextrecord = \context_module::instance($cm->id);
- $pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->instance]);
+ $pageurl = new \moodle_url('/mod/assign/view.php', ['id' => $cm->id]);
$PAGE->set_cm($cm);
break;
case 'system':
@@ -135,7 +135,131 @@ class secondary_test extends \advanced_testcase {
return [
'Testing in a course context' => ['course', 'coursehome', 'courseheader', 'Course page'],
'Testing in a module context' => ['module', 'modulepage', 'activityheader', 'Activity'],
- 'Testing in a site admin' => ['system', 'siteadminnode', 'menu', 'Site administration'],
+ 'Testing in a site admin' => ['system', 'siteadminnode', 'homeheader', 'Site administration'],
+ ];
+ }
+
+ /**
+ * Test the force_nodes_into_more_menu method.
+ *
+ * @param array $secondarynavnodesdata The array which contains the data used to generate the secondary navigation
+ * @param array $defaultmoremenunodes The array containing the keys of the navigation nodes which should be added
+ * to the "more" menu by default
+ * @param array $expecedmoremenunodes The array containing the keys of the expected navigation nodes which are
+ * forced into the "more" menu
+ * @dataProvider test_force_nodes_into_more_menu_provider
+ */
+ public function test_force_nodes_into_more_menu(array $secondarynavnodesdata, array $defaultmoremenunodes,
+ array $expecedmoremenunodes) {
+ global $PAGE;
+
+ // Create a dummy secondary navigation.
+ $secondary = new secondary($PAGE);
+ foreach ($secondarynavnodesdata as $nodedata) {
+ $secondary->add($nodedata['text'], '#', secondary::TYPE_SETTING, null, $nodedata['key']);
+ }
+
+ $method = new ReflectionMethod('core\navigation\views\secondary', 'force_nodes_into_more_menu');
+ $method->setAccessible(true);
+ $method->invoke($secondary, $defaultmoremenunodes);
+
+ $actualmoremenunodes = [];
+ foreach ($secondary->children as $node) {
+ if ($node->forceintomoremenu) {
+ $actualmoremenunodes[] = $node->key;
+ }
+ }
+ // Assert that the actual nodes forced into the "more" menu matches the expected ones.
+ $this->assertEquals($expecedmoremenunodes, $actualmoremenunodes);
+ }
+
+ /**
+ * Data provider for the test_force_nodes_into_more_menu function.
+ *
+ * @return array
+ */
+ public function test_force_nodes_into_more_menu_provider(): array {
+ return [
+ 'The total number of navigation nodes exceeds the max display limit (5); ' .
+ 'navnode2 and navnode4 are forced into "more" menu by default.' =>
+ [
+ [
+ [ 'text' => 'Navigation node 1', 'key' => 'navnode1'],
+ [ 'text' => 'Navigation node 2', 'key' => 'navnode2'],
+ [ 'text' => 'Navigation node 3', 'key' => 'navnode3'],
+ [ 'text' => 'Navigation node 4', 'key' => 'navnode4'],
+ [ 'text' => 'Navigation node 5', 'key' => 'navnode5'],
+ [ 'text' => 'Navigation node 6', 'key' => 'navnode6'],
+ [ 'text' => 'Navigation node 7', 'key' => 'navnode7'],
+ [ 'text' => 'Navigation node 8', 'key' => 'navnode8'],
+ [ 'text' => 'Navigation node 9', 'key' => 'navnode9'],
+ ],
+ [
+ 'navnode2',
+ 'navnode4',
+ ],
+ [
+ 'navnode2',
+ 'navnode4',
+ 'navnode8',
+ 'navnode9',
+ ],
+ ],
+ 'The total number of navigation nodes does not exceed the max display limit (5); ' .
+ 'navnode2 and navnode4 are forced into "more" menu by default.' =>
+ [
+ [
+ [ 'text' => 'Navigation node 1', 'key' => 'navnode1'],
+ [ 'text' => 'Navigation node 2', 'key' => 'navnode2'],
+ [ 'text' => 'Navigation node 3', 'key' => 'navnode3'],
+ [ 'text' => 'Navigation node 4', 'key' => 'navnode4'],
+ [ 'text' => 'Navigation node 5', 'key' => 'navnode5'],
+ ],
+ [
+ 'navnode2',
+ 'navnode4',
+ ],
+ [
+ 'navnode2',
+ 'navnode4',
+ ],
+ ],
+ 'The total number of navigation nodes exceeds the max display limit (5); ' .
+ 'no forced navigation nodes into "more" menu by default.' =>
+ [
+ [
+ [ 'text' => 'Navigation node 1', 'key' => 'navnode1'],
+ [ 'text' => 'Navigation node 2', 'key' => 'navnode2'],
+ [ 'text' => 'Navigation node 3', 'key' => 'navnode3'],
+ [ 'text' => 'Navigation node 4', 'key' => 'navnode4'],
+ [ 'text' => 'Navigation node 5', 'key' => 'navnode5'],
+ [ 'text' => 'Navigation node 6', 'key' => 'navnode6'],
+ [ 'text' => 'Navigation node 7', 'key' => 'navnode7'],
+ [ 'text' => 'Navigation node 8', 'key' => 'navnode8'],
+ ],
+ [],
+ [
+ 'navnode6',
+ 'navnode7',
+ 'navnode8',
+ ],
+ ],
+ 'The total number of navigation nodes does not exceed the max display limit (5); ' .
+ 'no forced navigation nodes into "more" menu by default.' =>
+ [
+ [
+ [ 'text' => 'Navigation node 1', 'key' => 'navnode1'],
+ [ 'text' => 'Navigation node 2', 'key' => 'navnode2'],
+ [ 'text' => 'Navigation node 3', 'key' => 'navnode3'],
+ [ 'text' => 'Navigation node 4', 'key' => 'navnode4'],
+ [ 'text' => 'Navigation node 5', 'key' => 'navnode5'],
+ [ 'text' => 'Navigation node 6', 'key' => 'navnode6'],
+ ],
+ [],
+ [
+ 'navnode6',
+ ],
+ ],
];
}
}
diff --git a/lib/tests/navigationlib_test.php b/lib/tests/navigationlib_test.php
index 0848ee8effa..47dd2eabde6 100644
--- a/lib/tests/navigationlib_test.php
+++ b/lib/tests/navigationlib_test.php
@@ -545,6 +545,58 @@ class core_navigationlib_testcase extends advanced_testcase {
// Test it's empty again!
$this->assertEquals(0, count($navigationnodecollection->get_key_list()));
}
+
+ /**
+ * Test the set_force_into_more_menu method.
+ *
+ * @param bool $haschildren Whether the navigation node has children nodes
+ * @param bool $forceintomoremenu Whether to force the navigation node and its children into the "more" menu
+ * @dataProvider test_set_force_into_more_menu_provider
+ */
+ public function test_set_force_into_more_menu(bool $haschildren, bool $forceintomoremenu) {
+ // Create a navigation node.
+ $node = new navigation_node(['text' => 'Navigation node', 'key' => 'navnode']);
+
+ // If required, add some children nodes to the navigation node.
+ if ($haschildren) {
+ for ($i = 1; $i <= 3; $i++) {
+ $node->add("Child navigation node {$i}");
+ }
+ }
+
+ $node->set_force_into_more_menu($forceintomoremenu);
+ // Assert that the expected value has been assigned to the 'forceintomoremenu' property
+ // in the navigation node and its children.
+ $this->assertEquals($forceintomoremenu, $node->forceintomoremenu);
+ foreach ($node->children as $child) {
+ $this->assertEquals($forceintomoremenu, $child->forceintomoremenu);
+ }
+ }
+
+ /**
+ * Data provider for the test_set_force_into_more_menu function.
+ *
+ * @return array
+ */
+ public function test_set_force_into_more_menu_provider(): array {
+ return [
+ 'Navigation node without any children nodes; Force into "more" menu => true.' =>
+ [
+ false,
+ true,
+ ],
+ 'Navigation node with children nodes; Force into "more" menu => true.' =>
+ [
+ true,
+ true,
+ ],
+ 'Navigation node with children nodes; Force into "more" menu => false.' =>
+ [
+ true,
+ false,
+ ],
+ ];
+ }
}
diff --git a/mod/assign/classes/local/views/secondary.php b/mod/assign/classes/local/views/secondary.php
new file mode 100644
index 00000000000..d78577c9c7c
--- /dev/null
+++ b/mod/assign/classes/local/views/secondary.php
@@ -0,0 +1,44 @@
+.
+
+namespace mod_assign\local\views;
+
+use core\navigation\views\secondary as core_secondary;
+
+/**
+ * Class secondary_navigation_view.
+ *
+ * Custom implementation for a plugin.
+ *
+ * @package mod_assign
+ * @category navigation
+ * @copyright 2021 onwards Peter Dias
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class secondary extends core_secondary {
+ protected function get_default_module_mapping(): array {
+ return [
+ self::TYPE_SETTING => [
+ 'modedit' => 1,
+ "mod_{$this->page->activityname}_useroverrides" => 2, // Overrides are module specific.
+ "mod_{$this->page->activityname}_groupoverrides" => 3,
+ ],
+ self::TYPE_CUSTOM => [
+ 'advgrading' => 4,
+ ],
+ ];
+ }
+}
diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php
index d35131f5091..05c71bcbe5a 100644
--- a/mod/assign/locallib.php
+++ b/mod/assign/locallib.php
@@ -4564,18 +4564,19 @@ class assign {
$gradingoptionsform->set_data($gradingoptionsdata);
$actionformtext = $this->get_renderer()->render($gradingactions);
+
+ $currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']);
+
$header = new assign_header($this->get_instance(),
$this->get_context(),
false,
$this->get_course_module()->id,
get_string('grading', 'assign'),
- $actionformtext);
+ $actionformtext,
+ '',
+ $currenturl);
$o .= $this->get_renderer()->render($header);
- $currenturl = $CFG->wwwroot .
- '/mod/assign/view.php?id=' .
- $this->get_course_module()->id .
- '&action=grading';
$o .= groups_print_activity_menu($this->get_course_module(), $currenturl, true);
diff --git a/mod/assign/renderable.php b/mod/assign/renderable.php
index 936be1649af..295fc3c93f0 100644
--- a/mod/assign/renderable.php
+++ b/mod/assign/renderable.php
@@ -671,6 +671,8 @@ class assign_header implements renderable {
public $preface = '';
/** @var string $postfix optional postfix (text to show after the intro) */
public $postfix = '';
+ /** @var moodle_url $subpageurl link for the subpage */
+ public $subpageurl = null;
/**
* Constructor
@@ -688,7 +690,8 @@ class assign_header implements renderable {
$coursemoduleid,
$subpage='',
$preface='',
- $postfix='') {
+ $postfix='',
+ moodle_url $subpageurl = null) {
$this->assign = $assign;
$this->context = $context;
$this->showintro = $showintro;
@@ -696,6 +699,7 @@ class assign_header implements renderable {
$this->subpage = $subpage;
$this->preface = $preface;
$this->postfix = $postfix;
+ $this->subpageurl = $subpageurl;
}
}
diff --git a/mod/assign/renderer.php b/mod/assign/renderer.php
index 19476ed5a6c..f32e978ff64 100644
--- a/mod/assign/renderer.php
+++ b/mod/assign/renderer.php
@@ -233,7 +233,7 @@ class mod_assign_renderer extends plugin_renderer_base {
$o = '';
if ($header->subpage) {
- $this->page->navbar->add($header->subpage);
+ $this->page->navbar->add($header->subpage, $header->subpageurl);
$args = ['contextname' => $header->context->get_context_name(false, true), 'subpage' => $header->subpage];
$title = get_string('subpagetitle', 'assign', $args);
} else {
diff --git a/mod/assign/version.php b/mod/assign/version.php
index 86058277cd8..08be89e031e 100644
--- a/mod/assign/version.php
+++ b/mod/assign/version.php
@@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version = 2021052502; // The current module version (Date: YYYYMMDDXX).
+$plugin->version = 2021052503; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2021052500; // Requires this Moodle version.
diff --git a/mod/book/tests/behat/reorganize_chapters.feature b/mod/book/tests/behat/reorganize_chapters.feature
index be0c0883248..4a4601b4921 100644
--- a/mod/book/tests/behat/reorganize_chapters.feature
+++ b/mod/book/tests/behat/reorganize_chapters.feature
@@ -51,7 +51,8 @@ Feature: In a book, chapters and subchapters can be rearranged
Scenario: Moving chapters down rearranges them properly
Given I click on "Move chapter down \"1. Originally first chapter\"" "link"
- When I follow "Test book"
+ When I am on "Course 1" course homepage
+ And I follow "Test book"
Then I should see "1. A great second chapter"
And I should see "#2 chapter content"
And I should see "1.1. Second chapter, subchapter 1"
@@ -61,7 +62,8 @@ Feature: In a book, chapters and subchapters can be rearranged
Scenario: Moving chapters up rearranges them properly
Given I click on "Move chapter up \"3. There aren't 2 without 3\"" "link"
- When I follow "Test book"
+ When I am on "Course 1" course homepage
+ And I follow "Test book"
Then I should see "1. Originally first chapter"
And I should see "#1 chapter content"
And I should see "2. There aren't 2 without 3"
@@ -71,30 +73,26 @@ Feature: In a book, chapters and subchapters can be rearranged
Scenario: Moving subchapters down within chapter rearranges them properly
Given I click on "Move chapter down \"2.1. Second chapter, subchapter 1\"" "link"
- When I follow "Test book"
- Then I should see "2.1. Second chapter, subchapter 2"
- And I should see "2.2. Second chapter, subchapter 1"
+ When I should see "2.1. Second chapter, subchapter 2"
+ Then I should see "2.2. Second chapter, subchapter 1"
Scenario: Moving subchapters down out of chapter rearranges them properly
Given I click on "Move chapter down \"2.2. Second chapter, subchapter 2\"" "link"
- When I follow "Test book"
- Then I should see "3.1. Second chapter, subchapter 2"
- And I click on "Move chapter down \"3. There aren't 2 without 3\"" "link"
+ When I should see "3.1. Second chapter, subchapter 2"
+ Then I click on "Move chapter down \"3. There aren't 2 without 3\"" "link"
And I should not see "4. There aren't 2 without 3"
And I should see "3. There aren't 2 without 3"
And I should see "3.1. Second chapter, subchapter 2"
Scenario: Moving subchapters up within chapter rearranges them properly
Given I click on "Move chapter up \"2.2. Second chapter, subchapter 2\"" "link"
- When I follow "Test book"
- Then I should see "2.1. Second chapter, subchapter 2"
- And I should see "2.2. Second chapter, subchapter 1"
+ When I should see "2.1. Second chapter, subchapter 2"
+ Then I should see "2.2. Second chapter, subchapter 1"
Scenario: Moving subchapters up out of chapter rearranges them properly
Given I click on "Move chapter up \"2.1. Second chapter, subchapter 1\"" "link"
- When I follow "Test book"
- Then I should see "1.1. Second chapter, subchapter 1"
- And I click on "Move chapter up \"1.1. Second chapter, subchapter 1\"" "link"
+ When I should see "1.1. Second chapter, subchapter 1"
+ Then I click on "Move chapter up \"1.1. Second chapter, subchapter 1\"" "link"
And I should not see "1.1. Second chapter, subchapter 1"
And I should see "1. Second chapter, subchapter 1"
And I should see "2. Originally first chapter"
diff --git a/mod/choice/tests/behat/behat_mod_choice.php b/mod/choice/tests/behat/behat_mod_choice.php
index 2ba915656b7..f002f87276f 100644
--- a/mod/choice/tests/behat/behat_mod_choice.php
+++ b/mod/choice/tests/behat/behat_mod_choice.php
@@ -46,7 +46,7 @@ class behat_mod_choice extends behat_base {
* @return array
*/
public function I_choose_option_from_activity($option, $choiceactivity) {
- $this->execute("behat_navigation::i_am_on_page_instance", [$this->escape($choiceactivity), 'choice activity']);
+ $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($choiceactivity), 'choice activity']);
$this->execute('behat_forms::i_set_the_field_to', array( $this->escape($option), 1));
diff --git a/mod/feedback/tests/behat/templates.feature b/mod/feedback/tests/behat/templates.feature
index 93bc0af7efe..dfa0f6d4b84 100644
--- a/mod/feedback/tests/behat/templates.feature
+++ b/mod/feedback/tests/behat/templates.feature
@@ -57,7 +57,6 @@ Feature: Saving, using and deleting feedback templates
And I press "Use this template"
And I should see "this is a multiple choice 1"
And I press "Save changes"
- And I follow "Edit questions"
And I should see "this is a multiple choice 1"
# Make sure this template is not available in another course
And I am on the "Learning experience course 2" "feedback activity" page
@@ -85,7 +84,6 @@ Feature: Saving, using and deleting feedback templates
And I press "Use this template"
And I set the field "Append new items" to "1"
And I press "Save changes"
- And I follow "Edit questions"
Then "What is your favourite subject" "text" should appear before "this is a multiple choice 1" "text"
# Import template replacing items
And I follow "Templates"
@@ -93,6 +91,7 @@ Feature: Saving, using and deleting feedback templates
And I press "Use this template"
And I set the field "Delete old items" to "1"
And I press "Save changes"
+ And I am on the "Another feedback in course 1" "feedback activity" page
And I follow "Edit questions"
And I should not see "What is your favourite subject"
And I should see "this is a multiple choice 1"
@@ -110,7 +109,6 @@ Feature: Saving, using and deleting feedback templates
And I press "Use this template"
Then I should see "this is a multiple choice 1"
And I press "Save changes"
- And I follow "Edit questions"
And I should see "this is a multiple choice 1"
Scenario: Teacher can delete course templates but can not delete public templates
diff --git a/mod/forum/search.php b/mod/forum/search.php
index 8bd1bb3f7a3..f72e33b56ac 100644
--- a/mod/forum/search.php
+++ b/mod/forum/search.php
@@ -139,8 +139,10 @@ $strpage = get_string("page");
if (!$search || $showform) {
- $PAGE->navbar->add($strforums, new moodle_url('/mod/forum/index.php', array('id'=>$course->id)));
- $PAGE->navbar->add(get_string('advancedsearch', 'forum'));
+ $url = new moodle_url('/mod/forum/index.php', array('id' => $course->id));
+ $PAGE->navbar->add($strforums, $url);
+ $url = new moodle_url('/mod/forum/search.php', array('id' => $course->id));
+ $PAGE->navbar->add(get_string('advancedsearch', 'forum'), $url);
$PAGE->set_title($strsearch);
$PAGE->set_heading($course->fullname);
diff --git a/mod/forum/subscribers.php b/mod/forum/subscribers.php
index 55665799ae9..2e62c44df5a 100644
--- a/mod/forum/subscribers.php
+++ b/mod/forum/subscribers.php
@@ -97,7 +97,7 @@ if (data_submitted()) {
}
$strsubscribers = get_string("subscribers", "forum");
-$PAGE->navbar->add($strsubscribers);
+$PAGE->navbar->add($strsubscribers, $url);
$PAGE->set_title($strsubscribers);
$PAGE->set_heading($COURSE->fullname);
if (has_capability('mod/forum:managesubscriptions', $context) && \mod_forum\subscriptions::is_forcesubscribed($forum) === false) {
diff --git a/mod/forum/tests/behat/behat_mod_forum.php b/mod/forum/tests/behat/behat_mod_forum.php
index f6025b6ec62..f0244dea444 100644
--- a/mod/forum/tests/behat/behat_mod_forum.php
+++ b/mod/forum/tests/behat/behat_mod_forum.php
@@ -113,7 +113,7 @@ class behat_mod_forum extends behat_base {
*/
public function i_reply_post_from_forum_using_an_inpage_reply_with($postsubject, $forumname, TableNode $table) {
// Navigate to forum.
- $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
+ $this->execute("behat_navigation::go_to_breadcrumb_location", $this->escape($forumname));
$this->execute('behat_general::click_link', $this->escape($postsubject));
$this->execute('behat_general::click_link', get_string('reply', 'forum'));
@@ -132,7 +132,7 @@ class behat_mod_forum extends behat_base {
*/
public function i_navigate_to_post_in_forum($postsubject, $forumname) {
// Navigate to forum discussion.
- $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
+ $this->execute("behat_navigation::go_to_breadcrumb_location", $this->escape($forumname));
$this->execute('behat_general::click_link', $this->escape($postsubject));
}
@@ -493,7 +493,7 @@ class behat_mod_forum extends behat_base {
*/
protected function add_new_discussion_inline($forumname, TableNode $table, $buttonstr) {
// Navigate to forum.
- $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
+ $this->execute("behat_navigation::go_to_breadcrumb_location", $this->escape($forumname));
$this->execute('behat_general::click_link', $buttonstr);
$this->fill_new_discussion_form($table);
}
diff --git a/mod/forum/tests/behat/discussion_subscriptions.feature b/mod/forum/tests/behat/discussion_subscriptions.feature
index 8bf50661194..fc8e8ba3a67 100644
--- a/mod/forum/tests/behat/discussion_subscriptions.feature
+++ b/mod/forum/tests/behat/discussion_subscriptions.feature
@@ -341,7 +341,6 @@ Feature: A user can control their own subscription preferences for a discussion
And I follow "You are not subscribed to this discussion. Click to subscribe"
And I should see "Student One will be notified of new posts in 'Test post subject one' of 'Test forum name'"
And "Unsubscribe from this discussion" "checkbox" should exist in the "Test post subject one" "table_row"
- And I follow "Test forum name"
And I navigate to "Subscribe to this forum" in current page administration
And I should see "Student One will be notified of new posts in 'Test forum name'"
And "Unsubscribe from this forum" "link" should exist in current page administration
diff --git a/mod/forum/tests/behat/edit_post_teacher.feature b/mod/forum/tests/behat/edit_post_teacher.feature
index 4875a8d648c..6a9ec280d7e 100644
--- a/mod/forum/tests/behat/edit_post_teacher.feature
+++ b/mod/forum/tests/behat/edit_post_teacher.feature
@@ -57,6 +57,7 @@ Feature: Teachers can edit or delete any forum post
And I should see "Edited by Teacher 1 - original submission"
Scenario: A student can't edit or delete another user's posts
- When I follow "Teacher post subject"
+ When I follow "Test forum name"
+ And I follow "Teacher post subject"
Then I should not see "Edit" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Teacher post subject')]" "xpath_element"
And I should not see "Delete" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' forumpost ')][contains(., 'Teacher post subject')]" "xpath_element"
diff --git a/mod/forum/tests/behat/private_replies.feature b/mod/forum/tests/behat/private_replies.feature
index eb2d8eda5ab..364dc3d7fd6 100644
--- a/mod/forum/tests/behat/private_replies.feature
+++ b/mod/forum/tests/behat/private_replies.feature
@@ -36,7 +36,8 @@ Feature: Forum posts can be replied to in private
| Reply privately | 1 |
Scenario: As a teacher I can see my own response
- Given I follow "Answers to the homework"
+ Given I follow "Study discussions"
+ And I follow "Answers to the homework"
Then I should see "How about you and I have a meeting after class about plagiarism?"
Scenario: As a fellow teacher I can see the other teacher's response
diff --git a/mod/glossary/tests/behat/behat_mod_glossary.php b/mod/glossary/tests/behat/behat_mod_glossary.php
index bf84100a699..9ff7c2afb12 100644
--- a/mod/glossary/tests/behat/behat_mod_glossary.php
+++ b/mod/glossary/tests/behat/behat_mod_glossary.php
@@ -61,7 +61,7 @@ class behat_mod_glossary extends behat_base {
*/
public function i_add_a_glossary_entries_category_named($categoryname) {
- $this->execute("behat_general::click_link", get_string('categoryview', 'mod_glossary'));
+ $this->execute("behat_navigation::go_to_breadcrumb_location", get_string('categoryview', 'mod_glossary'));
$this->execute("behat_forms::press_button", get_string('editcategories', 'mod_glossary'));
diff --git a/mod/glossary/tests/behat/entries_require_approval.feature b/mod/glossary/tests/behat/entries_require_approval.feature
index cc04cb760a2..f45b3a7e45a 100644
--- a/mod/glossary/tests/behat/entries_require_approval.feature
+++ b/mod/glossary/tests/behat/entries_require_approval.feature
@@ -43,7 +43,7 @@ Feature: A teacher can choose whether glossary entries require approval
And I follow "Waiting approval"
Then I should see "(this entry is currently hidden)"
And I follow "Approve"
- And I click on "Test glossary name" "link" in the "page-header" "region"
+ And I am on the "Test glossary name" "glossary activity" page
Then I should see "Concept definition"
And I log out
# Check that the entry can now be viewed by students.
diff --git a/mod/lesson/tests/behat/lesson_student_resume.feature b/mod/lesson/tests/behat/lesson_student_resume.feature
index e3d8c4185e0..8e207dfedcb 100644
--- a/mod/lesson/tests/behat/lesson_student_resume.feature
+++ b/mod/lesson/tests/behat/lesson_student_resume.feature
@@ -89,6 +89,7 @@ Feature: In a lesson activity a student should
And I should see "Second page contents"
And I press "Next page"
And I should see "Third page contents"
+ And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -98,6 +99,7 @@ Feature: In a lesson activity a student should
And I wait "1" seconds
And I press "Next page"
And I should see "Paper is made from trees."
+ And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -108,6 +110,7 @@ Feature: In a lesson activity a student should
And I press "Submit"
And I press "Continue"
And I should see "Kermit is a frog"
+ And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -165,6 +168,7 @@ Feature: In a lesson activity a student should
And I wait "1" seconds
And I press "Next page"
And I should see "Third page contents"
+ And I am on "Course 1" course homepage
And I follow "Test lesson name"
Then I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -286,6 +290,7 @@ Feature: In a lesson activity a student should
And I press "Submit"
And I press "Continue"
And I should see "2+2=4"
+ And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
Then I should see "Do you want to start at the last page you saw?"
@@ -298,6 +303,7 @@ Feature: In a lesson activity a student should
And I press "Submit"
And I press "Continue"
And I should see "Second content page"
+ And I am on "Course 1" course homepage
And I follow "Test lesson name"
And I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
@@ -399,6 +405,7 @@ Feature: In a lesson activity a student should
And I press "Submit"
And I press "Continue"
And I should see "2+2=4"
+ And I am on "Course 1" course homepage
And I follow "Test lesson name"
Then I should see "You have seen more than one page of this lesson already."
And I should see "Do you want to start at the last page you saw?"
diff --git a/mod/lti/tests/behat/backup_restore.feature b/mod/lti/tests/behat/backup_restore.feature
index 7ae20306771..9da97b66c9d 100644
--- a/mod/lti/tests/behat/backup_restore.feature
+++ b/mod/lti/tests/behat/backup_restore.feature
@@ -27,13 +27,12 @@ Feature: Restoring Moodle 2 backup restores LTI configuration
And I navigate to "Plugins > Activity modules > External tool > Manage tools" in site administration
And "This tool has not yet been used" "text" should exist in the "//div[contains(@id,'tool-card-container') and contains(., 'My site tool')]" "xpath_element"
And I am on site homepage
- And I follow "Course 1"
+ And I am on "Course 1" course homepage
And I turn editing mode on
And I add a "External tool" to section "1" and I fill the form with:
| Activity name | My LTI module |
| Preconfigured tool | My site tool |
| Launch container | Embed |
- And I follow "Course 1"
And I should see "My LTI module"
And I backup "Course 1" course using this options:
| Confirmation | Filename | test_backup.mbz |
diff --git a/mod/quiz/tests/behat/quiz_activity_completion.feature b/mod/quiz/tests/behat/quiz_activity_completion.feature
index 6879206e7af..2f6c097b826 100644
--- a/mod/quiz/tests/behat/quiz_activity_completion.feature
+++ b/mod/quiz/tests/behat/quiz_activity_completion.feature
@@ -54,6 +54,7 @@ Feature: View activity completion in the quiz activity
And user "student1" has attempted "Test quiz name" with responses:
| slot | response |
| 1 | False |
+ And I am on "Course 1" course homepage
And I follow "Test quiz name"
And the "View" completion condition of "Test quiz name" is displayed as "done"
And the "Make attempts: 1" completion condition of "Test quiz name" is displayed as "done"
diff --git a/mod/workshop/allocation.php b/mod/workshop/allocation.php
index 5901bcff703..ebc5abaff02 100644
--- a/mod/workshop/allocation.php
+++ b/mod/workshop/allocation.php
@@ -46,7 +46,7 @@ require_capability('mod/workshop:allocate', $context);
$PAGE->set_title($workshop->name);
$PAGE->set_heading($course->fullname);
-$PAGE->navbar->add(get_string('allocation', 'workshop'));
+$PAGE->navbar->add(get_string('allocation', 'workshop'), $workshop->allocation_url($method));
$allocator = $workshop->allocator_instance($method);
$initresult = $allocator->init();
diff --git a/mod/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php b/mod/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php
index 4ad7fd94bf1..bc33974f6b4 100644
--- a/mod/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php
+++ b/mod/workshop/allocation/manual/tests/behat/behat_workshopallocation_manual.php
@@ -87,7 +87,7 @@ class behat_workshopallocation_manual extends behat_base {
* @param TableNode $table should have one column with title 'Reviewer' and another with title 'Participant' (or 'Reviewee')
*/
public function i_allocate_submissions_in_workshop_as($workshopname, TableNode $table) {
- $this->execute("behat_general::i_click_on_in_the", [$this->escape($workshopname), 'link', 'page', 'region']);
+ $this->execute("behat_navigation::go_to_breadcrumb_location", $workshopname);
$this->execute('behat_navigation::i_navigate_to_in_current_page_administration', get_string('allocate', 'workshop'));
$rows = $table->getRows();
$reviewer = $participant = null;
diff --git a/mod/workshop/tests/behat/behat_mod_workshop.php b/mod/workshop/tests/behat/behat_mod_workshop.php
index 734bb8c49f3..ca2f6e4f88f 100644
--- a/mod/workshop/tests/behat/behat_mod_workshop.php
+++ b/mod/workshop/tests/behat/behat_mod_workshop.php
@@ -52,7 +52,7 @@ class behat_mod_workshop extends behat_base {
$xpath = "//*[@class='userplan']/descendant::div[./span[contains(.,$phaseliteral)]]";
$continue = $this->escape(get_string('continue'));
- $this->execute("behat_general::i_click_on_in_the", [$this->escape($workshopname), 'link', 'page', 'region']);
+ $this->execute("behat_navigation::go_to_breadcrumb_location", $workshopname);
$this->execute('behat_general::i_click_on_in_the',
array('a.action-icon', "css_element", $this->escape($xpath), "xpath_element")
@@ -73,7 +73,7 @@ class behat_mod_workshop extends behat_base {
$savechanges = $this->escape(get_string('savechanges'));
$xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' ownsubmission ')]/descendant::*[@type='submit']";
- $this->execute("behat_navigation::i_am_on_page_instance", [$this->escape($workshopname), 'workshop activity']);
+ $this->execute("behat_navigation::i_am_on_page_instance", [$workshopname, 'workshop activity']);
$this->execute("behat_general::i_click_on", array($xpath, "xpath_element"));
@@ -116,7 +116,7 @@ class behat_mod_workshop extends behat_base {
$assess = $this->escape(get_string('assess', 'workshop'));
$saveandclose = $this->escape(get_string('saveandclose', 'workshop'));
- $this->execute("behat_navigation::i_am_on_page_instance", [$workshopname, 'workshop activity']);
+ $this->execute("behat_navigation::go_to_breadcrumb_location", $workshopname);
$this->execute('behat_general::i_click_on_in_the',
array($assess, "button", $xpath, "xpath_element")
diff --git a/pix/i/arrow-left.svg b/pix/i/arrow-left.svg
new file mode 100644
index 00000000000..a5058fc73d3
--- /dev/null
+++ b/pix/i/arrow-left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/pix/i/arrow-right.svg b/pix/i/arrow-right.svg
new file mode 100644
index 00000000000..939b57c5b54
--- /dev/null
+++ b/pix/i/arrow-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/report/outline/tests/behat/user.feature b/report/outline/tests/behat/user.feature
index 41de4ce28a1..dbeac31bf9f 100644
--- a/report/outline/tests/behat/user.feature
+++ b/report/outline/tests/behat/user.feature
@@ -35,14 +35,14 @@ Feature: View the user page for the outline report
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "Folder name"
- And I follow "Folder name"
- And I follow "Folder name"
- And I follow "Folder name"
- And I am on "Course 1" course homepage
+ And I reload the page
+ And I reload the page
+ And I reload the page
# We want to view this multiple times, to make sure the count is working.
+ And I am on "Course 1" course homepage
And I follow "URL name"
- And I follow "URL name"
- And I follow "URL name"
+ And I reload the page
+ And I reload the page
And I follow "Profile" in the user menu
And I click on "Course 1" "link" in the "region-main" "region"
When I follow "Outline report"
@@ -63,14 +63,14 @@ Feature: View the user page for the outline report
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "Folder name"
- And I follow "Folder name"
- And I follow "Folder name"
- And I follow "Folder name"
+ And I reload the page
+ And I reload the page
+ And I reload the page
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "URL name"
- And I follow "URL name"
- And I follow "URL name"
+ And I reload the page
+ And I reload the page
And I follow "Profile" in the user menu
And I click on "Course 1" "link" in the "region-main" "region"
When I follow "Outline report"
@@ -93,14 +93,14 @@ Feature: View the user page for the outline report
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "Folder name"
- And I follow "Folder name"
- And I follow "Folder name"
- And I follow "Folder name"
+ And I reload the page
+ And I reload the page
+ And I reload the page
And I am on "Course 1" course homepage
# We want to view this multiple times, to make sure the count is working.
And I follow "URL name"
- And I follow "URL name"
- And I follow "URL name"
+ And I reload the page
+ And I reload the page
And I follow "Profile" in the user menu
And I click on "Course 1" "link" in the "region-main" "region"
When I follow "Outline report"
diff --git a/tag/index.php b/tag/index.php
index b0373168bd6..a5857b19b00 100644
--- a/tag/index.php
+++ b/tag/index.php
@@ -78,7 +78,8 @@ if ($ctx && ($context = context::instance_by_id($ctx, IGNORE_MISSING)) && $conte
$tagcollid = $tag->tagcollid;
-$PAGE->set_url($tag->get_view_url($exclusivemode, $fromctx, $ctx, $rec));
+$pageurl = $tag->get_view_url($exclusivemode, $fromctx, $ctx, $rec);
+$PAGE->set_url($pageurl);
$PAGE->set_subpage($tag->id);
$tagnode = $PAGE->navigation->find('tags', null);
$tagnode->make_active();
@@ -97,7 +98,7 @@ if ($PAGE->user_allowed_editing()) {
$buttons .= $OUTPUT->edit_button(clone($PAGE->url));
}
-$PAGE->navbar->add($tagname);
+$PAGE->navbar->add($tagname, $pageurl);
$PAGE->set_title(get_string('tag', 'tag') .' - '. $tag->get_display_name());
$PAGE->set_heading($COURSE->fullname);
$PAGE->set_button($buttons);
diff --git a/tag/tests/behat/collections.feature b/tag/tests/behat/collections.feature
index 50bdd8bf9cb..a1deb030b3e 100644
--- a/tag/tests/behat/collections.feature
+++ b/tag/tests/behat/collections.feature
@@ -86,7 +86,7 @@ Feature: Managers can create and manage tag collections
And I should see "Tag3"
And I should not see "Tag1"
And I should not see "Tag2"
- And I follow "Manage tags"
+ And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
# Tag "Swimming" was not standard and was moved completely.
And I should not see "Swimming"
diff --git a/tag/tests/behat/delete_tag.feature b/tag/tests/behat/delete_tag.feature
index 18eb048f16a..b29b1469ac8 100644
--- a/tag/tests/behat/delete_tag.feature
+++ b/tag/tests/behat/delete_tag.feature
@@ -61,6 +61,7 @@ Feature: Manager is able to delete tags
And I press "Yes"
And I should see "Tag(s) deleted"
And I should not see "Dog"
+ And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should not see "Dog"
And I follow "Cat"
@@ -94,6 +95,7 @@ Feature: Manager is able to delete tags
And I should see "Tag(s) deleted"
And I should not see "Dog"
And I should not see "Neverusedtag"
+ And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should not see "Dog"
And I should not see "Neverusedtag"
diff --git a/tag/tests/behat/edit_tag.feature b/tag/tests/behat/edit_tag.feature
index c3bc35e51ec..21fd143a93e 100644
--- a/tag/tests/behat/edit_tag.feature
+++ b/tag/tests/behat/edit_tag.feature
@@ -123,7 +123,6 @@ Feature: Users can edit tags to add description or rename
| Related tags | Dog, Turtle,Fish |
| Standard | 0 |
And I press "Update"
- Then "Default collection" "link" should exist in the ".breadcrumb" "css_element"
And I follow "Kitten"
And "Description of tag 1" "text" should exist in the ".tag-description" "css_element"
And I should see "Related tags:" in the ".tag_list" "css_element"
@@ -143,12 +142,10 @@ Feature: Users can edit tags to add description or rename
And I set the following fields to these values:
| Tag name | Kitten |
And I press "Update"
- Then "Default collection" "text" should exist in the ".breadcrumb" "css_element"
And I click on "Edit this tag" "link" in the "Kitten" "table_row"
And I set the following fields to these values:
| Tag name | KITTEN |
And I press "Update"
- And "Default collection" "text" should exist in the ".breadcrumb" "css_element"
And I should see "KITTEN"
And I should not see "Kitten"
@@ -161,6 +158,7 @@ Feature: Users can edit tags to add description or rename
And I set the field "Edit tag name" in the "Cat" "table_row" to "Kitten"
Then I should not see "Cat"
And "New name for tag" "field" should not exist
+ And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should see "Kitten"
And I should not see "Cat"
@@ -172,6 +170,7 @@ Feature: Users can edit tags to add description or rename
And I should see "Turtle"
And I should see "Dog"
And I should not see "DOG"
+ And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should see "Turtle"
And I should see "Dog"
@@ -183,6 +182,7 @@ Feature: Users can edit tags to add description or rename
And "New name for tag" "field" should not exist
And I should see "Turtle"
And I should not see "Penguin"
+ And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And I should see "Turtle"
And I should not see "Penguin"
diff --git a/tag/tests/behat/flag_tags.feature b/tag/tests/behat/flag_tags.feature
index 7eda63891eb..b9fade17cf9 100644
--- a/tag/tests/behat/flag_tags.feature
+++ b/tag/tests/behat/flag_tags.feature
@@ -78,6 +78,7 @@ Feature: Users can flag tags and manager can reset flags
And "(1)" "text" should exist in the "//tr[contains(.,'Nicetag')]//td[contains(@class,'col-flag')]" "xpath_element"
And "(" "text" should not exist in the "//tr[contains(.,'Badtag')]//td[contains(@class,'col-flag')]" "xpath_element"
And "(" "text" should not exist in the "//tr[contains(.,'Neverusedtag')]//td[contains(@class,'col-flag')]" "xpath_element"
+ And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And "Nicetag" "link" should appear before "Sweartag" "link"
And "Sweartag" "link" should appear before "Badtag" "link"
diff --git a/tag/tests/behat/standard_tags.feature b/tag/tests/behat/standard_tags.feature
index ef06919ff1a..dbc7e98bb0f 100644
--- a/tag/tests/behat/standard_tags.feature
+++ b/tag/tests/behat/standard_tags.feature
@@ -63,6 +63,7 @@ Feature: Manager can add standard tags and change the tag type of existing tags
And "Remove from standard tags" "link" should exist in the "Tag1" "table_row"
And "Make standard" "link" should exist in the "Tag2" "table_row"
And "Make standard" "link" should exist in the "Tag3" "table_row"
+ And I navigate to "Appearance > Manage tags" in site administration
And I follow "Default collection"
And "Make standard" "link" should exist in the "Tag0" "table_row"
And "Remove from standard tags" "link" should exist in the "Tag1" "table_row"
diff --git a/theme/boost/classes/boostnavbar.php b/theme/boost/classes/boostnavbar.php
new file mode 100644
index 00000000000..30dc2f95262
--- /dev/null
+++ b/theme/boost/classes/boostnavbar.php
@@ -0,0 +1,180 @@
+.
+
+namespace theme_boost;
+
+/**
+ * Creates a navbar for boost that allows easy control of the navbar items.
+ *
+ * @package theme_boost
+ * @copyright 2021 Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class boostnavbar implements \renderable {
+
+ /** @var array The individual items of the navbar. */
+ protected $items = [];
+
+ /**
+ * Takes a navbar object and picks the necessary parts for display.
+ *
+ * @param \navbar $navbar The navigation bar.
+ */
+ public function __construct(\navbar $navbar) {
+ foreach ($navbar->get_items() as $item) {
+ $this->items[] = $item;
+ }
+ $this->prepare_nodes_for_boost();
+ }
+
+ /**
+ * Prepares the navigation nodes for use with boost.
+ */
+ protected function prepare_nodes_for_boost(): void {
+ // Don't display the navbar if we are in the site navigation.
+ if (!is_null($this->get_item('root'))) {
+ $this->clear_items();
+ return;
+ }
+
+ $this->remove('myhome'); // Dashboard.
+ $this->remove('home');
+
+ // Set the designated one path for courses.
+ $mycoursesnode = $this->get_item('mycourses');
+ if (!is_null($mycoursesnode)) {
+ // TODO: Once MDL-70801 lands point this to the new page.
+ $url = new \moodle_url('/course/');
+ $mycoursesnode->action = $url;
+ $mycoursesnode->text = get_string('courses');
+ }
+
+ $this->remove_no_link_items();
+
+ // Don't display the navbar if there is only one item. Apparently this is bad UX design.
+ if ($this->item_count() <= 1) {
+ $this->clear_items();
+ return;
+ }
+
+ // Make sure that the last item is not a link. Not sure if this is always a good idea.
+ $this->remove_last_item_action();
+ }
+
+ /**
+ * Get all the boostnavbaritem elements.
+ *
+ * @return boostnavbaritem[] Boost navbar items.
+ */
+ public function get_items(): array {
+ return $this->items;
+ }
+
+ /**
+ * Removes all navigation items out of this boost navbar
+ */
+ protected function clear_items(): void {
+ $this->items = [];
+ }
+
+ /**
+ * Retrieve a single navbar item.
+ *
+ * @param string|int $key The identifier of the navbar item to return.
+ * @return \breadcrumb_navigation_node|null The navbar item.
+ */
+ protected function get_item($key): ?\breadcrumb_navigation_node {
+ foreach ($this->items as $item) {
+ if ($key === $item->key) {
+ return $item;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Counts all of the navbar items.
+ *
+ * @return int How many navbar items there are.
+ */
+ protected function item_count(): int {
+ return count($this->items);
+ }
+
+ /**
+ * Remove a boostnavbaritem from the boost navbar.
+ *
+ * @param string|int $itemkey An identifier for the boostnavbaritem
+ */
+ protected function remove($itemkey): void {
+
+ $itemfound = false;
+ foreach ($this->items as $key => $item) {
+ if ($item->key === $itemkey) {
+ unset($this->items[$key]);
+ $itemfound = true;
+ break;
+ }
+ }
+ if (!$itemfound) {
+ return;
+ }
+
+ $itemcount = $this->item_count();
+ if ($itemcount <= 0) {
+ return;
+ }
+
+ $this->items = array_values($this->items);
+ // Set the last item to last item if it is not.
+ $lastitem = $this->items[$itemcount - 1];
+ if (!$lastitem->is_last()) {
+ $lastitem->set_last(true);
+ }
+ }
+
+ /**
+ * Removes the action from the last item of the boostnavbaritem.
+ */
+ protected function remove_last_item_action(): void {
+ $item = end($this->items);
+ $item->action = null;
+ reset($this->items);
+ }
+
+ /**
+ * Returns the second last navbar item. This is for use in the mobile view where we are showing just the second
+ * last item in the breadcrumb navbar.
+ *
+ * @return breakcrumb_navigation_node|null The second last navigation node.
+ */
+ public function get_penultimate_item(): ?\breadcrumb_navigation_node {
+ $number = $this->item_count() - 2;
+ return ($number >= 0) ? $this->items[$number] : null;
+ }
+
+ /**
+ * Remove items that are categories or have no actions associated with them.
+ */
+ protected function remove_no_link_items(): void {
+ foreach ($this->items as $key => $value) {
+ if (!$value->has_action() || $value->type == \navigation_node::TYPE_SECTION) {
+ unset($this->items[$key]);
+ }
+ }
+ $this->items = array_values($this->items);
+ }
+}
diff --git a/theme/boost/classes/output/context_header.php b/theme/boost/classes/output/context_header.php
new file mode 100644
index 00000000000..8e2a25ed6f5
--- /dev/null
+++ b/theme/boost/classes/output/context_header.php
@@ -0,0 +1,239 @@
+.
+
+namespace theme_boost\output;
+
+use moodle_url;
+use get_string;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * The context header for the boost theme.
+ *
+ * @package theme_boost
+ * @copyright 2021 Adrian Greeve
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class context_header implements \renderable, \templatable {
+
+ /** \moodle_page This current page information. */
+ protected $page;
+
+ /** array Header data. */
+ protected $headerinfo;
+
+ /** int What level the header should be i.e. 1 =
. */
+ protected $headinglevel;
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param \moodle_page $page This current page information.
+ * @param array $headerinfo Header data.
+ * @param int $headinglevel What level the header should be i.e. 1 =
{{#hasregionmainsettingsmenu}}
diff --git a/theme/boost/templates/context_header.mustache b/theme/boost/templates/context_header.mustache
new file mode 100644
index 00000000000..748bb1c7338
--- /dev/null
+++ b/theme/boost/templates/context_header.mustache
@@ -0,0 +1,81 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template theme_boost/context_header
+
+ Context variables required for this template:
+ * logo - Information to display a logo
+ * imagedata - Data for an image if the header has one
+ * prefix - Information displayed before the heading.
+ * heading - The heading.
+ * userbuttons - Additional buttons. Currently only shown when displaying the user.
+
+
+ Example context (json):
+ {
+ "logo": {
+ "image": {
+ "src": "https://www.example.com/feeds/news/kitchen.jpg",
+ "alt": "Alternate text"
+ },
+ "heading": "Name of the site."
+ },
+ "imagedata": "Image data",
+ "prefix": "Assignment",
+ "heading": "Title of the assignment",
+ "userbuttons": {
+ "page": true,
+ "formattedimage": "formatted image",
+ "title": "Press me"
+ }
+ }
+}}
+{{#logo}}
+
{{#hasregionmainsettingsmenu}}
diff --git a/theme/boost/templates/language_menu.mustache b/theme/boost/templates/language_menu.mustache
new file mode 100644
index 00000000000..305f8a85c63
--- /dev/null
+++ b/theme/boost/templates/language_menu.mustache
@@ -0,0 +1,63 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template theme_boost/language_menu
+
+ Language menu template.
+
+ Context variables required for this template:
+ * title - The title of the menu (displays the text of the currently active language).
+ * items - Array of data representing the available languages to be displayed in the language menu.
+ * link - If a link is provided render it.
+ * title - The title of the language item.
+ * text - The text displayed for the language item.
+ * url - The url link to activate the given language.
+ * isactive - Whether the language is currently active.
+
+ Example context (json):
+ {
+ "title": "English (en)",
+ "items": {
+ "link": {
+ "title": "English (en)",
+ "text": "English (en)",
+ "url": "http://example.com",
+ "isactive": 0
+ }
+ }
+ }
+}}
+
diff --git a/theme/boost/tests/behat/behat_theme_boost_behat_navigation.php b/theme/boost/tests/behat/behat_theme_boost_behat_navigation.php
new file mode 100644
index 00000000000..7d3c9508622
--- /dev/null
+++ b/theme/boost/tests/behat/behat_theme_boost_behat_navigation.php
@@ -0,0 +1,91 @@
+.
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+// For that reason, we can't even rely on $CFG->admin being available here.
+
+require_once(__DIR__ . '/../../../../lib/tests/behat/behat_navigation.php');
+
+use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+use Behat\Mink\Exception\ExpectationException as ExpectationException;
+
+/**
+ * Step definitions related to the navigation in the Boost theme.
+ *
+ * @package theme_boost
+ * @category test
+ * @copyright 2021 Mihail Geshoski
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_theme_boost_behat_navigation extends behat_navigation {
+ /**
+ * Checks whether a node is active in the navbar.
+ *
+ * @override i should see :name is active in navigation
+ *
+ * @throws ElementNotFoundException
+ * @param string $element The name of the nav elemnent to look for.
+ * @return void
+ */
+ public function i_should_see_is_active_in_navigation($element) {
+ $this->execute("behat_general::assert_element_contains_text",
+ [$element, '.navbar .nav-link.active', 'css_element']);
+ }
+
+ /**
+ * Checks whether the language selector menu is present in the navbar.
+ *
+ * @Given language selector menu should exist in the navbar
+ * @Given language selector menu should :not exist in the navbar
+ *
+ * @throws ElementNotFoundException
+ * @param string|null $not Instructs to checks whether the element does not exist in the user menu, if defined
+ * @return void
+ */
+ public function lang_menu_should_exist($not = null) {
+ $callfunction = is_null($not) ? 'should_exist' : 'should_not_exist';
+ $this->execute("behat_general::{$callfunction}", [$this->get_lang_menu_xpath(), 'xpath_element']);
+ }
+
+ /**
+ * Checks whether an item exists in the language selector menu.
+ *
+ * @Given :itemtext :selectortype should exist in the language selector menu
+ * @Given :itemtext :selectortype should :not exist in the language selector menu
+ *
+ * @throws ElementNotFoundException
+ * @param string $itemtext The menu item to find
+ * @param string $selectortype The selector type
+ * @param string|null $not Instructs to checks whether the element does not exist in the user menu, if defined
+ * @return void
+ */
+ public function should_exist_in_lang_menu($itemtext, $selectortype, $not = null) {
+ $callfunction = is_null($not) ? 'should_exist_in_the' : 'should_not_exist_in_the';
+ $this->execute("behat_general::{$callfunction}",
+ [$itemtext, $selectortype, $this->get_lang_menu_xpath(), 'xpath_element']);
+ }
+
+ /**
+ * Return the xpath for the language selector menu element.
+ *
+ * @return string The xpath
+ */
+ protected function get_lang_menu_xpath() {
+ return "//nav[contains(concat(' ', @class, ' '), ' navbar ')]" .
+ "//div[contains(concat(' ', @class, ' '), ' langmenu ')]" .
+ "//div[contains(concat(' ', @class, ' '), ' dropdown-menu ')]";
+ }
+}
diff --git a/theme/boost/tests/behat/languagemenu.feature b/theme/boost/tests/behat/languagemenu.feature
new file mode 100644
index 00000000000..94eac0609b0
--- /dev/null
+++ b/theme/boost/tests/behat/languagemenu.feature
@@ -0,0 +1,79 @@
+@javascript @theme_boost
+Feature: Language selector menu
+ To be able to set the preferred language for the site
+ As a user
+ I need to be presented with a language selector menu
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ 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 "en_ar"
+ And I press "Install selected language pack(s)"
+ And the "Installed language packs" select box should contain "en_ar"
+ And I log out
+
+ Scenario: Logged user is presented with a language selector which is placed within the user menu
+ Given I log in as "teacher1"
+ And I am on site homepage
+ # The language selector menu is not present in the navbar when a user is logged in.
+ And language selector menu should not exist in the navbar
+ # The language selector is present within the user menu.
+ And "Language" "link" should exist in the user menu
+ When I follow "Language" in the user menu
+ Then I should see "Language selector" user submenu
+ And "English (en)" "link" should exist in the "Language selector" user submenu
+ And "English (pirate) (en_ar)" "link" should exist in the "Language selector" user submenu
+
+ Scenario: Non-logged user is presented with a language selector which is placed within the navbar
+ Given I am on site homepage
+ # The language selector menu is present in the navbar when a user is not logged in.
+ And language selector menu should exist in the navbar
+ And "English (en)" "link" should exist in the language selector menu
+ And "English (pirate) (en_ar)" "link" should exist in the language selector menu
+
+ Scenario: Logged user is not presented with a language selector in a course if a language is forced in that context
+ Given I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I navigate to "Edit settings" in current page administration
+ And I expand all fieldsets
+ And I set the following fields to these values:
+ | id_lang | en |
+ And I press "Save and display"
+ # The language selector is not present within the user menu in the course context when a language is enforced.
+ When I am on "Course 1" course homepage
+ And "Language" "link" should not exist in the user menu
+ # The language selector is present within the user menu in other contexts.
+ And I am on site homepage
+ And "Language" "link" should exist in the user menu
+
+ Scenario: Logged user is not presented with a language selector if there is less than two installed languages
+ Given I log in as "admin"
+ And I navigate to "Language > Language packs" in site administration
+ And I set the field "Installed language packs" to "en_ar"
+ And I press "Uninstall selected language pack(s)"
+ And I click on "Yes" "button" in the "Uninstall selected language pack(s)" "dialogue"
+ And the "Installed language packs" select box should not contain "en_ar"
+ When I am on site homepage
+ # The language selector is not present within the user menu.
+ And "Language" "link" should not exist in the user menu
+
+ Scenario: Non-logged user is not presented with a language selector if there is less than two installed languages
+ Given I log in as "admin"
+ And I navigate to "Language > Language packs" in site administration
+ And I set the field "Installed language packs" to "en_ar"
+ And I press "Uninstall selected language pack(s)"
+ And I click on "Yes" "button" in the "Uninstall selected language pack(s)" "dialogue"
+ And the "Installed language packs" select box should not contain "en_ar"
+ And I log out
+ When I am on site homepage
+ # The language selector menu is not present in the navbar.
+ Then language selector menu should not exist in the navbar
diff --git a/theme/boost/version.php b/theme/boost/version.php
index 4284349dd4c..5864b20f47b 100644
--- a/theme/boost/version.php
+++ b/theme/boost/version.php
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2021052500;
+$plugin->version = 2021052505;
$plugin->requires = 2021052500;
$plugin->component = 'theme_boost';
diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css
index 7d90c458cef..a2c424db9d6 100644
--- a/theme/classic/style/moodle.css
+++ b/theme/classic/style/moodle.css
@@ -10202,10 +10202,12 @@ img.resize {
width: 1em; }
.action-menu .dropdown-toggle {
- text-decoration: none; }
+ text-decoration: none;
+ display: inline-block; }
.action-menu {
- white-space: nowrap; }
+ white-space: nowrap;
+ display: inline; }
.block img.resize {
height: 0.9em;
@@ -11571,7 +11573,8 @@ nav.navbar .logo img {
position: relative; }
.page-context-header .page-header-image {
margin-right: 1em;
- margin-bottom: 1em; }
+ margin-bottom: 0.5em;
+ margin-top: 0.7em; }
.page-context-header .page-header-image > a {
display: inline-block; }
.page-context-header .page-header-headings,
@@ -12073,42 +12076,42 @@ input[disabled] {
border: 0; }
#page-header .card .card-body {
padding: 0; }
- .nav-tabs,
+ .nav-tabs:not(.more-nav),
.nav-pills {
margin: 0;
border: 0;
padding: 0.125rem;
background-color: #e9ecef; }
- .nav-tabs .nav-item,
+ .nav-tabs:not(.more-nav) .nav-item,
.nav-pills .nav-item {
flex: 1 1 auto;
text-align: center; }
- .nav-tabs .nav-link,
+ .nav-tabs:not(.more-nav) .nav-link,
.nav-pills .nav-link {
background: #fff;
border: 0;
margin: 0.125rem; }
- .nav-tabs .nav-link.active,
+ .nav-tabs:not(.more-nav) .nav-link.active,
.nav-pills .nav-link.active {
color: #6e7377;
border-color: #6e7377;
border-color: #6e7377; }
- .nav-tabs .nav-link.active:hover,
+ .nav-tabs:not(.more-nav) .nav-link.active:hover,
.nav-pills .nav-link.active:hover {
color: #fff;
background-color: #6e7377;
border-color: #6e7377; }
- .nav-tabs .nav-link.active:focus, .nav-tabs .nav-link.active.focus,
+ .nav-tabs:not(.more-nav) .nav-link.active:focus, .nav-tabs:not(.more-nav) .nav-link.active.focus,
.nav-pills .nav-link.active:focus,
.nav-pills .nav-link.active.focus {
box-shadow: 0 0 0 0.2rem rgba(110, 115, 119, 0.5); }
- .nav-tabs .nav-link.active.disabled, .nav-tabs .nav-link.active:disabled,
+ .nav-tabs:not(.more-nav) .nav-link.active.disabled, .nav-tabs:not(.more-nav) .nav-link.active:disabled,
.nav-pills .nav-link.active.disabled,
.nav-pills .nav-link.active:disabled {
color: #6e7377;
background-color: transparent; }
- .nav-tabs .nav-link.active:not(:disabled):not(.disabled):active, .nav-tabs .nav-link.active:not(:disabled):not(.disabled).active,
- .show > .nav-tabs .nav-link.active.dropdown-toggle,
+ .nav-tabs:not(.more-nav) .nav-link.active:not(:disabled):not(.disabled):active, .nav-tabs:not(.more-nav) .nav-link.active:not(:disabled):not(.disabled).active,
+ .show > .nav-tabs:not(.more-nav) .nav-link.active.dropdown-toggle,
.nav-pills .nav-link.active:not(:disabled):not(.disabled):active,
.nav-pills .nav-link.active:not(:disabled):not(.disabled).active,
.show >
@@ -12116,8 +12119,8 @@ input[disabled] {
color: #fff;
background-color: #6e7377;
border-color: #6e7377; }
- .nav-tabs .nav-link.active:not(:disabled):not(.disabled):active:focus, .nav-tabs .nav-link.active:not(:disabled):not(.disabled).active:focus,
- .show > .nav-tabs .nav-link.active.dropdown-toggle:focus,
+ .nav-tabs:not(.more-nav) .nav-link.active:not(:disabled):not(.disabled):active:focus, .nav-tabs:not(.more-nav) .nav-link.active:not(:disabled):not(.disabled).active:focus,
+ .show > .nav-tabs:not(.more-nav) .nav-link.active.dropdown-toggle:focus,
.nav-pills .nav-link.active:not(:disabled):not(.disabled):active:focus,
.nav-pills .nav-link.active:not(:disabled):not(.disabled).active:focus,
.show >
@@ -20196,11 +20199,55 @@ div.editor_atto_toolbar button .icon {
align-items: center; }
.navbar.fixed-top .usermenu .dropdown-toggle::after {
display: none; }
+ .navbar.fixed-top .usermenu .dropdown-menu {
+ min-width: 235px; }
+ .navbar.fixed-top .usermenu .dropdown-menu .carousel-navigation-link > * {
+ pointer-events: none; }
+ .navbar.fixed-top .usermenu .dropdown-menu .dropdown-item {
+ padding: .25rem 1.75rem .25rem .75rem; }
+ .navbar.fixed-top .usermenu .dropdown-menu .dropdown-item.carousel-navigation-link::after {
+ font-family: FontAwesome;
+ content: "";
+ font-size: 1rem;
+ right: .75rem;
+ position: absolute; }
+ .navbar.fixed-top .usermenu .dropdown-menu .submenu .header {
+ padding: .25rem .75rem;
+ font-size: .975rem; }
+ .navbar.fixed-top .usermenu .dropdown-menu .submenu .header .icon {
+ font-size: 20px;
+ height: 20px;
+ width: 20px;
+ margin: 0; }
+ .navbar.fixed-top .usermenu .dropdown-menu .submenu .items .dropdown-item[aria-current="true"]::before {
+ font-family: FontAwesome;
+ content: "";
+ font-size: 0.75rem;
+ padding-left: .25rem; }
+ .navbar.fixed-top .langmenu .dropdown-menu .dropdown-item[aria-current="true"]::before {
+ content: "\f00c";
+ font-size: 0.75rem;
+ padding-left: .25rem; }
.navbar.fixed-top .moodle-actionmenu .menubar,
.navbar.fixed-top .action-menu-trigger .dropdown {
height: 100%;
display: flex; }
+.dir-rtl .navbar.fixed-top .usermenu .dropdown-menu .dropdown-item.carousel-navigation-link::after {
+ content: ""; }
+
+.dir-rtl .navbar.fixed-top .usermenu .dropdown-menu .carousel .carousel-inner .carousel-item-prev.carousel-item-right,
+.dir-rtl .navbar.fixed-top .usermenu .dropdown-menu .carousel .carousel-inner .carousel-item-next.carousel-item-left {
+ transform: translateX(0); }
+
+.dir-rtl .navbar.fixed-top .usermenu .dropdown-menu .carousel .carousel-inner .carousel-item-next,
+.dir-rtl .navbar.fixed-top .usermenu .dropdown-menu .carousel .carousel-inner .carousel-item-right.active {
+ transform: translateX(-100%); }
+
+.dir-rtl .navbar.fixed-top .usermenu .dropdown-menu .carousel .carousel-inner .carousel-item-prev,
+.dir-rtl .navbar.fixed-top .usermenu .dropdown-menu .carousel .carousel-inner .carousel-item-left.active {
+ transform: translateX(100%); }
+
#page {
margin-top: 50px; }
@@ -20283,6 +20330,43 @@ div.editor_atto_toolbar button .icon {
height: 1rem;
margin: 0.5rem 0; }
+.moremenu {
+ opacity: 0;
+ height: 40px; }
+ .moremenu.observed {
+ opacity: 1; }
+ .moremenu .nav-link {
+ height: 40px;
+ display: flex;
+ align-items: center; }
+ .moremenu .nav-tabs {
+ margin-left: 0; }
+ .moremenu .dropdownmoremenu > .dropdown-menu > .dropdown-item {
+ padding: 0; }
+ .moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu {
+ position: static;
+ padding: 0;
+ border: 0; }
+ .moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu.show {
+ display: block; }
+ .moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item {
+ background-color: #f8f9fa; }
+ .moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item:hover, .moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-item:focus {
+ color: #fff;
+ background-color: #0f6fc5; }
+ .moremenu .dropdownmoremenu > .dropdown-menu .dropdown-menu .dropdown-divider {
+ display: none; }
+
+.navbar.fixed-top .moremenu {
+ height: 50px; }
+ .navbar.fixed-top .moremenu .nav-link {
+ height: 50px; }
+
+.secondarynavigation {
+ z-index: 1; }
+ .secondarynavigation .moremenu {
+ margin-bottom: -1px; }
+
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }
diff --git a/theme/classic/tests/behat/blacklist.json b/theme/classic/tests/behat/blacklist.json
index 1589fae200a..dc484070395 100644
--- a/theme/classic/tests/behat/blacklist.json
+++ b/theme/classic/tests/behat/blacklist.json
@@ -1,4 +1,7 @@
{
+ "contexts": [
+ "behat_theme_boost_behat_navigation"
+ ],
"features": [
"lib/tests/behat/action_menu.feature",
"blocks/tests/behat/hide_blocks.feature",
diff --git a/user/lib.php b/user/lib.php
index 6332648c790..85e5cf3a679 100644
--- a/user/lib.php
+++ b/user/lib.php
@@ -823,6 +823,16 @@ function user_get_user_navigation_info($user, $page, $options = array()) {
$returnobject->navitems = array();
$returnobject->metadata = array();
+ $guest = isguestuser();
+ if (!isloggedin() || $guest) {
+ $returnobject->unauthenticateduser = [
+ 'guest' => $guest,
+ 'content' => $guest ? 'loggedinasguest' : 'loggedinnot',
+ ];
+
+ return $returnobject;
+ }
+
$course = $page->course;
// Query the environment.
diff --git a/user/tests/behat/set_default_homepage.feature b/user/tests/behat/set_default_homepage.feature
index e2e6df503c4..a82add7237e 100644
--- a/user/tests/behat/set_default_homepage.feature
+++ b/user/tests/behat/set_default_homepage.feature
@@ -11,6 +11,9 @@ Feature: Set the site home page and dashboard as the default home page
And the following "users" exist:
| username | firstname | lastname | email |
| user1 | User | One | user1@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | user1 | C1 | student |
Scenario: Admin sets the site page and then the dashboard as the default home page
# This functionality does not work without the administration block.
@@ -28,14 +31,16 @@ Feature: Set the site home page and dashboard as the default home page
And I am on site homepage
And I follow "Make this my home page"
And I should not see "Make this my home page"
- And I am on "Course 1" course homepage
- And "Home" "text" should exist in the ".breadcrumb" "css_element"
+# The following lines should be changed once MDL-72110 is resolved.
+# And I am on "Course 1" course homepage
+# And "Home" "text" should exist in the ".breadcrumb" "css_element"
And I am on site homepage
And I follow "Dashboard"
And I follow "Make this my home page"
And I should not see "Make this my home page"
- And I am on "Course 1" course homepage
- Then "Dashboard" "text" should exist in the ".breadcrumb" "css_element"
+# The following lines should be changed once MDL-72110 is resolved.
+# And I am on "Course 1" course homepage
+# Then "Dashboard" "text" should exist in the ".breadcrumb" "css_element"
Scenario: User cannot configure their preferred default home page unless allowed by admin
Given I log in as "user1"
@@ -53,7 +58,9 @@ Feature: Set the site home page and dashboard as the default home page
And I follow "Home page"
And I set the field "Home page" to ""
And I press "Save changes"
- Then "" "text" should exist in the ".breadcrumb" "css_element"
+ And I log out
+ And I log in as "user1"
+ Then I should see "" is active in navigation
Examples:
| preference | breadcrumb |
diff --git a/user/tests/userlib_test.php b/user/tests/userlib_test.php
index aaca2547d21..68a383fb4a2 100644
--- a/user/tests/userlib_test.php
+++ b/user/tests/userlib_test.php
@@ -583,6 +583,7 @@ class core_userliblib_testcase extends advanced_testcase {
$PAGE->set_url('/');
$user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
$opts = user_get_user_navigation_info($user, $PAGE, array('avatarsize' => $testsize));
$avatarhtml = $opts->metadata['useravatar'];
diff --git a/version.php b/version.php
index 403b2c4aee5..ed80c32b00e 100644
--- a/version.php
+++ b/version.php
@@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
-$version = 2021082000.00; // YYYYMMDD = weekly release date of this DEV branch.
+$version = 2021082000.03; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.0dev (Build: 20210820)'; // Human-friendly version name