* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class participants_test extends core_reportbuilder_testcase {
+final class participants_test extends core_reportbuilder_testcase {
/**
* Load required test libraries
@@ -141,6 +141,10 @@ public function test_datasource_non_default_columns(): void {
$courseitem = grade_item::fetch_course_item($course->id);
$courseitem->update_final_grade($user1->id, 42.5);
+ // Add some cohort data.
+ $cohort = $this->getDataGenerator()->create_cohort(['name' => 'My cohort']);
+ cohort_add_member($cohort->id, $user1->id);
+
// Set some last access value for the user in the course.
$DB->insert_record('user_lastaccess',
['userid' => $user1->id, 'courseid' => $course->id, 'timeaccess' => $timelastaccess]);
@@ -171,6 +175,7 @@ public function test_datasource_non_default_columns(): void {
$generator->create_column(['reportid' => $report->get('id'),
'uniqueidentifier' => 'group:name']);
+ $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'cohort:name']);
$generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:criteria']);
$generator->create_column(['reportid' => $report->get('id'),
'uniqueidentifier' => 'completion:completed']);
@@ -193,15 +198,12 @@ public function test_datasource_non_default_columns(): void {
$generator->create_column(['reportid' => $report->get('id'),
'uniqueidentifier' => 'completion:grade']);
- // Add filter to the report.
- $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:plugin']);
-
- $content = $this->get_custom_report_content($report->get('id'));
-
// It should get 3 records (manual enrolment, self and guest).
+ $content = $this->get_custom_report_content($report->get('id'));
$this->assertCount(3, $content);
// Filter by Manual enrolment method.
+ $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:plugin']);
$content = $this->get_custom_report_content($report->get('id'), 30, [
'enrol:plugin_operator' => select::EQUAL_TO,
'enrol:plugin_value' => 'manual',
@@ -223,6 +225,7 @@ public function test_datasource_non_default_columns(): void {
'student', // Role shortname.
'Students generally have fewer privileges within a course.', // Role description.
$group->name, // Group name.
+ $cohort->name, // Cohort name.
"All criteria below are required\n- Self completion: Self completion
\n
", // Completion criteria.
'Yes', // Course completed.
userdate($timelastaccess), // Time last access.
@@ -242,7 +245,7 @@ public function test_datasource_non_default_columns(): void {
*
* @return array
*/
- public function datasource_filters_provider(): array {
+ public static function datasource_filters_provider(): array {
global $DB;
return [
@@ -331,6 +334,22 @@ public function datasource_filters_provider(): array {
],
['Luna'],
],
+ [
+ 'group:name',
+ [
+ 'group:name_operator' => text::IS_EQUAL_TO,
+ 'group:name_value' => 'My group',
+ ],
+ ['Lionel'],
+ ],
+ [
+ 'cohort:name',
+ [
+ 'cohort:name_operator' => text::IS_EQUAL_TO,
+ 'cohort:name_value' => 'My cohort',
+ ],
+ ['Kira'],
+ ],
[
'completion:completed',
[
@@ -407,6 +426,14 @@ public function test_datasource_filters(string $filter, array $filtervalues, arr
$this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher',
'manual', time(), time(), ENROL_USER_SUSPENDED);
+ // Add user1 to a group.
+ $group = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'My group']);
+ $this->getDataGenerator()->create_group_member(['groupid' => $group->id, 'userid' => $user1->id]);
+
+ // Add some cohort data.
+ $cohort = $this->getDataGenerator()->create_cohort(['name' => 'My cohort']);
+ cohort_add_member($cohort->id, $user2->id);
+
// Mark course as completed for the user.
$ccompletion = new completion_completion(array('course' => $course->id, 'userid' => $user1->id));
$ccompletion->mark_enrolled($timestart);
diff --git a/enrol/database/db/upgrade.php b/enrol/database/db/upgrade.php
index f9524cf264bf4..4658d2238dcbb 100644
--- a/enrol/database/db/upgrade.php
+++ b/enrol/database/db/upgrade.php
@@ -32,5 +32,8 @@ function xmldb_enrol_database_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/flatfile/db/upgrade.php b/enrol/flatfile/db/upgrade.php
index b9183dd95dc55..f27b2656a9b8a 100644
--- a/enrol/flatfile/db/upgrade.php
+++ b/enrol/flatfile/db/upgrade.php
@@ -32,5 +32,8 @@ function xmldb_enrol_flatfile_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/guest/db/upgrade.php b/enrol/guest/db/upgrade.php
index 11dba17306a85..d1719d14d6476 100644
--- a/enrol/guest/db/upgrade.php
+++ b/enrol/guest/db/upgrade.php
@@ -32,5 +32,8 @@ function xmldb_enrol_guest_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/imsenterprise/db/upgrade.php b/enrol/imsenterprise/db/upgrade.php
index cd60ea8e3b000..ec9df3a07201c 100644
--- a/enrol/imsenterprise/db/upgrade.php
+++ b/enrol/imsenterprise/db/upgrade.php
@@ -38,5 +38,8 @@ function xmldb_enrol_imsenterprise_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/lti/db/upgrade.php b/enrol/lti/db/upgrade.php
index 81f71013fe137..95f37a8119654 100644
--- a/enrol/lti/db/upgrade.php
+++ b/enrol/lti/db/upgrade.php
@@ -44,5 +44,8 @@ function xmldb_enrol_lti_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/manual/db/upgrade.php b/enrol/manual/db/upgrade.php
index bfea1cc3a5a41..bb592770f3328 100644
--- a/enrol/manual/db/upgrade.php
+++ b/enrol/manual/db/upgrade.php
@@ -32,5 +32,8 @@ function xmldb_enrol_manual_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/mnet/db/upgrade.php b/enrol/mnet/db/upgrade.php
index 3b69df6bd6160..da3c7a8406df4 100644
--- a/enrol/mnet/db/upgrade.php
+++ b/enrol/mnet/db/upgrade.php
@@ -32,5 +32,8 @@ function xmldb_enrol_mnet_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/paypal/db/upgrade.php b/enrol/paypal/db/upgrade.php
index d715fbe791f5f..db601be973dfe 100644
--- a/enrol/paypal/db/upgrade.php
+++ b/enrol/paypal/db/upgrade.php
@@ -50,5 +50,8 @@ function xmldb_enrol_paypal_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/self/db/upgrade.php b/enrol/self/db/upgrade.php
index f8677bd646aa3..73a822a8c7c7c 100644
--- a/enrol/self/db/upgrade.php
+++ b/enrol/self/db/upgrade.php
@@ -32,5 +32,8 @@ function xmldb_enrol_self_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/enrol/tests/externallib_test.php b/enrol/tests/externallib_test.php
index d5ebb5dce8755..c81ac56a14321 100644
--- a/enrol/tests/externallib_test.php
+++ b/enrol/tests/externallib_test.php
@@ -328,8 +328,9 @@ public function test_get_enrolled_users_visibility($settings, $results) {
$this->expectExceptionMessage($exception['message']);
} else {
// Failed, only canview and exception are supported.
- $this->markTestIncomplete('Incomplete, only canview and exception are supported');
+ throw new \coding_exception('Incomplete, only canview and exception are supported');
}
+
// Switch to the user and assign the role.
$this->setUser(${$user});
role_assign($roleid, $USER->id, $coursecontext);
diff --git a/filter/displayh5p/db/upgrade.php b/filter/displayh5p/db/upgrade.php
index d9bf449e1acc8..c5da55869bbb4 100644
--- a/filter/displayh5p/db/upgrade.php
+++ b/filter/displayh5p/db/upgrade.php
@@ -38,5 +38,8 @@ function xmldb_filter_displayh5p_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/filter/mathjaxloader/db/upgrade.php b/filter/mathjaxloader/db/upgrade.php
index 455007064de7e..c2dada9e57686 100644
--- a/filter/mathjaxloader/db/upgrade.php
+++ b/filter/mathjaxloader/db/upgrade.php
@@ -36,5 +36,8 @@ function xmldb_filter_mathjaxloader_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/filter/mediaplugin/db/upgrade.php b/filter/mediaplugin/db/upgrade.php
index 1ca6b5811b0cf..0a9a1a15bb3f9 100644
--- a/filter/mediaplugin/db/upgrade.php
+++ b/filter/mediaplugin/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_filter_mediaplugin_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/filter/tex/db/upgrade.php b/filter/tex/db/upgrade.php
index 9db1ecd761c2b..12d0647e5055d 100644
--- a/filter/tex/db/upgrade.php
+++ b/filter/tex/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_filter_tex_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/grade/grading/form/guide/db/upgrade.php b/grade/grading/form/guide/db/upgrade.php
index fd45ed63af719..df9b1331e3485 100644
--- a/grade/grading/form/guide/db/upgrade.php
+++ b/grade/grading/form/guide/db/upgrade.php
@@ -42,5 +42,8 @@ function xmldb_gradingform_guide_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/grade/grading/form/guide/tests/behat/define_marking_guide.feature b/grade/grading/form/guide/tests/behat/define_marking_guide.feature
new file mode 100644
index 0000000000000..04ccd8b9d76fe
--- /dev/null
+++ b/grade/grading/form/guide/tests/behat/define_marking_guide.feature
@@ -0,0 +1,74 @@
+@gradingform @gradingform_guide
+Feature: Teacher can define a marking guide
+ As a teacher,
+ I should be able to define a marking guide
+
+ Background:
+ Given the following "users" exist:
+ | username | firtname | lastname | email |
+ | teacher1 | Teacher | One | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ And the following "activities" exist:
+ | activity | course | name | advancedgradingmethod_submissions |
+ | assign | C1 | Assign 1 | guide |
+ And I am on the "Course 1" course page logged in as teacher1
+ And I go to "Assign 1" advanced grading definition page
+ And I set the following fields to these values:
+ | Name | Marking guide 1 |
+
+ Scenario: No criterion added to marking guide
+ When I press "Save as draft"
+ # Confirm that criterion parameters are required
+ Then I should see "Criterion name can not be empty"
+ And I should see "Criterion max score can not be empty"
+ # Confirm that marking guide is not saved due to the missing criterion
+ And I should not see "Marking guide 1 Draft"
+ And I should not see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
+
+ @javascript
+ Scenario: Marking guide criterion is added to marking guide
+ Given I define the following marking guide:
+ | Criterion name | Description for students | Description for markers | Maximum score |
+ | Criteria 1 | Criteria 1 description for student | Criteria 1 description for marker | 70 |
+ | Criteria 2 | Criteria 2 description for student | Criteria 2 description for marker | 30 |
+ # Move Criteria 1 below Criteria 2
+ And I click on "Move down" "button" in the "Criteria 1" "table_row"
+ When I press "Save as draft"
+ And I go to "Assign 1" advanced grading definition page
+ # Confirm that the order of criterion shown matches input -- Criteria 2 is listed before Criteria 1
+ Then "Move down" "button" in the "Criteria 2" "table_row" should be visible
+ And "Move up" "button" in the "Criteria 2" "table_row" should not be visible
+ And "Move up" "button" in the "Criteria 1" "table_row" should be visible
+ And "Move down" "button" in the "Criteria 1" "table_row" should not be visible
+ # Confirm the other information entered were saved
+ And I should see "Criteria 2 description for student" in the "Criteria 2" "table_row"
+ And I should see "Criteria 2 description for marker" in the "Criteria 2" "table_row"
+ And I should see "30" in the "Criteria 2" "table_row"
+ And I should see "Criteria 1 description for student" in the "Criteria 1" "table_row"
+ And I should see "Criteria 1 description for marker" in the "Criteria 1" "table_row"
+ And I should see "70" in the "Criteria 1" "table_row"
+
+ Scenario: Marking guide options and frequently used comment are added to marking guide
+ Given I define the following marking guide:
+ | Criterion name | Description for students | Description for markers | Maximum score |
+ | Criteria 1 | Criteria 1 description for student | Criteria 1 description for marker | 50 |
+ | Criteria 2 | Criteria 2 description for student | Criteria 2 description for marker | 50 |
+ # Add frequently used comments and other marking guide options
+ And I define the following frequently used comments:
+ | Comment 1 |
+ | Comment 2 |
+ And I set the following fields to these values:
+ | Show guide definition to students | 1 |
+ | Show marks per criterion to students | 0 |
+ When I press "Save as draft"
+ And I go to "Assign 1" advanced grading definition page
+ # Confirm that frequently used comments and marking guide options specified during registration are retained
+ Then I should see "Comment 1"
+ And I should see "Comment 2"
+ And the field "Show guide definition to students" matches value "1"
+ And the field "Show marks per criterion to students" matches value "0"
diff --git a/grade/grading/form/rubric/db/upgrade.php b/grade/grading/form/rubric/db/upgrade.php
index 999296542f6e8..edaa2ddb0495f 100644
--- a/grade/grading/form/rubric/db/upgrade.php
+++ b/grade/grading/form/rubric/db/upgrade.php
@@ -38,5 +38,8 @@ function xmldb_gradingform_rubric_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/grade/report/grader/db/upgrade.php b/grade/report/grader/db/upgrade.php
index c0af003d6c112..bb1c2b891e6bb 100644
--- a/grade/report/grader/db/upgrade.php
+++ b/grade/report/grader/db/upgrade.php
@@ -77,5 +77,8 @@ function xmldb_gradereport_grader_upgrade(int $oldversion): bool {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/grade/report/history/db/upgrade.php b/grade/report/history/db/upgrade.php
index 26a444ced6bb0..a3909c35fe0b7 100644
--- a/grade/report/history/db/upgrade.php
+++ b/grade/report/history/db/upgrade.php
@@ -38,5 +38,8 @@ function xmldb_gradereport_history_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/grade/report/overview/db/upgrade.php b/grade/report/overview/db/upgrade.php
index 5df0e8e7b8a6e..16c73681b915a 100644
--- a/grade/report/overview/db/upgrade.php
+++ b/grade/report/overview/db/upgrade.php
@@ -38,5 +38,8 @@ function xmldb_gradereport_overview_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/grade/report/user/classes/report/user.php b/grade/report/user/classes/report/user.php
index 8ed200e854b78..c9cfc11682fad 100644
--- a/grade/report/user/classes/report/user.php
+++ b/grade/report/user/classes/report/user.php
@@ -713,7 +713,7 @@ private function fill_table_recursive(array &$element) {
$gradestatusclass = '';
$gradepassicon = '';
$ispassinggrade = $gradegrade->is_passed($gradegrade->grade_item);
- if (!is_null($ispassinggrade)) {
+ if (!is_null($gradeval) && !is_null($ispassinggrade)) {
$gradestatusclass = $ispassinggrade ? 'gradepass' : 'gradefail';
if ($ispassinggrade) {
$gradepassicon = $OUTPUT->pix_icon(
diff --git a/grade/report/user/db/upgrade.php b/grade/report/user/db/upgrade.php
index 1cd616c3631ed..977a9f2518f68 100644
--- a/grade/report/user/db/upgrade.php
+++ b/grade/report/user/db/upgrade.php
@@ -36,5 +36,8 @@ function xmldb_gradereport_user_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/h5p/classes/core.php b/h5p/classes/core.php
index 8c6935dd59863..6bedab9b7e2ca 100644
--- a/h5p/classes/core.php
+++ b/h5p/classes/core.php
@@ -273,8 +273,16 @@ public function fetch_content_type(array $library): ?int {
$librarykey = static::libraryToString($library);
+ if (is_null($factory->get_storage()->h5pC->librariesJsonData)) {
+ // There was an error fetching the content type.
+ debugging('Error fetching content type: ' . $librarykey);
+ return null;
+ }
+
$libraryjson = $factory->get_storage()->h5pC->librariesJsonData[$librarykey];
- if (!array_key_exists('libraryId', $libraryjson)) {
+ if (is_null($libraryjson) || !array_key_exists('libraryId', $libraryjson)) {
+ // There was an error fetching the content type.
+ debugging('Error fetching content type: ' . $librarykey);
return null;
}
diff --git a/h5p/classes/player.php b/h5p/classes/player.php
index a2f8564cea3f2..c1af5db6e5795 100644
--- a/h5p/classes/player.php
+++ b/h5p/classes/player.php
@@ -154,7 +154,7 @@ public function __construct(string $url, \stdClass $config, bool $preventredirec
* Get the encoded URL for embeding this H5P content.
*
* @param string $url Local URL of the H5P file to display.
- * @param stdClass $config Configuration for H5P buttons.
+ * @param \stdClass $config Configuration for H5P buttons.
* @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions
* @param string $component optional moodle component to sent xAPI tracking
* @param bool $displayedit Whether the edit button should be displayed below the H5P content.
@@ -163,7 +163,8 @@ public function __construct(string $url, \stdClass $config, bool $preventredirec
* @return string The embedable code to display a H5P file.
*/
public static function display(
- string $url, \stdClass $config,
+ string $url,
+ \stdClass $config,
bool $preventredirect = true,
string $component = '',
bool $displayedit = false,
@@ -193,7 +194,7 @@ public static function display(
if ($originalfile) {
// Check if the user can edit this content.
if (api::can_edit_content($originalfile)) {
- $template->editurl = $CFG->wwwroot . '/h5p/edit.php?url=' . $url;
+ $template->editurl = (new \moodle_url('/h5p/edit.php', ['url' => $url]))->out(false);
}
}
}
diff --git a/install/lang/bg/error.php b/install/lang/bg/error.php
index 485dd25fe1028..bc1e612f32bec 100644
--- a/install/lang/bg/error.php
+++ b/install/lang/bg/error.php
@@ -39,4 +39,7 @@
$string['cannotfindcomponent'] = 'Не можа да намери компонент';
$string['cannotunzipfile'] = 'Файлът не може да се разархивира';
$string['componentisuptodate'] = 'Компонентът е актуален';
-$string['remotedownloaderror'] = 'Изтеглянето на компонента към вашия сървър пропадна, проверете настройките на proxy, препоръчително е PHP разширението cURL.
Вие трябва ръчно да изтеглите файла {$a->url}, да го копирате в директория {$a->dest} на вашия сървър и да го разархивирате там.';
+$string['remotedownloaderror'] = 'Изтеглянето на компонента към вашия сървър пропадна, проверете настройките на proxy; препоръчително е PHP разширението cURL.
Вие трябва ръчно да изтеглите файла {$a->url}, да го копирате в директория "{$a->dest}" на вашия сървър и да го разархивирате там.
';
+$string['wrongdestpath'] = 'Грешен път към целта';
+$string['wrongsourcebase'] = 'Грешен изходен адрес';
+$string['wrongzipfilename'] = 'Грешно име на ZIP файл-а';
diff --git a/install/lang/pt/install.php b/install/lang/pt/install.php
index 314f4ce8da30e..b551bb4c06261 100644
--- a/install/lang/pt/install.php
+++ b/install/lang/pt/install.php
@@ -32,7 +32,7 @@
$string['admindirname'] = 'Pasta de administração';
$string['availablelangs'] = 'Pacotes linguísticos disponíveis';
$string['chooselanguagehead'] = 'Selecione um idioma';
-$string['chooselanguagesub'] = 'Selecione o idioma a utilizar durante a instalação. O idioma escolhido será definido como o predefinido mas poderá depois selecionar outro(s) idioma(s) para o site e para os utilizadores.';
+$string['chooselanguagesub'] = 'Selecione o idioma a utilizar durante a instalação. O idioma escolhido será definido como o predefinido, mas poderá depois selecionar outro(s) idioma(s) para o site e para os utilizadores.';
$string['clialreadyconfigured'] = 'O ficheiro config.php já existe. Use \'admin/cli/install_database.php\' para instalar o Moodle para este site.';
$string['clialreadyinstalled'] = 'O ficheiro config.php já existe, use admin/cli/install_database.php para atualizar o Moodle para este site.';
$string['cliinstallheader'] = 'Programa para instalação do Moodle {$a} através da linha de comandos';
@@ -56,9 +56,9 @@
$string['pathsrodataroot'] = 'A pasta dos dados é só de leitura (não permite escrita).';
$string['pathsroparentdataroot'] = 'A pasta ascendente {$a->parent} não tem permissões de escrita. O programa de instalação não conseguiu criar a pasta {$a->dataroot}.';
$string['pathssubadmindir'] = 'Alguns servidores Web utilizam a pasta admin em URLs especiais de acesso a funcionalidades especiais, como é o caso de painéis de controlo. Algumas situações podem criar conflitos com a localização normal das páginas de administração do Moodle. Estes problemas podem ser resolvidos renomeando a pasta admin na instalação do Moodle e indicando aqui o novo nome a utilizar. Exemplo:
moodleadmin
Esta ação resolverá os problemas de acesso das hiperligações para as funcionalidades de administração do Moodle.';
-$string['pathssubdataroot'] = 'Pasta onde o Moodle irá armazenar todo o conteúdo de ficheiros enviados pelos utilizadores.
-O utilizador do Moodle no servidor web (normalmente nobody, apache ou www-data) deve ter permissão de leitura e escrita nessa pasta.
-
Não deve ser acessível diretamente através da web.
+$string['pathssubdataroot'] = 'Pasta onde o Moodle irá armazenar todo o conteúdo dos ficheiros enviados pelos utilizadores.
+O utilizador do Moodle no servidor web (normalmente nobody, apache ou www-data) deve ter permissão de leitura e de escrita nessa pasta.
+
Não pode ser acessível diretamente através da web.
Se a pasta não existir, o processo de instalação tentará criá-la.
';
$string['pathssubdirroot'] = 'Caminho completo para a pasta que contém o código Moodle.';
$string['pathssubwwwroot'] = 'Endereço web completo de acesso ao Moodle. Não é possível aceder ao Moodle usando mais do que um endereço. Se o site tiver mais do que um endereço público, devem ser configurados redirecionamentos permanentes em todos eles, à exceção deste. Se o site pode ser acedido a partir da Internet e de Intranet, use o endereço público aqui. Se o endereço atual não está correto, altere o endereço indicado na barra de endereço do seu navegador e reinicie a instalação.';
diff --git a/lang/en/reportbuilder.php b/lang/en/reportbuilder.php
index 2e61d15c0a6d4..aeba45202b008 100644
--- a/lang/en/reportbuilder.php
+++ b/lang/en/reportbuilder.php
@@ -126,7 +126,6 @@
$string['filterdatebefore'] = 'Before';
$string['filterdatecurrent'] = 'Current';
$string['filterdatedays'] = 'day(s)';
-$string['filterdatefrom'] = 'Date from';
$string['filterdatefuture'] = 'In the future';
$string['filterdatehours'] = 'hour(s)';
$string['filterdatelast'] = 'Last';
@@ -135,7 +134,6 @@
$string['filterdatenext'] = 'Next';
$string['filterdatepast'] = 'In the past';
$string['filterdateseconds'] = 'second(s)';
-$string['filterdateto'] = 'Date to';
$string['filterdateweeks'] = 'week(s)';
$string['filterdateyears'] = 'year(s)';
$string['filterdeleted'] = 'Deleted filter \'{$a}\'';
@@ -143,6 +141,7 @@
$string['filterendswith'] = 'Ends with';
$string['filterequalorgreaterthan'] = 'Greater than or equal';
$string['filterequalorlessthan'] = 'Less than or equal';
+$string['filterfieldfrom'] = '{$a} from';
$string['filterfieldoperator'] = '{$a} operator';
$string['filterfieldto'] = '{$a} to';
$string['filterfieldunit'] = '{$a} unit';
@@ -287,3 +286,7 @@
// Deprecated since Moodle 4.4.
$string['filterdurationunit'] = '{$a} unit';
+
+// Deprecated since Moodle 4.5.
+$string['filterdatefrom'] = 'Date from';
+$string['filterdateto'] = 'Date to';
diff --git a/lib/antivirus/clamav/db/upgrade.php b/lib/antivirus/clamav/db/upgrade.php
index 0d7b0d545b373..1ecd63dd951dd 100644
--- a/lib/antivirus/clamav/db/upgrade.php
+++ b/lib/antivirus/clamav/db/upgrade.php
@@ -38,5 +38,8 @@ function xmldb_antivirus_clamav_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/lib/behat/classes/util.php b/lib/behat/classes/util.php
index 1eb74ac266a93..25d9bfb7db6ce 100644
--- a/lib/behat/classes/util.php
+++ b/lib/behat/classes/util.php
@@ -226,7 +226,7 @@ public static function check_server_status() {
behat_error (BEHAT_EXITCODE_REQUIREMENT, $CFG->behat_wwwroot . ' is not available, ensure you specified ' .
'correct url and that the server is set up and started.' . PHP_EOL . ' More info in ' .
- behat_command::DOCS_URL . PHP_EOL);
+ behat_command::DOCS_URL . PHP_EOL . parent::get_site_info());
}
// Check if cli version is same as web version.
diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php
index 180d15def4001..e5d169047cf5c 100644
--- a/lib/classes/output/icon_system_fontawesome.php
+++ b/lib/classes/output/icon_system_fontawesome.php
@@ -265,8 +265,8 @@ public function get_core_icon_map() {
'core:i/info' => 'fa-info',
'core:i/invalid' => 'fa-times text-danger',
'core:i/item' => 'fa-circle',
- 'core:i/loading' => 'fa-circle-o-notch fa-spin',
- 'core:i/loading_small' => 'fa-circle-o-notch fa-spin',
+ 'core:i/loading' => 'fa-circle-o-notch fa-spin fa-sm',
+ 'core:i/loading_small' => 'fa-circle-o-notch fa-spin fa-sm',
'core:i/location' => 'fa-map-marker',
'core:i/lock' => 'fa-lock',
'core:i/log' => 'fa-list-alt',
diff --git a/lib/classes/report_helper.php b/lib/classes/report_helper.php
index 05a0b26a9a544..3d1715edc9648 100644
--- a/lib/classes/report_helper.php
+++ b/lib/classes/report_helper.php
@@ -14,24 +14,16 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see .
-/**
- * Report plugins helper class
- *
- * @package core
- * @subpackage report
- * @copyright 2021 Sujith Haridasan
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
namespace core;
+
use context_course;
-use moodle_url;
use stdClass;
/**
* A helper class with static methods to help report plugins
*
* @package core
+ * @subpackage report
* @copyright 2021 Sujith Haridasan
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -93,24 +85,11 @@ public static function print_report_selector(string $pluginname): void {
}
/**
- * Save the last selected report in the session
- *
* @deprecated since Moodle 4.0
- * @param int $id The course id
- * @param moodle_url $url The moodle url
- * @return void
*/
- public static function save_selected_report(int $id, moodle_url $url): void {
- global $USER;
-
- debugging('save_selected_report() has been deprecated because it is no longer used and will be '.
- 'removed in future versions of Moodle', DEBUG_DEVELOPER);
-
- // Last selected report.
- if (!isset($USER->course_last_report)) {
- $USER->course_last_report = [];
- }
- $USER->course_last_report[$id] = $url;
+ #[\core\attribute\deprecated(null, reason: 'It is no longer used', since: '4.0', final: true)]
+ public static function save_selected_report() {
+ \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
}
/**
diff --git a/lib/classes/text.php b/lib/classes/text.php
index b3b3164fc3f6e..f72b310541957 100644
--- a/lib/classes/text.php
+++ b/lib/classes/text.php
@@ -75,13 +75,17 @@ public static function is_charset_supported(string $charset): bool {
}
/**
- * Reset internal textlib caches.
- * @static
* @deprecated since Moodle 4.0. See MDL-53544.
- * @todo To be removed in Moodle 4.4 - MDL-71748
*/
- public static function reset_caches() {
- debugging("reset_caches() is deprecated. Typo3 has been removed and caches aren't used anymore.", DEBUG_DEVELOPER);
+ #[\core\attribute\deprecated(
+ 'core_text::reset_caches',
+ since: '4.0',
+ reason:'Typo3 has been removed and caches aren\'t used anymore.',
+ mdl: 'MDL-53544',
+ final: true,
+ )]
+ public static function reset_caches(): void {
+ \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
}
/**
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php
index 5afd146c2d071..f853b44ea15af 100644
--- a/lib/db/upgrade.php
+++ b/lib/db/upgrade.php
@@ -1199,10 +1199,11 @@ function xmldb_main_upgrade($oldversion) {
$dbman->create_table($table);
}
- // Main savepoint reached.
-// upgrade_main_savepoint(true, 2024042200.01);
// We're an unnamed version at this point.
}
+
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
return true;
}
diff --git a/lib/deprecatedlib.php b/lib/deprecatedlib.php
index 409a16a418780..5d29835affa3e 100644
--- a/lib/deprecatedlib.php
+++ b/lib/deprecatedlib.php
@@ -3216,3 +3216,11 @@ function question_add_context_in_key() {
function question_fix_top_names() {
throw new coding_exception(__FUNCTION__ . '() has been removed.');
}
+
+/**
+ * @deprecated since Moodle 2.9
+ */
+#[\core\attribute\deprecated('search_generate_SQL', since: '2.9', mdl: 'MDL-48939', final: true)]
+function search_generate_text_SQL() {
+ \core\deprecation::emit_deprecation_if_present(__FUNCTION__);
+}
diff --git a/lib/dml/tests/mysqli_native_moodle_database_test.php b/lib/dml/tests/mysqli_native_moodle_database_test.php
index dd17338224abb..565ee41c26623 100644
--- a/lib/dml/tests/mysqli_native_moodle_database_test.php
+++ b/lib/dml/tests/mysqli_native_moodle_database_test.php
@@ -122,7 +122,8 @@ public function test_ssl_connection(): void {
} catch (moodle_exception $e) {
// ... or fail.
// Unfortunately we cannot be sure with the error string.
- $this->markTestIncomplete('SSL not supported?');
+ $this->markTestSkipped('MySQL server does not support SSL. Unable to complete the test.');
+ return;
}
try {
diff --git a/lib/dml/tests/pgsql_native_moodle_database_test.php b/lib/dml/tests/pgsql_native_moodle_database_test.php
index 3e0a32e554641..09e6030391dc4 100644
--- a/lib/dml/tests/pgsql_native_moodle_database_test.php
+++ b/lib/dml/tests/pgsql_native_moodle_database_test.php
@@ -409,7 +409,8 @@ public function test_ssl_connection(): void {
// ... or fail with SSL not supported.
$this->assertStringContainsString($pgconnerr, $e->debuginfo);
$this->assertStringContainsString('server does not support SSL', $e->debuginfo);
- $this->markTestIncomplete('SSL not supported.');
+ $this->markTestSkipped('Postgres server does not support SSL. Unable to complete the test.');
+ return;
}
try {
diff --git a/lib/editor/atto/db/upgrade.php b/lib/editor/atto/db/upgrade.php
index fafd77fb6c57a..1c1352e74bde4 100644
--- a/lib/editor/atto/db/upgrade.php
+++ b/lib/editor/atto/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_editor_atto_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/lib/editor/atto/plugins/equation/db/upgrade.php b/lib/editor/atto/plugins/equation/db/upgrade.php
index adf83d5478373..cd834a38cfd3f 100644
--- a/lib/editor/atto/plugins/equation/db/upgrade.php
+++ b/lib/editor/atto/plugins/equation/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_atto_equation_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/lib/editor/atto/plugins/recordrtc/db/upgrade.php b/lib/editor/atto/plugins/recordrtc/db/upgrade.php
index f5c08aba072dd..10d7463819ca5 100644
--- a/lib/editor/atto/plugins/recordrtc/db/upgrade.php
+++ b/lib/editor/atto/plugins/recordrtc/db/upgrade.php
@@ -38,5 +38,8 @@ function xmldb_atto_recordrtc_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/lib/editor/tiny/amd/build/editor.min.js b/lib/editor/tiny/amd/build/editor.min.js
index 9f9f76543a688..4cafdce0939a0 100644
--- a/lib/editor/tiny/amd/build/editor.min.js
+++ b/lib/editor/tiny/amd/build/editor.min.js
@@ -1,3 +1,3 @@
-define("editor_tiny/editor",["exports","jquery","core/pending","./defaults","./loader","./options","./utils"],(function(_exports,_jquery,_pending,_defaults,_loader,Options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending),Options=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Options);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.remove(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);return setupForTarget(target,options)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=function(){let{plugins:plugins=null}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return plugins||(defaultOptions.plugins?defaultOptions.plugins:{})},getStandardConfig=(target,tinyMCE,options,plugins)=>{const lang=document.querySelector("html").lang,config=Object.assign({},(0,_defaults.getDefaultConfiguration)(),{base_url:_loader.baseUrl,target:target,min_height:175,height:target.clientHeight||"auto",language:lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,extended_valid_elements:"script[*],p[*],i[*]",xss_sanitization:!1,quickbars_insert_toolbar:"",block_formats:"Paragraph=p; Heading 3=h3; Heading 4=h4; Heading 5=h5; Heading 6=h6; Preformatted=pre",plugins:[...plugins],skin:"oxide",help_accessibility:!1,promotion:!1,branding:options.branding,table_header_type:"sectionCells",entity_encoding:"raw",ui_mode:"split",browser_spellcheck:!0,setup:editor=>{Options.register(editor,options),editor.on("PreInit",(function(){this.contentWindow=this.iframeElement.contentWindow})),editor.on("init",(function(){(0,_utils.removeSubmenuItem)(editor,"align","tiny:justify"),((editor,target)=>{let expectedEditingAreaHeight=0;expectedEditingAreaHeight=target.clientHeight?target.clientHeight:target.rows*(parseFloat(window.getComputedStyle(target).lineHeight)||22),editor.getContainer().querySelector(".tox-sidebar-wrap").clientHeight{const{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);return instanceConfig.menu.file&&(instanceConfig.menu.file.items=""),instanceConfig.menu.format&&(instanceConfig.menu.format.items=instanceConfig.menu.format.items.replace(/forecolor ?/,"").replace(/backcolor ?/,"").replace(/fontfamily ?/,"").replace(/fontsize ?/,"").replace(/styles ?/,"").replaceAll(/\| *\|/g,"|")),instanceConfig.quickbars_selection_toolbar=instanceConfig.quickbars_selection_toolbar.replace("h2 h3","h3 h4 h5 h6"),pluginConfig.filter((pluginConfig=>"function"==typeof pluginConfig.configure)).forEach((pluginConfig=>{const pluginInstanceOverride=pluginConfig.configure(instanceConfig,options);Object.assign(instanceConfig,pluginInstanceOverride)})),Object.assign(instanceConfig,Options.getInitialPluginConfiguration(options)),instanceConfig},isModalMode=target=>!!target.closest('[data-region="modal"]'),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]);tinyMCE.get().filter((editor=>!editor.getElement().isConnected)).forEach((editor=>{editor.remove()}));const existingEditor=tinyMCE.EditorManager.get(target.id);if(existingEditor){if(existingEditor.getElement()===target)return pendingPromise.resolve(),Promise.resolve(existingEditor);throw pendingPromise.resolve(),new Error("TinyMCE instance already exists for different target with same ID")}const instanceConfig=getEditorConfiguration(target,0,options,pluginValues),[editor]=await tinyMCE.init(instanceConfig);return target.dataset.fieldtype="editor",instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm),target.targetElm.dataset.fieldtype=null})),target.form&&(0,_jquery.default)(target.form).on("submit",(()=>{editor.save()})),editor.on("blur",(()=>{editor.save()})),editor.on("OpenWindow",(()=>{const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")||modal.classList.add("hide")}))})),editor.on("CloseWindow",(()=>{if(isModalMode(target)){const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")&&modal.classList.remove("hide")}))}})),pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}}));
+define("editor_tiny/editor",["exports","jquery","core/pending","./defaults","./loader","./options","./utils"],(function(_exports,_jquery,_pending,_defaults,_loader,Options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending),Options=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Options);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.remove(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);return setupForTarget(target,options)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=function(){let{plugins:plugins=null}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return plugins||(defaultOptions.plugins?defaultOptions.plugins:{})},getStandardConfig=(target,tinyMCE,options,plugins)=>{const lang=document.querySelector("html").lang,config=Object.assign({},(0,_defaults.getDefaultConfiguration)(),{base_url:_loader.baseUrl,target:target,min_height:175,height:target.clientHeight||"auto",language:lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,extended_valid_elements:"script[*],p[*],i[*]",xss_sanitization:!1,quickbars_insert_toolbar:"",block_formats:"Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre",plugins:[...plugins],skin:"oxide",help_accessibility:!1,promotion:!1,branding:options.branding,table_header_type:"sectionCells",entity_encoding:"raw",ui_mode:"split",browser_spellcheck:!0,setup:editor=>{Options.register(editor,options),editor.on("PreInit",(function(){this.contentWindow=this.iframeElement.contentWindow})),editor.on("init",(function(){(0,_utils.removeSubmenuItem)(editor,"align","tiny:justify"),((editor,target)=>{let expectedEditingAreaHeight=0;expectedEditingAreaHeight=target.clientHeight?target.clientHeight:target.rows*(parseFloat(window.getComputedStyle(target).lineHeight)||22),editor.getContainer().querySelector(".tox-sidebar-wrap").clientHeight{const{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);return instanceConfig.menu.file&&(instanceConfig.menu.file.items=""),instanceConfig.menu.format&&(instanceConfig.menu.format.items=instanceConfig.menu.format.items.replace(/forecolor ?/,"").replace(/backcolor ?/,"").replace(/fontfamily ?/,"").replace(/fontsize ?/,"").replace(/styles ?/,"").replaceAll(/\| *\|/g,"|")),instanceConfig.quickbars_selection_toolbar=instanceConfig.quickbars_selection_toolbar.replace("h2 h3","h3 h4 h5 h6"),pluginConfig.filter((pluginConfig=>"function"==typeof pluginConfig.configure)).forEach((pluginConfig=>{const pluginInstanceOverride=pluginConfig.configure(instanceConfig,options);Object.assign(instanceConfig,pluginInstanceOverride)})),Object.assign(instanceConfig,Options.getInitialPluginConfiguration(options)),instanceConfig},isModalMode=target=>!!target.closest('[data-region="modal"]'),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]);tinyMCE.get().filter((editor=>!editor.getElement().isConnected)).forEach((editor=>{editor.remove()}));const existingEditor=tinyMCE.EditorManager.get(target.id);if(existingEditor){if(existingEditor.getElement()===target)return pendingPromise.resolve(),Promise.resolve(existingEditor);throw pendingPromise.resolve(),new Error("TinyMCE instance already exists for different target with same ID")}const instanceConfig=getEditorConfiguration(target,0,options,pluginValues),[editor]=await tinyMCE.init(instanceConfig);return target.dataset.fieldtype="editor",instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm),target.targetElm.dataset.fieldtype=null})),target.form&&(0,_jquery.default)(target.form).on("submit",(()=>{editor.save()})),editor.on("blur",(()=>{editor.save()})),editor.on("OpenWindow",(()=>{const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")||modal.classList.add("hide")}))})),editor.on("CloseWindow",(()=>{if(isModalMode(target)){const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")&&modal.classList.remove("hide")}))}})),pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}}));
//# sourceMappingURL=editor.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/amd/build/editor.min.js.map b/lib/editor/tiny/amd/build/editor.min.js.map
index 09739266c2d58..df9f6837aa1c7 100644
--- a/lib/editor/tiny/amd/build/editor.min.js.map
+++ b/lib/editor/tiny/amd/build/editor.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"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 * TinyMCE Editor Manager.\n *\n * @module editor_tiny/editor\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport Pending from 'core/pending';\nimport {getDefaultConfiguration} from './defaults';\nimport {getTinyMCE, baseUrl} from './loader';\nimport * as Options from './options';\nimport {addToolbarButton, addToolbarButtons, addToolbarSection,\n removeToolbarButton, removeSubmenuItem, updateEditorState} from './utils';\n\n/**\n * Storage for the TinyMCE instances on the page.\n * @type {Map}\n */\nconst instanceMap = new Map();\n\n/**\n * The default editor configuration.\n * @type {Object}\n */\nlet defaultOptions = {};\n\n/**\n * Require the modules for the named set of TinyMCE plugins.\n *\n * @param {string[]} pluginList The list of plugins\n * @return {Promise[]} A matching set of Promises relating to the requested plugins\n */\nconst importPluginList = async(pluginList) => {\n // Fetch all of the plugins from the list of plugins.\n // If a plugin contains a '/' then it is assumed to be a Moodle AMD module to import.\n const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {\n if (pluginPath.indexOf('/') === -1) {\n // A standard TinyMCE Plugin.\n return Promise.resolve(pluginPath);\n }\n\n return import(pluginPath);\n }));\n\n // Normalise the plugin data to a list of plugin names.\n // Two formats are supported:\n // - a string; and\n // - an array whose first element is the plugin name, and the second element is the plugin configuration.\n const pluginNames = pluginHandlers.map((pluginConfig) => {\n if (typeof pluginConfig === 'string') {\n return pluginConfig;\n }\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[0];\n }\n return null;\n }).filter((value) => value);\n\n // Fetch the list of pluginConfig handlers.\n const pluginConfig = pluginHandlers.map((pluginConfig) => {\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[1];\n }\n return null;\n }).filter((value) => value);\n\n return {\n pluginNames,\n pluginConfig,\n };\n};\n\n/**\n * Fetch the language data for the specified language.\n *\n * @param {string} language The language identifier\n * @returns {object}\n */\nconst fetchLanguage = (language) => fetch(\n `${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}`\n).then(response => response.json());\n\n/**\n * Get a list of all Editors in a Map, keyed by the DOM Node that the Editor is associated with.\n *\n * @returns {Map}\n */\nexport const getAllInstances = () => new Map(instanceMap.entries());\n\n/**\n * Get the TinyMCE instance for the specified Node ID.\n *\n * @param {string} elementId\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));\n\n/*\n * Get the TinyMCE instance for the specified HTMLElement.\n *\n * @param {HTMLElement} element\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElement = element => {\n const instance = instanceMap.get(element);\n if (instance && instance.removed) {\n instanceMap.remove(element);\n return undefined;\n }\n return instance;\n};\n\n/**\n * Set up TinyMCE for the selector at the specified HTML Node id.\n *\n * @param {object} config The configuration required to setup the editor\n * @param {string} config.elementId The HTML Node ID\n * @param {Object} config.options The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForElementId = ({elementId, options}) => {\n const target = document.getElementById(elementId);\n return setupForTarget(target, options);\n};\n\n/**\n * Initialise the page with standard TinyMCE requirements.\n *\n * Currently this includes the language taken from the HTML lang property.\n */\nconst initialisePage = async() => {\n const lang = document.querySelector('html').lang;\n\n const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]);\n tinyMCE.addI18n(lang, langData);\n};\ninitialisePage();\n\n/**\n * Get the list of plugins to load for the specified configuration.\n *\n * If the specified configuration does not include a plugin configuration, then return the default configuration.\n *\n * @param {object} options\n * @param {array} [options.plugins=null] The plugin list\n * @returns {object}\n */\nconst getPlugins = ({plugins = null} = {}) => {\n if (plugins) {\n return plugins;\n }\n\n if (defaultOptions.plugins) {\n return defaultOptions.plugins;\n }\n\n return {};\n};\n\n/**\n * Adjust the editor size base on the target element.\n *\n * @param {TinyMCE} editor TinyMCE editor\n * @param {Node} target Target element\n */\nconst adjustEditorSize = (editor, target) => {\n let expectedEditingAreaHeight = 0;\n if (target.clientHeight) {\n expectedEditingAreaHeight = target.clientHeight;\n } else {\n // If the target element is hidden, we cannot get the lineHeight of the target element.\n // We don't have a proper way to retrieve the general lineHeight of the theme, so we use 22 here, it's equivalent to 1.5em.\n expectedEditingAreaHeight = target.rows * (parseFloat(window.getComputedStyle(target).lineHeight) || 22);\n }\n const currentEditingAreaHeight = editor.getContainer().querySelector('.tox-sidebar-wrap').clientHeight;\n if (currentEditingAreaHeight < expectedEditingAreaHeight) {\n // Change the height based on the target element's height.\n editor.getContainer().querySelector('.tox-sidebar-wrap').style.height = `${expectedEditingAreaHeight}px`;\n }\n};\n\n/**\n * Get the standard configuration for the specified options.\n *\n * @param {Node} target\n * @param {tinyMCE} tinyMCE\n * @param {object} options\n * @param {Array} plugins\n * @returns {object}\n */\nconst getStandardConfig = (target, tinyMCE, options, plugins) => {\n const lang = document.querySelector('html').lang;\n\n const config = Object.assign({}, getDefaultConfiguration(), {\n // eslint-disable-next-line camelcase\n base_url: baseUrl,\n\n // Set the editor target.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target\n target,\n\n // https://www.tiny.cloud/docs/tinymce/6/customize-ui/#set-maximum-and-minimum-heights-and-widths\n // Set the minimum height to the smallest height that we can fit the Menu bar, Tool bar, Status bar and the text area.\n // eslint-disable-next-line camelcase\n min_height: 175,\n\n // Base the height on the size of the text area.\n // In some cases, E.g.: The target is an advanced element, it will be hidden. We cannot get the height at this time.\n // So set the height to auto, and adjust it later by adjustEditorSize().\n height: target.clientHeight || 'auto',\n\n // Set the language.\n // https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language\n // eslint-disable-next-line camelcase\n language: lang,\n\n // Load the editor stylesheet into the editor iframe.\n // https://www.tiny.cloud/docs/tinymce/6/add-css-options/\n // eslint-disable-next-line camelcase\n content_css: [\n options.css,\n ],\n\n // Do not convert URLs to relative URLs.\n // https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls\n // eslint-disable-next-line camelcase\n convert_urls: false,\n\n // Enabled 'advanced' a11y options.\n // This includes allowing role=\"presentation\" from the image uploader.\n // https://www.tiny.cloud/docs/tinymce/6/accessibility/\n // eslint-disable-next-line camelcase\n a11y_advanced_options: true,\n\n // Add specific rules to the valid elements.\n // eslint-disable-next-line camelcase\n extended_valid_elements: 'script[*],p[*],i[*]',\n\n // Disable XSS Sanitisation.\n // We do this in PHP.\n // https://www.tiny.cloud/docs/tinymce/6/security/#turning-dompurify-off\n // Note: This feature has been backported from TinyMCE 6.4.0.\n // eslint-disable-next-line camelcase\n xss_sanitization: false,\n\n // Disable quickbars entirely.\n // The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle.\n // eslint-disable-next-line camelcase\n quickbars_insert_toolbar: '',\n\n // Override the standard block formats property (removing h1 & h2).\n // https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats\n // eslint-disable-next-line camelcase\n block_formats: 'Paragraph=p; Heading 3=h3; Heading 4=h4; Heading 5=h5; Heading 6=h6; Preformatted=pre',\n\n // The list of plugins to include in the instance.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins\n plugins: [\n ...plugins,\n ],\n\n // Skins\n skin: 'oxide',\n\n // Do not show the help link in the status bar.\n // https://www.tiny.cloud/docs/tinymce/latest/accessibility/#help_accessibility\n // eslint-disable-next-line camelcase\n help_accessibility: false,\n\n // Remove the \"Upgrade\" link for Tiny.\n // https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/\n promotion: false,\n\n // Allow the administrator to disable branding.\n // https://www.tiny.cloud/docs/tinymce/6/statusbar-configuration-options/#branding\n branding: options.branding,\n\n // Put th cells in a thead element.\n // https://www.tiny.cloud/docs/tinymce/6/table-options/#table_header_type\n // eslint-disable-next-line camelcase\n table_header_type: 'sectionCells',\n\n // Stored text in non-entity form.\n // https://www.tiny.cloud/docs/tinymce/6/content-filtering/#entity_encoding\n // eslint-disable-next-line camelcase\n entity_encoding: \"raw\",\n\n // Enable support for editors in scrollable containers.\n // https://www.tiny.cloud/docs/tinymce/6/ui-mode-configuration-options/#ui_mode\n // eslint-disable-next-line camelcase\n ui_mode: 'split',\n\n // Enable browser-supported spell checking.\n // https://www.tiny.cloud/docs/tinymce/latest/spelling/\n // eslint-disable-next-line camelcase\n browser_spellcheck: true,\n\n setup: (editor) => {\n Options.register(editor, options);\n\n editor.on('PreInit', function() {\n // Work around a bug in TinyMCE with Firefox.\n // When an editor is removed, and replaced with an identically attributed editor (same ID),\n // and the Firefox window is freshly opened (e.g. Behat, Private browsing), the wrong contentWindow\n // is assigned to the editor instance leading to an NS_ERROR_UNEXPECTED error in Firefox.\n // This is a workaround for that issue.\n this.contentWindow = this.iframeElement.contentWindow;\n });\n editor.on('init', function() {\n // Hide justify alignment sub-menu.\n removeSubmenuItem(editor, 'align', 'tiny:justify');\n // Adjust the editor size.\n adjustEditorSize(editor, target);\n });\n\n target.addEventListener('form:editorUpdated', function() {\n updateEditorState(editor, target);\n });\n\n target.dispatchEvent(new Event('form:editorUpdated'));\n },\n });\n\n config.toolbar = addToolbarSection(config.toolbar, 'content', 'formatting', true);\n config.toolbar = addToolbarButton(config.toolbar, 'content', 'link');\n\n // Add directionality plugins, always.\n config.toolbar = addToolbarSection(config.toolbar, 'directionality', 'alignment', true);\n config.toolbar = addToolbarButtons(config.toolbar, 'directionality', ['ltr', 'rtl']);\n\n // Remove the align justify button from the toolbar.\n config.toolbar = removeToolbarButton(config.toolbar, 'alignment', 'alignjustify');\n\n return config;\n};\n\n/**\n * Fetch the TinyMCE configuration for this editor instance.\n *\n * @param {HTMLElement} target\n * @param {TinyMCE} tinyMCE The TinyMCE API\n * @param {Object} options The editor plugin configuration\n * @param {object} pluginValues\n * @param {object} pluginValues.pluginConfig The list of plugin configuration\n * @param {object} pluginValues.pluginNames The list of plugins to load\n * @returns {object} The TinyMCE Configuration\n */\nconst getEditorConfiguration = (target, tinyMCE, options, pluginValues) => {\n const {\n pluginNames,\n pluginConfig,\n } = pluginValues;\n\n // Allow plugins to modify the configuration.\n // This seems a little strange, but we must double-process the config slightly.\n\n // First we fetch the standard configuration.\n const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);\n\n // Next we make any standard changes.\n // Here we remove the file menu, as it doesn't offer any useful functionality.\n // We only empty the items list so that a plugin may choose to add to it themselves later if they wish.\n if (instanceConfig.menu.file) {\n instanceConfig.menu.file.items = '';\n }\n\n // We disable the styles, backcolor, and forecolor plugins from the format menu.\n // These are not useful for Moodle and we don't want to encourage their use.\n if (instanceConfig.menu.format) {\n instanceConfig.menu.format.items = instanceConfig.menu.format.items\n // Remove forecolor and backcolor.\n .replace(/forecolor ?/, '')\n .replace(/backcolor ?/, '')\n\n // Remove fontfamily for now.\n .replace(/fontfamily ?/, '')\n\n // Remove fontsize for now.\n .replace(/fontsize ?/, '')\n\n // Remove styles - it just duplicates the format menu in a way which does not respect configuration\n .replace(/styles ?/, '')\n\n // Remove any duplicate separators.\n .replaceAll(/\\| *\\|/g, '|');\n }\n\n // eslint-disable-next-line camelcase\n instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6');\n\n // Next we call the `configure` function for any plugin which defines it.\n // We pass the current instanceConfig in here, to allow them to make certain changes to the global configuration.\n // For example, to add themselves to any menu, toolbar, and so on.\n // Any plugin which wishes to have configuration options must register those options here.\n pluginConfig.filter((pluginConfig) => typeof pluginConfig.configure === 'function').forEach((pluginConfig) => {\n const pluginInstanceOverride = pluginConfig.configure(instanceConfig, options);\n Object.assign(instanceConfig, pluginInstanceOverride);\n });\n\n // Next we convert the plugin configuration into a format that TinyMCE understands.\n Object.assign(instanceConfig, Options.getInitialPluginConfiguration(options));\n\n return instanceConfig;\n};\n\n/**\n * Check if the target for TinyMCE is in a modal or not.\n *\n * @param {HTMLElement} target Target to check\n * @returns {boolean} True if the target is in a modal form.\n */\nconst isModalMode = (target) => {\n return !!target.closest('[data-region=\"modal\"]');\n};\n\n/**\n * Set up TinyMCE for the HTML Element.\n *\n * @param {HTMLElement} target\n * @param {Object} [options={}] The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForTarget = async(target, options = {}) => {\n const instance = getInstanceForElement(target);\n if (instance) {\n return Promise.resolve(instance);\n }\n\n // Register a new pending promise to ensure that Behat waits for the editor setup to complete before continuing.\n const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');\n\n // Get the list of plugins.\n const plugins = getPlugins(options);\n\n // Fetch the tinyMCE API, and instantiate the plugins.\n const [tinyMCE, pluginValues] = await Promise.all([\n getTinyMCE(),\n importPluginList(Object.keys(plugins)),\n ]);\n\n // TinyMCE uses the element ID as a map key internally, even if the target has changed.\n // In the case where we have an editor in a modal form which has been detached from the DOM, but the editor not removed,\n // we need to manually destroy the editor.\n // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,\n // or added back elsewhere in the DOM.\n\n // First remove any detached editors.\n tinyMCE.get().filter((editor) => !editor.getElement().isConnected).forEach((editor) => {\n editor.remove();\n });\n\n // Now check for any existing editor which shares the same ID.\n const existingEditor = tinyMCE.EditorManager.get(target.id);\n if (existingEditor) {\n if (existingEditor.getElement() === target) {\n pendingPromise.resolve();\n return Promise.resolve(existingEditor);\n } else {\n pendingPromise.resolve();\n throw new Error('TinyMCE instance already exists for different target with same ID');\n }\n }\n\n // Get the editor configuration for this editor.\n const instanceConfig = getEditorConfiguration(target, tinyMCE, options, pluginValues);\n\n // Initialise the editor instance for the given configuration.\n // At this point any plugin which has configuration options registered will have them applied for this instance.\n const [editor] = await tinyMCE.init(instanceConfig);\n\n // Update the textarea when the editor to set the field type for Behat.\n target.dataset.fieldtype = 'editor';\n\n // Store the editor instance in the instanceMap and register a listener on removal to remove it from the map.\n instanceMap.set(target, editor);\n editor.on('remove', ({target}) => {\n // Handle removal of the editor from the map on destruction.\n instanceMap.delete(target.targetElm);\n target.targetElm.dataset.fieldtype = null;\n });\n\n // If the editor is part of a form, also listen to the jQuery submit event.\n // The jQuery submit event will not trigger the native submit event, and therefore the content will not be saved.\n // We cannot rely on listening to the bubbled submit event on the document because other events on child nodes may\n // consume the data before it is saved.\n if (target.form) {\n jQuery(target.form).on('submit', () => {\n editor.save();\n });\n }\n\n // Save the editor content to the textarea when the editor is blurred.\n editor.on('blur', () => {\n editor.save();\n });\n\n // If the editor is in a modal, we need to hide the modal when window editor's window is opened.\n editor.on('OpenWindow', () => {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (!modal.classList.contains('hide')) {\n modal.classList.add('hide');\n }\n });\n }\n });\n\n // If the editor's window is closed, we need to show the hidden modal back.\n editor.on('CloseWindow', () => {\n if (isModalMode(target)) {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (modal.classList.contains('hide')) {\n modal.classList.remove('hide');\n }\n });\n }\n }\n });\n\n pendingPromise.resolve();\n return editor;\n};\n\n/**\n * Set the default editor configuration.\n *\n * This configuration is used when an editor is initialised without any configuration.\n *\n * @param {object} [options={}]\n */\nexport const configureDefaultEditor = (options = {}) => {\n defaultOptions = options;\n};\n"],"names":["instanceMap","Map","defaultOptions","importPluginList","async","pluginHandlers","Promise","all","pluginList","map","pluginPath","indexOf","resolve","pluginNames","pluginConfig","Array","isArray","filter","value","entries","elementId","getInstanceForElement","document","getElementById","element","instance","get","removed","remove","_ref","options","target","setupForTarget","lang","querySelector","tinyMCE","langData","language","fetch","M","cfg","wwwroot","langrev","then","response","json","addI18n","initialisePage","getPlugins","plugins","getStandardConfig","config","Object","assign","base_url","baseUrl","min_height","height","clientHeight","content_css","css","convert_urls","a11y_advanced_options","extended_valid_elements","xss_sanitization","quickbars_insert_toolbar","block_formats","skin","help_accessibility","promotion","branding","table_header_type","entity_encoding","ui_mode","browser_spellcheck","setup","editor","Options","register","on","contentWindow","this","iframeElement","expectedEditingAreaHeight","rows","parseFloat","window","getComputedStyle","lineHeight","getContainer","style","adjustEditorSize","addEventListener","dispatchEvent","Event","toolbar","getEditorConfiguration","pluginValues","instanceConfig","menu","file","items","format","replace","replaceAll","quickbars_selection_toolbar","configure","forEach","pluginInstanceOverride","getInitialPluginConfiguration","isModalMode","closest","pendingPromise","Pending","keys","getElement","isConnected","existingEditor","EditorManager","id","Error","init","dataset","fieldtype","set","_ref2","delete","targetElm","form","save","modals","querySelectorAll","modal","classList","contains","add"],"mappings":"4oDAmCMA,YAAc,IAAIC,QAMpBC,eAAiB,SAQfC,iBAAmBC,MAAAA,mBAGfC,qBAAuBC,QAAQC,IAAIC,WAAWC,KAAIC,aACnB,IAA7BA,WAAWC,QAAQ,KAEZL,QAAQM,QAAQF,4NAGbA,4WAAAA,gBAOZG,YAAcR,eAAeI,KAAKK,cACR,iBAAjBA,aACAA,aAEPC,MAAMC,QAAQF,cACPA,aAAa,GAEjB,OACRG,QAAQC,OAAUA,cAUd,CACHL,YAAAA,YACAC,aATiBT,eAAeI,KAAKK,cACjCC,MAAMC,QAAQF,cACPA,aAAa,GAEjB,OACRG,QAAQC,OAAUA,mCAuBM,IAAM,IAAIjB,IAAID,YAAYmB,4CAQlBC,WAAaC,sBAAsBC,SAASC,eAAeH,kBAQrFC,sBAAwBG,gBAC3BC,SAAWzB,YAAY0B,IAAIF,aAC7BC,WAAYA,SAASE,eAIlBF,SAHHzB,YAAY4B,OAAOJ,0FAcMK,WAACT,UAACA,UAADU,QAAYA,oBACpCC,OAAST,SAASC,eAAeH,kBAChCY,eAAeD,OAAQD,UAQX1B,iBACb6B,KAAOX,SAASY,cAAc,QAAQD,MAErCE,QAASC,gBAAkB9B,QAAQC,IAAI,EAAC,yBAvD5B8B,SAuDwDJ,KAvD3CK,gBAC7BC,EAAEC,IAAIC,6CAAoCF,EAAEC,IAAIE,oBAAWL,WAChEM,MAAKC,UAAYA,SAASC,YAFLR,IAAAA,SAwDnBF,QAAQW,QAAQb,KAAMG,WAE1BW,SAWMC,WAAa,eAACC,QAACA,QAAU,6DAAQ,UAC/BA,UAIA/C,eAAe+C,QACR/C,eAAe+C,QAGnB,KAkCLC,kBAAoB,CAACnB,OAAQI,QAASL,QAASmB,iBAC3ChB,KAAOX,SAASY,cAAc,QAAQD,KAEtCkB,OAASC,OAAOC,OAAO,IAAI,uCAA2B,CAExDC,SAAUC,gBAIVxB,OAAAA,OAKAyB,WAAY,IAKZC,OAAQ1B,OAAO2B,cAAgB,OAK/BrB,SAAUJ,KAKV0B,YAAa,CACT7B,QAAQ8B,KAMZC,cAAc,EAMdC,uBAAuB,EAIvBC,wBAAyB,sBAOzBC,kBAAkB,EAKlBC,yBAA0B,GAK1BC,cAAe,wFAIfjB,QAAS,IACFA,SAIPkB,KAAM,QAKNC,oBAAoB,EAIpBC,WAAW,EAIXC,SAAUxC,QAAQwC,SAKlBC,kBAAmB,eAKnBC,gBAAiB,MAKjBC,QAAS,QAKTC,oBAAoB,EAEpBC,MAAQC,SACJC,QAAQC,SAASF,OAAQ9C,SAEzB8C,OAAOG,GAAG,WAAW,gBAMZC,cAAgBC,KAAKC,cAAcF,iBAE5CJ,OAAOG,GAAG,QAAQ,wCAEIH,OAAQ,QAAS,gBAjJ1B,EAACA,OAAQ7C,cAC1BoD,0BAA4B,EAE5BA,0BADApD,OAAO2B,aACqB3B,OAAO2B,aAIP3B,OAAOqD,MAAQC,WAAWC,OAAOC,iBAAiBxD,QAAQyD,aAAe,IAExEZ,OAAOa,eAAevD,cAAc,qBAAqBwB,aAC3DyB,4BAE3BP,OAAOa,eAAevD,cAAc,qBAAqBwD,MAAMjC,iBAAY0B,kCAuInEQ,CAAiBf,OAAQ7C,WAG7BA,OAAO6D,iBAAiB,sBAAsB,wCACxBhB,OAAQ7C,WAG9BA,OAAO8D,cAAc,IAAIC,MAAM,iCAIvC3C,OAAO4C,SAAU,4BAAkB5C,OAAO4C,QAAS,UAAW,cAAc,GAC5E5C,OAAO4C,SAAU,2BAAiB5C,OAAO4C,QAAS,UAAW,QAG7D5C,OAAO4C,SAAU,4BAAkB5C,OAAO4C,QAAS,iBAAkB,aAAa,GAClF5C,OAAO4C,SAAU,4BAAkB5C,OAAO4C,QAAS,iBAAkB,CAAC,MAAO,QAG7E5C,OAAO4C,SAAU,8BAAoB5C,OAAO4C,QAAS,YAAa,gBAE3D5C,QAcL6C,uBAAyB,CAACjE,OAAQI,QAASL,QAASmE,sBAChDpF,YACFA,YADEC,aAEFA,cACAmF,aAMEC,eAAiBhD,kBAAkBnB,OAAQI,EAASL,QAASjB,oBAK/DqF,eAAeC,KAAKC,OACpBF,eAAeC,KAAKC,KAAKC,MAAQ,IAKjCH,eAAeC,KAAKG,SACpBJ,eAAeC,KAAKG,OAAOD,MAAQH,eAAeC,KAAKG,OAAOD,MAEzDE,QAAQ,cAAe,IACvBA,QAAQ,cAAe,IAGvBA,QAAQ,eAAgB,IAGxBA,QAAQ,aAAc,IAGtBA,QAAQ,WAAY,IAGpBC,WAAW,UAAW,MAI/BN,eAAeO,4BAA8BP,eAAeO,4BAA4BF,QAAQ,QAAS,eAMzGzF,aAAaG,QAAQH,cAAmD,mBAA3BA,aAAa4F,YAA0BC,SAAS7F,qBACnF8F,uBAAyB9F,aAAa4F,UAAUR,eAAgBpE,SACtEsB,OAAOC,OAAO6C,eAAgBU,2BAIlCxD,OAAOC,OAAO6C,eAAgBrB,QAAQgC,8BAA8B/E,UAE7DoE,gBASLY,YAAe/E,UACRA,OAAOgF,QAAQ,yBAUf/E,eAAiB5B,eAAM2B,YAAQD,+DAAU,SAC5CL,SAAWJ,sBAAsBU,WACnCN,gBACOnB,QAAQM,QAAQa,gBAIrBuF,eAAiB,IAAIC,iBAAQ,qCAG7BhE,QAAUD,WAAWlB,UAGpBK,QAAS8D,oBAAsB3F,QAAQC,IAAI,EAC9C,wBACAJ,iBAAiBiD,OAAO8D,KAAKjE,YAUjCd,QAAQT,MAAMT,QAAQ2D,SAAYA,OAAOuC,aAAaC,cAAaT,SAAS/B,SACxEA,OAAOhD,kBAILyF,eAAiBlF,QAAQmF,cAAc5F,IAAIK,OAAOwF,OACpDF,eAAgB,IACZA,eAAeF,eAAiBpF,cAChCiF,eAAepG,UACRN,QAAQM,QAAQyG,sBAEvBL,eAAepG,UACT,IAAI4G,MAAM,2EAKlBtB,eAAiBF,uBAAuBjE,OAAQI,EAASL,QAASmE,eAIjErB,cAAgBzC,QAAQsF,KAAKvB,uBAGpCnE,OAAO2F,QAAQC,UAAY,SAG3B3H,YAAY4H,IAAI7F,OAAQ6C,QACxBA,OAAOG,GAAG,UAAU8C,YAAC9F,OAACA,cAElB/B,YAAY8H,OAAO/F,OAAOgG,WAC1BhG,OAAOgG,UAAUL,QAAQC,UAAY,QAOrC5F,OAAOiG,0BACAjG,OAAOiG,MAAMjD,GAAG,UAAU,KAC7BH,OAAOqD,UAKfrD,OAAOG,GAAG,QAAQ,KACdH,OAAOqD,UAIXrD,OAAOG,GAAG,cAAc,WACdmD,OAAS5G,SAAS6G,iBAAiB,yBACrCD,QACAA,OAAOvB,SAASyB,QACPA,MAAMC,UAAUC,SAAS,SAC1BF,MAAMC,UAAUE,IAAI,cAOpC3D,OAAOG,GAAG,eAAe,QACjB+B,YAAY/E,QAAS,OACfmG,OAAS5G,SAAS6G,iBAAiB,yBACrCD,QACAA,OAAOvB,SAASyB,QACRA,MAAMC,UAAUC,SAAS,SACzBF,MAAMC,UAAUzG,OAAO,eAO3CoF,eAAepG,UACRgE,+EAU2B,eAAC9C,+DAAU,GAC7C5B,eAAiB4B"}
\ No newline at end of file
+{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"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 * TinyMCE Editor Manager.\n *\n * @module editor_tiny/editor\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport Pending from 'core/pending';\nimport {getDefaultConfiguration} from './defaults';\nimport {getTinyMCE, baseUrl} from './loader';\nimport * as Options from './options';\nimport {addToolbarButton, addToolbarButtons, addToolbarSection,\n removeToolbarButton, removeSubmenuItem, updateEditorState} from './utils';\n\n/**\n * Storage for the TinyMCE instances on the page.\n * @type {Map}\n */\nconst instanceMap = new Map();\n\n/**\n * The default editor configuration.\n * @type {Object}\n */\nlet defaultOptions = {};\n\n/**\n * Require the modules for the named set of TinyMCE plugins.\n *\n * @param {string[]} pluginList The list of plugins\n * @return {Promise[]} A matching set of Promises relating to the requested plugins\n */\nconst importPluginList = async(pluginList) => {\n // Fetch all of the plugins from the list of plugins.\n // If a plugin contains a '/' then it is assumed to be a Moodle AMD module to import.\n const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {\n if (pluginPath.indexOf('/') === -1) {\n // A standard TinyMCE Plugin.\n return Promise.resolve(pluginPath);\n }\n\n return import(pluginPath);\n }));\n\n // Normalise the plugin data to a list of plugin names.\n // Two formats are supported:\n // - a string; and\n // - an array whose first element is the plugin name, and the second element is the plugin configuration.\n const pluginNames = pluginHandlers.map((pluginConfig) => {\n if (typeof pluginConfig === 'string') {\n return pluginConfig;\n }\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[0];\n }\n return null;\n }).filter((value) => value);\n\n // Fetch the list of pluginConfig handlers.\n const pluginConfig = pluginHandlers.map((pluginConfig) => {\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[1];\n }\n return null;\n }).filter((value) => value);\n\n return {\n pluginNames,\n pluginConfig,\n };\n};\n\n/**\n * Fetch the language data for the specified language.\n *\n * @param {string} language The language identifier\n * @returns {object}\n */\nconst fetchLanguage = (language) => fetch(\n `${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}`\n).then(response => response.json());\n\n/**\n * Get a list of all Editors in a Map, keyed by the DOM Node that the Editor is associated with.\n *\n * @returns {Map}\n */\nexport const getAllInstances = () => new Map(instanceMap.entries());\n\n/**\n * Get the TinyMCE instance for the specified Node ID.\n *\n * @param {string} elementId\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));\n\n/*\n * Get the TinyMCE instance for the specified HTMLElement.\n *\n * @param {HTMLElement} element\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElement = element => {\n const instance = instanceMap.get(element);\n if (instance && instance.removed) {\n instanceMap.remove(element);\n return undefined;\n }\n return instance;\n};\n\n/**\n * Set up TinyMCE for the selector at the specified HTML Node id.\n *\n * @param {object} config The configuration required to setup the editor\n * @param {string} config.elementId The HTML Node ID\n * @param {Object} config.options The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForElementId = ({elementId, options}) => {\n const target = document.getElementById(elementId);\n return setupForTarget(target, options);\n};\n\n/**\n * Initialise the page with standard TinyMCE requirements.\n *\n * Currently this includes the language taken from the HTML lang property.\n */\nconst initialisePage = async() => {\n const lang = document.querySelector('html').lang;\n\n const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]);\n tinyMCE.addI18n(lang, langData);\n};\ninitialisePage();\n\n/**\n * Get the list of plugins to load for the specified configuration.\n *\n * If the specified configuration does not include a plugin configuration, then return the default configuration.\n *\n * @param {object} options\n * @param {array} [options.plugins=null] The plugin list\n * @returns {object}\n */\nconst getPlugins = ({plugins = null} = {}) => {\n if (plugins) {\n return plugins;\n }\n\n if (defaultOptions.plugins) {\n return defaultOptions.plugins;\n }\n\n return {};\n};\n\n/**\n * Adjust the editor size base on the target element.\n *\n * @param {TinyMCE} editor TinyMCE editor\n * @param {Node} target Target element\n */\nconst adjustEditorSize = (editor, target) => {\n let expectedEditingAreaHeight = 0;\n if (target.clientHeight) {\n expectedEditingAreaHeight = target.clientHeight;\n } else {\n // If the target element is hidden, we cannot get the lineHeight of the target element.\n // We don't have a proper way to retrieve the general lineHeight of the theme, so we use 22 here, it's equivalent to 1.5em.\n expectedEditingAreaHeight = target.rows * (parseFloat(window.getComputedStyle(target).lineHeight) || 22);\n }\n const currentEditingAreaHeight = editor.getContainer().querySelector('.tox-sidebar-wrap').clientHeight;\n if (currentEditingAreaHeight < expectedEditingAreaHeight) {\n // Change the height based on the target element's height.\n editor.getContainer().querySelector('.tox-sidebar-wrap').style.height = `${expectedEditingAreaHeight}px`;\n }\n};\n\n/**\n * Get the standard configuration for the specified options.\n *\n * @param {Node} target\n * @param {tinyMCE} tinyMCE\n * @param {object} options\n * @param {Array} plugins\n * @returns {object}\n */\nconst getStandardConfig = (target, tinyMCE, options, plugins) => {\n const lang = document.querySelector('html').lang;\n\n const config = Object.assign({}, getDefaultConfiguration(), {\n // eslint-disable-next-line camelcase\n base_url: baseUrl,\n\n // Set the editor target.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target\n target,\n\n // https://www.tiny.cloud/docs/tinymce/6/customize-ui/#set-maximum-and-minimum-heights-and-widths\n // Set the minimum height to the smallest height that we can fit the Menu bar, Tool bar, Status bar and the text area.\n // eslint-disable-next-line camelcase\n min_height: 175,\n\n // Base the height on the size of the text area.\n // In some cases, E.g.: The target is an advanced element, it will be hidden. We cannot get the height at this time.\n // So set the height to auto, and adjust it later by adjustEditorSize().\n height: target.clientHeight || 'auto',\n\n // Set the language.\n // https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language\n // eslint-disable-next-line camelcase\n language: lang,\n\n // Load the editor stylesheet into the editor iframe.\n // https://www.tiny.cloud/docs/tinymce/6/add-css-options/\n // eslint-disable-next-line camelcase\n content_css: [\n options.css,\n ],\n\n // Do not convert URLs to relative URLs.\n // https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls\n // eslint-disable-next-line camelcase\n convert_urls: false,\n\n // Enabled 'advanced' a11y options.\n // This includes allowing role=\"presentation\" from the image uploader.\n // https://www.tiny.cloud/docs/tinymce/6/accessibility/\n // eslint-disable-next-line camelcase\n a11y_advanced_options: true,\n\n // Add specific rules to the valid elements.\n // eslint-disable-next-line camelcase\n extended_valid_elements: 'script[*],p[*],i[*]',\n\n // Disable XSS Sanitisation.\n // We do this in PHP.\n // https://www.tiny.cloud/docs/tinymce/6/security/#turning-dompurify-off\n // Note: This feature has been backported from TinyMCE 6.4.0.\n // eslint-disable-next-line camelcase\n xss_sanitization: false,\n\n // Disable quickbars entirely.\n // The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle.\n // eslint-disable-next-line camelcase\n quickbars_insert_toolbar: '',\n\n // Override the standard block formats property (removing h1 & h2).\n // https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats\n // eslint-disable-next-line camelcase\n block_formats: 'Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre',\n\n // The list of plugins to include in the instance.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins\n plugins: [\n ...plugins,\n ],\n\n // Skins\n skin: 'oxide',\n\n // Do not show the help link in the status bar.\n // https://www.tiny.cloud/docs/tinymce/latest/accessibility/#help_accessibility\n // eslint-disable-next-line camelcase\n help_accessibility: false,\n\n // Remove the \"Upgrade\" link for Tiny.\n // https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/\n promotion: false,\n\n // Allow the administrator to disable branding.\n // https://www.tiny.cloud/docs/tinymce/6/statusbar-configuration-options/#branding\n branding: options.branding,\n\n // Put th cells in a thead element.\n // https://www.tiny.cloud/docs/tinymce/6/table-options/#table_header_type\n // eslint-disable-next-line camelcase\n table_header_type: 'sectionCells',\n\n // Stored text in non-entity form.\n // https://www.tiny.cloud/docs/tinymce/6/content-filtering/#entity_encoding\n // eslint-disable-next-line camelcase\n entity_encoding: \"raw\",\n\n // Enable support for editors in scrollable containers.\n // https://www.tiny.cloud/docs/tinymce/6/ui-mode-configuration-options/#ui_mode\n // eslint-disable-next-line camelcase\n ui_mode: 'split',\n\n // Enable browser-supported spell checking.\n // https://www.tiny.cloud/docs/tinymce/latest/spelling/\n // eslint-disable-next-line camelcase\n browser_spellcheck: true,\n\n setup: (editor) => {\n Options.register(editor, options);\n\n editor.on('PreInit', function() {\n // Work around a bug in TinyMCE with Firefox.\n // When an editor is removed, and replaced with an identically attributed editor (same ID),\n // and the Firefox window is freshly opened (e.g. Behat, Private browsing), the wrong contentWindow\n // is assigned to the editor instance leading to an NS_ERROR_UNEXPECTED error in Firefox.\n // This is a workaround for that issue.\n this.contentWindow = this.iframeElement.contentWindow;\n });\n editor.on('init', function() {\n // Hide justify alignment sub-menu.\n removeSubmenuItem(editor, 'align', 'tiny:justify');\n // Adjust the editor size.\n adjustEditorSize(editor, target);\n });\n\n target.addEventListener('form:editorUpdated', function() {\n updateEditorState(editor, target);\n });\n\n target.dispatchEvent(new Event('form:editorUpdated'));\n },\n });\n\n config.toolbar = addToolbarSection(config.toolbar, 'content', 'formatting', true);\n config.toolbar = addToolbarButton(config.toolbar, 'content', 'link');\n\n // Add directionality plugins, always.\n config.toolbar = addToolbarSection(config.toolbar, 'directionality', 'alignment', true);\n config.toolbar = addToolbarButtons(config.toolbar, 'directionality', ['ltr', 'rtl']);\n\n // Remove the align justify button from the toolbar.\n config.toolbar = removeToolbarButton(config.toolbar, 'alignment', 'alignjustify');\n\n return config;\n};\n\n/**\n * Fetch the TinyMCE configuration for this editor instance.\n *\n * @param {HTMLElement} target\n * @param {TinyMCE} tinyMCE The TinyMCE API\n * @param {Object} options The editor plugin configuration\n * @param {object} pluginValues\n * @param {object} pluginValues.pluginConfig The list of plugin configuration\n * @param {object} pluginValues.pluginNames The list of plugins to load\n * @returns {object} The TinyMCE Configuration\n */\nconst getEditorConfiguration = (target, tinyMCE, options, pluginValues) => {\n const {\n pluginNames,\n pluginConfig,\n } = pluginValues;\n\n // Allow plugins to modify the configuration.\n // This seems a little strange, but we must double-process the config slightly.\n\n // First we fetch the standard configuration.\n const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);\n\n // Next we make any standard changes.\n // Here we remove the file menu, as it doesn't offer any useful functionality.\n // We only empty the items list so that a plugin may choose to add to it themselves later if they wish.\n if (instanceConfig.menu.file) {\n instanceConfig.menu.file.items = '';\n }\n\n // We disable the styles, backcolor, and forecolor plugins from the format menu.\n // These are not useful for Moodle and we don't want to encourage their use.\n if (instanceConfig.menu.format) {\n instanceConfig.menu.format.items = instanceConfig.menu.format.items\n // Remove forecolor and backcolor.\n .replace(/forecolor ?/, '')\n .replace(/backcolor ?/, '')\n\n // Remove fontfamily for now.\n .replace(/fontfamily ?/, '')\n\n // Remove fontsize for now.\n .replace(/fontsize ?/, '')\n\n // Remove styles - it just duplicates the format menu in a way which does not respect configuration\n .replace(/styles ?/, '')\n\n // Remove any duplicate separators.\n .replaceAll(/\\| *\\|/g, '|');\n }\n\n // eslint-disable-next-line camelcase\n instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6');\n\n // Next we call the `configure` function for any plugin which defines it.\n // We pass the current instanceConfig in here, to allow them to make certain changes to the global configuration.\n // For example, to add themselves to any menu, toolbar, and so on.\n // Any plugin which wishes to have configuration options must register those options here.\n pluginConfig.filter((pluginConfig) => typeof pluginConfig.configure === 'function').forEach((pluginConfig) => {\n const pluginInstanceOverride = pluginConfig.configure(instanceConfig, options);\n Object.assign(instanceConfig, pluginInstanceOverride);\n });\n\n // Next we convert the plugin configuration into a format that TinyMCE understands.\n Object.assign(instanceConfig, Options.getInitialPluginConfiguration(options));\n\n return instanceConfig;\n};\n\n/**\n * Check if the target for TinyMCE is in a modal or not.\n *\n * @param {HTMLElement} target Target to check\n * @returns {boolean} True if the target is in a modal form.\n */\nconst isModalMode = (target) => {\n return !!target.closest('[data-region=\"modal\"]');\n};\n\n/**\n * Set up TinyMCE for the HTML Element.\n *\n * @param {HTMLElement} target\n * @param {Object} [options={}] The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForTarget = async(target, options = {}) => {\n const instance = getInstanceForElement(target);\n if (instance) {\n return Promise.resolve(instance);\n }\n\n // Register a new pending promise to ensure that Behat waits for the editor setup to complete before continuing.\n const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');\n\n // Get the list of plugins.\n const plugins = getPlugins(options);\n\n // Fetch the tinyMCE API, and instantiate the plugins.\n const [tinyMCE, pluginValues] = await Promise.all([\n getTinyMCE(),\n importPluginList(Object.keys(plugins)),\n ]);\n\n // TinyMCE uses the element ID as a map key internally, even if the target has changed.\n // In the case where we have an editor in a modal form which has been detached from the DOM, but the editor not removed,\n // we need to manually destroy the editor.\n // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,\n // or added back elsewhere in the DOM.\n\n // First remove any detached editors.\n tinyMCE.get().filter((editor) => !editor.getElement().isConnected).forEach((editor) => {\n editor.remove();\n });\n\n // Now check for any existing editor which shares the same ID.\n const existingEditor = tinyMCE.EditorManager.get(target.id);\n if (existingEditor) {\n if (existingEditor.getElement() === target) {\n pendingPromise.resolve();\n return Promise.resolve(existingEditor);\n } else {\n pendingPromise.resolve();\n throw new Error('TinyMCE instance already exists for different target with same ID');\n }\n }\n\n // Get the editor configuration for this editor.\n const instanceConfig = getEditorConfiguration(target, tinyMCE, options, pluginValues);\n\n // Initialise the editor instance for the given configuration.\n // At this point any plugin which has configuration options registered will have them applied for this instance.\n const [editor] = await tinyMCE.init(instanceConfig);\n\n // Update the textarea when the editor to set the field type for Behat.\n target.dataset.fieldtype = 'editor';\n\n // Store the editor instance in the instanceMap and register a listener on removal to remove it from the map.\n instanceMap.set(target, editor);\n editor.on('remove', ({target}) => {\n // Handle removal of the editor from the map on destruction.\n instanceMap.delete(target.targetElm);\n target.targetElm.dataset.fieldtype = null;\n });\n\n // If the editor is part of a form, also listen to the jQuery submit event.\n // The jQuery submit event will not trigger the native submit event, and therefore the content will not be saved.\n // We cannot rely on listening to the bubbled submit event on the document because other events on child nodes may\n // consume the data before it is saved.\n if (target.form) {\n jQuery(target.form).on('submit', () => {\n editor.save();\n });\n }\n\n // Save the editor content to the textarea when the editor is blurred.\n editor.on('blur', () => {\n editor.save();\n });\n\n // If the editor is in a modal, we need to hide the modal when window editor's window is opened.\n editor.on('OpenWindow', () => {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (!modal.classList.contains('hide')) {\n modal.classList.add('hide');\n }\n });\n }\n });\n\n // If the editor's window is closed, we need to show the hidden modal back.\n editor.on('CloseWindow', () => {\n if (isModalMode(target)) {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (modal.classList.contains('hide')) {\n modal.classList.remove('hide');\n }\n });\n }\n }\n });\n\n pendingPromise.resolve();\n return editor;\n};\n\n/**\n * Set the default editor configuration.\n *\n * This configuration is used when an editor is initialised without any configuration.\n *\n * @param {object} [options={}]\n */\nexport const configureDefaultEditor = (options = {}) => {\n defaultOptions = options;\n};\n"],"names":["instanceMap","Map","defaultOptions","importPluginList","async","pluginHandlers","Promise","all","pluginList","map","pluginPath","indexOf","resolve","pluginNames","pluginConfig","Array","isArray","filter","value","entries","elementId","getInstanceForElement","document","getElementById","element","instance","get","removed","remove","_ref","options","target","setupForTarget","lang","querySelector","tinyMCE","langData","language","fetch","M","cfg","wwwroot","langrev","then","response","json","addI18n","initialisePage","getPlugins","plugins","getStandardConfig","config","Object","assign","base_url","baseUrl","min_height","height","clientHeight","content_css","css","convert_urls","a11y_advanced_options","extended_valid_elements","xss_sanitization","quickbars_insert_toolbar","block_formats","skin","help_accessibility","promotion","branding","table_header_type","entity_encoding","ui_mode","browser_spellcheck","setup","editor","Options","register","on","contentWindow","this","iframeElement","expectedEditingAreaHeight","rows","parseFloat","window","getComputedStyle","lineHeight","getContainer","style","adjustEditorSize","addEventListener","dispatchEvent","Event","toolbar","getEditorConfiguration","pluginValues","instanceConfig","menu","file","items","format","replace","replaceAll","quickbars_selection_toolbar","configure","forEach","pluginInstanceOverride","getInitialPluginConfiguration","isModalMode","closest","pendingPromise","Pending","keys","getElement","isConnected","existingEditor","EditorManager","id","Error","init","dataset","fieldtype","set","_ref2","delete","targetElm","form","save","modals","querySelectorAll","modal","classList","contains","add"],"mappings":"4oDAmCMA,YAAc,IAAIC,QAMpBC,eAAiB,SAQfC,iBAAmBC,MAAAA,mBAGfC,qBAAuBC,QAAQC,IAAIC,WAAWC,KAAIC,aACnB,IAA7BA,WAAWC,QAAQ,KAEZL,QAAQM,QAAQF,4NAGbA,4WAAAA,gBAOZG,YAAcR,eAAeI,KAAKK,cACR,iBAAjBA,aACAA,aAEPC,MAAMC,QAAQF,cACPA,aAAa,GAEjB,OACRG,QAAQC,OAAUA,cAUd,CACHL,YAAAA,YACAC,aATiBT,eAAeI,KAAKK,cACjCC,MAAMC,QAAQF,cACPA,aAAa,GAEjB,OACRG,QAAQC,OAAUA,mCAuBM,IAAM,IAAIjB,IAAID,YAAYmB,4CAQlBC,WAAaC,sBAAsBC,SAASC,eAAeH,kBAQrFC,sBAAwBG,gBAC3BC,SAAWzB,YAAY0B,IAAIF,aAC7BC,WAAYA,SAASE,eAIlBF,SAHHzB,YAAY4B,OAAOJ,0FAcMK,WAACT,UAACA,UAADU,QAAYA,oBACpCC,OAAST,SAASC,eAAeH,kBAChCY,eAAeD,OAAQD,UAQX1B,iBACb6B,KAAOX,SAASY,cAAc,QAAQD,MAErCE,QAASC,gBAAkB9B,QAAQC,IAAI,EAAC,yBAvD5B8B,SAuDwDJ,KAvD3CK,gBAC7BC,EAAEC,IAAIC,6CAAoCF,EAAEC,IAAIE,oBAAWL,WAChEM,MAAKC,UAAYA,SAASC,YAFLR,IAAAA,SAwDnBF,QAAQW,QAAQb,KAAMG,WAE1BW,SAWMC,WAAa,eAACC,QAACA,QAAU,6DAAQ,UAC/BA,UAIA/C,eAAe+C,QACR/C,eAAe+C,QAGnB,KAkCLC,kBAAoB,CAACnB,OAAQI,QAASL,QAASmB,iBAC3ChB,KAAOX,SAASY,cAAc,QAAQD,KAEtCkB,OAASC,OAAOC,OAAO,IAAI,uCAA2B,CAExDC,SAAUC,gBAIVxB,OAAAA,OAKAyB,WAAY,IAKZC,OAAQ1B,OAAO2B,cAAgB,OAK/BrB,SAAUJ,KAKV0B,YAAa,CACT7B,QAAQ8B,KAMZC,cAAc,EAMdC,uBAAuB,EAIvBC,wBAAyB,sBAOzBC,kBAAkB,EAKlBC,yBAA0B,GAK1BC,cAAe,mFAIfjB,QAAS,IACFA,SAIPkB,KAAM,QAKNC,oBAAoB,EAIpBC,WAAW,EAIXC,SAAUxC,QAAQwC,SAKlBC,kBAAmB,eAKnBC,gBAAiB,MAKjBC,QAAS,QAKTC,oBAAoB,EAEpBC,MAAQC,SACJC,QAAQC,SAASF,OAAQ9C,SAEzB8C,OAAOG,GAAG,WAAW,gBAMZC,cAAgBC,KAAKC,cAAcF,iBAE5CJ,OAAOG,GAAG,QAAQ,wCAEIH,OAAQ,QAAS,gBAjJ1B,EAACA,OAAQ7C,cAC1BoD,0BAA4B,EAE5BA,0BADApD,OAAO2B,aACqB3B,OAAO2B,aAIP3B,OAAOqD,MAAQC,WAAWC,OAAOC,iBAAiBxD,QAAQyD,aAAe,IAExEZ,OAAOa,eAAevD,cAAc,qBAAqBwB,aAC3DyB,4BAE3BP,OAAOa,eAAevD,cAAc,qBAAqBwD,MAAMjC,iBAAY0B,kCAuInEQ,CAAiBf,OAAQ7C,WAG7BA,OAAO6D,iBAAiB,sBAAsB,wCACxBhB,OAAQ7C,WAG9BA,OAAO8D,cAAc,IAAIC,MAAM,iCAIvC3C,OAAO4C,SAAU,4BAAkB5C,OAAO4C,QAAS,UAAW,cAAc,GAC5E5C,OAAO4C,SAAU,2BAAiB5C,OAAO4C,QAAS,UAAW,QAG7D5C,OAAO4C,SAAU,4BAAkB5C,OAAO4C,QAAS,iBAAkB,aAAa,GAClF5C,OAAO4C,SAAU,4BAAkB5C,OAAO4C,QAAS,iBAAkB,CAAC,MAAO,QAG7E5C,OAAO4C,SAAU,8BAAoB5C,OAAO4C,QAAS,YAAa,gBAE3D5C,QAcL6C,uBAAyB,CAACjE,OAAQI,QAASL,QAASmE,sBAChDpF,YACFA,YADEC,aAEFA,cACAmF,aAMEC,eAAiBhD,kBAAkBnB,OAAQI,EAASL,QAASjB,oBAK/DqF,eAAeC,KAAKC,OACpBF,eAAeC,KAAKC,KAAKC,MAAQ,IAKjCH,eAAeC,KAAKG,SACpBJ,eAAeC,KAAKG,OAAOD,MAAQH,eAAeC,KAAKG,OAAOD,MAEzDE,QAAQ,cAAe,IACvBA,QAAQ,cAAe,IAGvBA,QAAQ,eAAgB,IAGxBA,QAAQ,aAAc,IAGtBA,QAAQ,WAAY,IAGpBC,WAAW,UAAW,MAI/BN,eAAeO,4BAA8BP,eAAeO,4BAA4BF,QAAQ,QAAS,eAMzGzF,aAAaG,QAAQH,cAAmD,mBAA3BA,aAAa4F,YAA0BC,SAAS7F,qBACnF8F,uBAAyB9F,aAAa4F,UAAUR,eAAgBpE,SACtEsB,OAAOC,OAAO6C,eAAgBU,2BAIlCxD,OAAOC,OAAO6C,eAAgBrB,QAAQgC,8BAA8B/E,UAE7DoE,gBASLY,YAAe/E,UACRA,OAAOgF,QAAQ,yBAUf/E,eAAiB5B,eAAM2B,YAAQD,+DAAU,SAC5CL,SAAWJ,sBAAsBU,WACnCN,gBACOnB,QAAQM,QAAQa,gBAIrBuF,eAAiB,IAAIC,iBAAQ,qCAG7BhE,QAAUD,WAAWlB,UAGpBK,QAAS8D,oBAAsB3F,QAAQC,IAAI,EAC9C,wBACAJ,iBAAiBiD,OAAO8D,KAAKjE,YAUjCd,QAAQT,MAAMT,QAAQ2D,SAAYA,OAAOuC,aAAaC,cAAaT,SAAS/B,SACxEA,OAAOhD,kBAILyF,eAAiBlF,QAAQmF,cAAc5F,IAAIK,OAAOwF,OACpDF,eAAgB,IACZA,eAAeF,eAAiBpF,cAChCiF,eAAepG,UACRN,QAAQM,QAAQyG,sBAEvBL,eAAepG,UACT,IAAI4G,MAAM,2EAKlBtB,eAAiBF,uBAAuBjE,OAAQI,EAASL,QAASmE,eAIjErB,cAAgBzC,QAAQsF,KAAKvB,uBAGpCnE,OAAO2F,QAAQC,UAAY,SAG3B3H,YAAY4H,IAAI7F,OAAQ6C,QACxBA,OAAOG,GAAG,UAAU8C,YAAC9F,OAACA,cAElB/B,YAAY8H,OAAO/F,OAAOgG,WAC1BhG,OAAOgG,UAAUL,QAAQC,UAAY,QAOrC5F,OAAOiG,0BACAjG,OAAOiG,MAAMjD,GAAG,UAAU,KAC7BH,OAAOqD,UAKfrD,OAAOG,GAAG,QAAQ,KACdH,OAAOqD,UAIXrD,OAAOG,GAAG,cAAc,WACdmD,OAAS5G,SAAS6G,iBAAiB,yBACrCD,QACAA,OAAOvB,SAASyB,QACPA,MAAMC,UAAUC,SAAS,SAC1BF,MAAMC,UAAUE,IAAI,cAOpC3D,OAAOG,GAAG,eAAe,QACjB+B,YAAY/E,QAAS,OACfmG,OAAS5G,SAAS6G,iBAAiB,yBACrCD,QACAA,OAAOvB,SAASyB,QACRA,MAAMC,UAAUC,SAAS,SACzBF,MAAMC,UAAUzG,OAAO,eAO3CoF,eAAepG,UACRgE,+EAU2B,eAAC9C,+DAAU,GAC7C5B,eAAiB4B"}
\ No newline at end of file
diff --git a/lib/editor/tiny/amd/src/editor.js b/lib/editor/tiny/amd/src/editor.js
index 361ecaf710e9e..65760cf47e596 100644
--- a/lib/editor/tiny/amd/src/editor.js
+++ b/lib/editor/tiny/amd/src/editor.js
@@ -268,7 +268,7 @@ const getStandardConfig = (target, tinyMCE, options, plugins) => {
// Override the standard block formats property (removing h1 & h2).
// https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats
// eslint-disable-next-line camelcase
- block_formats: 'Paragraph=p; Heading 3=h3; Heading 4=h4; Heading 5=h5; Heading 6=h6; Preformatted=pre',
+ block_formats: 'Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre',
// The list of plugins to include in the instance.
// https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins
diff --git a/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js b/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js
index 621fad27a73a5..c084ef3e6350f 100644
--- a/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js
+++ b/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js
@@ -1,3 +1,3 @@
-define("tiny_premium/configuration",["exports","editor_tiny/utils"],(function(_exports,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{let plugins=instanceConfig.plugins,menu=instanceConfig.menu,toolbar=(toolbar=>(toolbar=(0,_utils.addToolbarSection)(toolbar,"premium_a","advanced",!0),(0,_utils.addToolbarSection)(toolbar,"premium_b","formatting",!0)))(instanceConfig.toolbar),contextmenu=instanceConfig.contextmenu;return plugins+=" advtable",menu=(0,_utils.addMenubarItem)(menu,"table","| advtablerownumbering","advtablesort"),plugins+=" editimage",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","editimage","tiny_media_image"),instanceConfig.editimage_toolbar="rotateleft rotateright flipv fliph editimage",plugins+=" export",menu=(0,_utils.addMenubarItem)(menu,"tools","| export"),plugins+=" pageembed",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","pageembed","tiny_media_video"),plugins+=" typography",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_b","typography"),plugins+=" casechange",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","casechange"),plugins+=" checklist",toolbar=(0,_utils.addToolbarButton)(toolbar,"lists","checklist"),plugins+=" tinymcespellchecker",menu=(0,_utils.addMenubarItem)(menu,"tools","spellcheckdialog","spellcheckerlanguage"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"spellchecker"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","spellcheckdialog"),plugins+=" autocorrect",menu=(0,_utils.addMenubarItem)(menu,"tools","| autocorrect capitalization","spellcheckdialog"),plugins+=" permanentpen",menu=(0,_utils.addMenubarItem)(menu,"format","| permanentpen configurepermanentpen"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","permanentpen"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"configurepermanentpen"),plugins+=" formatpainter",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","formatpainter"),plugins+=" linkchecker",contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"linkchecker"),plugins+=" tableofcontents",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","tableofcontents"),plugins+=" footnotes",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","footnotes"),menu=(0,_utils.addMenubarItem)(menu,"insert","footnotes","tableofcontents"),plugins+=" powerpaste",{plugins:plugins,toolbar:toolbar,menu:menu,contextmenu:contextmenu}}}));
+define("tiny_premium/configuration",["exports","editor_tiny/utils","editor_tiny/options"],(function(_exports,_utils,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=(instanceConfig,options)=>{const enabledPremiumPlugins=(0,_options.getInitialPluginConfiguration)(options)[(0,_options.getPluginOptionName)("tiny_premium/plugin","premiumplugins")].split(",");let plugins=instanceConfig.plugins,menu=instanceConfig.menu,toolbar=(toolbar=>(toolbar=(0,_utils.addToolbarSection)(toolbar,"premium_a","advanced",!0),(0,_utils.addToolbarSection)(toolbar,"premium_b","formatting",!0)))(instanceConfig.toolbar),contextmenu=instanceConfig.contextmenu;return-1!==enabledPremiumPlugins.indexOf("advtable")&&(plugins+=" advtable",menu=(0,_utils.addMenubarItem)(menu,"table","| advtablerownumbering","advtablesort")),-1!==enabledPremiumPlugins.indexOf("editimage")&&(plugins+=" editimage",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","editimage","tiny_media_image"),instanceConfig.editimage_toolbar="rotateleft rotateright flipv fliph editimage"),-1!==enabledPremiumPlugins.indexOf("export")&&(plugins+=" export",menu=(0,_utils.addMenubarItem)(menu,"tools","| export")),-1!==enabledPremiumPlugins.indexOf("pageembed")&&(plugins+=" pageembed",toolbar=(0,_utils.addToolbarButton)(toolbar,"content","pageembed","tiny_media_video")),-1!==enabledPremiumPlugins.indexOf("typography")&&(plugins+=" typography",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_b","typography")),-1!==enabledPremiumPlugins.indexOf("casechange")&&(plugins+=" casechange",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","casechange")),-1!==enabledPremiumPlugins.indexOf("checklist")&&(plugins+=" checklist",toolbar=(0,_utils.addToolbarButton)(toolbar,"lists","checklist")),-1!==enabledPremiumPlugins.indexOf("tinymcespellchecker")&&(plugins+=" tinymcespellchecker",menu=(0,_utils.addMenubarItem)(menu,"tools","spellcheckdialog","spellcheckerlanguage"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"spellchecker"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","spellcheckdialog")),-1!==enabledPremiumPlugins.indexOf("autocorrect")&&(plugins+=" autocorrect",menu=(0,_utils.addMenubarItem)(menu,"tools","| autocorrect capitalization","spellcheckdialog")),-1!==enabledPremiumPlugins.indexOf("permanentpen")&&(plugins+=" permanentpen",menu=(0,_utils.addMenubarItem)(menu,"format","| permanentpen configurepermanentpen"),toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","permanentpen"),contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"configurepermanentpen")),-1!==enabledPremiumPlugins.indexOf("formatpainter")&&(plugins+=" formatpainter",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","formatpainter")),-1!==enabledPremiumPlugins.indexOf("linkchecker")&&(plugins+=" linkchecker",contextmenu=(0,_utils.addContextmenuItem)(contextmenu,"linkchecker")),-1!==enabledPremiumPlugins.indexOf("tableofcontents")&&(plugins+=" tableofcontents",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","tableofcontents")),-1!==enabledPremiumPlugins.indexOf("footnotes")&&(plugins+=" footnotes",toolbar=(0,_utils.addToolbarButton)(toolbar,"premium_a","footnotes"),menu=(0,_utils.addMenubarItem)(menu,"insert","footnotes","tableofcontents")),-1!==enabledPremiumPlugins.indexOf("powerpaste")&&(plugins+=" powerpaste"),{plugins:plugins,toolbar:toolbar,menu:menu,contextmenu:contextmenu}}}));
//# sourceMappingURL=configuration.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map b/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map
index ca798b5a1b989..404d85db577f9 100644
--- a/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map
+++ b/lib/editor/tiny/plugins/premium/amd/build/configuration.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"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 * Tiny Premium configuration.\n *\n * @module tiny_premium/configuration\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n addToolbarButton,\n addMenubarItem,\n addToolbarSection,\n addContextmenuItem\n} from 'editor_tiny/utils';\n\nconst configureToolbar = (toolbar) => {\n // Add premium toolbar sections to house all the plugins with no natural home.\n toolbar = addToolbarSection(toolbar, 'premium_a', 'advanced', true);\n toolbar = addToolbarSection(toolbar, 'premium_b', 'formatting', true);\n return toolbar;\n};\n\nexport const configure = (instanceConfig) => {\n // There is some manipulating of the plugin menu, toolbar, context and quickbar items.\n // This was necessary to enhance user experience and closer align to the Tiny demo site.\n let plugins = instanceConfig.plugins;\n let menu = instanceConfig.menu;\n let toolbar = configureToolbar(instanceConfig.toolbar);\n let contextmenu = instanceConfig.contextmenu;\n let pluginsettings = {};\n\n // Advanced Table.\n plugins += ` advtable`;\n menu = addMenubarItem(menu, 'table', '| advtablerownumbering', 'advtablesort');\n\n // Enhanced Image Editing.\n plugins += ` editimage`;\n toolbar = addToolbarButton(toolbar, 'content', 'editimage', 'tiny_media_image');\n // Remove the duplicate image button from the quickbar toolbar by redefining the values without 'imageoptions'.\n // eslint-disable-next-line camelcase\n instanceConfig.editimage_toolbar = 'rotateleft rotateright flipv fliph editimage';\n\n // Export.\n plugins += ` export`;\n menu = addMenubarItem(menu, 'tools', '| export');\n\n // Page Embed.\n plugins += ` pageembed`;\n toolbar = addToolbarButton(toolbar, 'content', 'pageembed', 'tiny_media_video');\n\n // Advanced Typography.\n plugins += ` typography`;\n toolbar = addToolbarButton(toolbar, 'premium_b', 'typography');\n\n // Case Change.\n plugins += ` casechange`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'casechange');\n\n // Checklist.\n plugins += ` checklist`;\n toolbar = addToolbarButton(toolbar, 'lists', 'checklist');\n\n // Spell Checker Pro.\n plugins += ` tinymcespellchecker`;\n menu = addMenubarItem(menu, 'tools', 'spellcheckdialog', 'spellcheckerlanguage');\n contextmenu = addContextmenuItem(contextmenu, 'spellchecker');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'spellcheckdialog');\n\n // Spelling Autocorrect.\n plugins += ` autocorrect`;\n menu = addMenubarItem(menu, 'tools', '| autocorrect capitalization', 'spellcheckdialog');\n\n // Permanent Pen.\n plugins += ` permanentpen`;\n menu = addMenubarItem(menu, 'format', '| permanentpen configurepermanentpen');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'permanentpen');\n contextmenu = addContextmenuItem(contextmenu, 'configurepermanentpen');\n\n // Format Painter.\n plugins += ` formatpainter`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'formatpainter');\n\n // Link Checker.\n plugins += ` linkchecker`;\n contextmenu = addContextmenuItem(contextmenu, 'linkchecker');\n\n // Table of Contents.\n plugins += ` tableofcontents`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'tableofcontents');\n\n // Footnotes.\n plugins += ` footnotes`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'footnotes');\n menu = addMenubarItem(menu, 'insert', 'footnotes', 'tableofcontents');\n\n // Powerpaste.\n plugins += ` powerpaste`;\n\n return {\n plugins,\n toolbar,\n menu,\n contextmenu,\n ...pluginsettings\n };\n};\n"],"names":["instanceConfig","plugins","menu","toolbar","configureToolbar","contextmenu","editimage_toolbar"],"mappings":"oMAqC0BA,qBAGlBC,QAAUD,eAAeC,QACzBC,KAAOF,eAAeE,KACtBC,QAZkBA,CAAAA,UAEtBA,SAAU,4BAAkBA,QAAS,YAAa,YAAY,IACpD,4BAAkBA,QAAS,YAAa,cAAc,IASlDC,CAAiBJ,eAAeG,SAC1CE,YAAcL,eAAeK,mBAIjCJ,qBACAC,MAAO,yBAAeA,KAAM,QAAS,yBAA0B,gBAG/DD,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,oBAG5DH,eAAeM,kBAAoB,+CAGnCL,mBACAC,MAAO,yBAAeA,KAAM,QAAS,YAGrCD,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,oBAG5DF,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,cAGjDF,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,cAGjDF,sBACAE,SAAU,2BAAiBA,QAAS,QAAS,aAG7CF,gCACAC,MAAO,yBAAeA,KAAM,QAAS,mBAAoB,wBACzDG,aAAc,6BAAmBA,YAAa,gBAC9CF,SAAU,2BAAiBA,QAAS,YAAa,oBAGjDF,wBACAC,MAAO,yBAAeA,KAAM,QAAS,+BAAgC,oBAGrED,yBACAC,MAAO,yBAAeA,KAAM,SAAU,wCACtCC,SAAU,2BAAiBA,QAAS,YAAa,gBACjDE,aAAc,6BAAmBA,YAAa,yBAG9CJ,0BACAE,SAAU,2BAAiBA,QAAS,YAAa,iBAGjDF,wBACAI,aAAc,6BAAmBA,YAAa,eAG9CJ,4BACAE,SAAU,2BAAiBA,QAAS,YAAa,mBAGjDF,sBACAE,SAAU,2BAAiBA,QAAS,YAAa,aACjDD,MAAO,yBAAeA,KAAM,SAAU,YAAa,mBAGnDD,uBAEO,CACHA,QAAAA,QACAE,QAAAA,QACAD,KAAAA,KACAG,YAAAA"}
\ No newline at end of file
+{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"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 * Tiny Premium configuration.\n *\n * @module tiny_premium/configuration\n * @copyright 2023 David Woloszyn \n * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n addToolbarButton,\n addMenubarItem,\n addToolbarSection,\n addContextmenuItem\n} from 'editor_tiny/utils';\nimport {\n getInitialPluginConfiguration,\n getPluginOptionName\n} from 'editor_tiny/options';\n\nconst configureToolbar = (toolbar) => {\n // Add premium toolbar sections to house all the plugins with no natural home.\n toolbar = addToolbarSection(toolbar, 'premium_a', 'advanced', true);\n toolbar = addToolbarSection(toolbar, 'premium_b', 'formatting', true);\n return toolbar;\n};\n\nexport const configure = (instanceConfig, options) => {\n // Get the namespaced options for Tiny Premium before they are officially initialised.\n // Due to the timing the plugin options are available, we need to get at the options in this slightly unconventional way.\n const pluginOptions = getInitialPluginConfiguration(options);\n const enabledPremiumPlugins = pluginOptions[getPluginOptionName('tiny_premium/plugin', 'premiumplugins')].split(',');\n\n let plugins = instanceConfig.plugins;\n let menu = instanceConfig.menu;\n let toolbar = configureToolbar(instanceConfig.toolbar);\n let contextmenu = instanceConfig.contextmenu;\n let pluginsettings = {};\n\n // Advanced Table.\n if (enabledPremiumPlugins.indexOf('advtable') !== -1) {\n plugins += ` advtable`;\n menu = addMenubarItem(menu, 'table', '| advtablerownumbering', 'advtablesort');\n }\n // Enhanced Image Editing.\n if (enabledPremiumPlugins.indexOf('editimage') !== -1) {\n plugins += ` editimage`;\n toolbar = addToolbarButton(toolbar, 'content', 'editimage', 'tiny_media_image');\n // Remove the duplicate image button from the quickbar toolbar by redefining the values without 'imageoptions'.\n // eslint-disable-next-line camelcase\n instanceConfig.editimage_toolbar = 'rotateleft rotateright flipv fliph editimage';\n }\n // Export.\n if (enabledPremiumPlugins.indexOf('export') !== -1) {\n plugins += ` export`;\n menu = addMenubarItem(menu, 'tools', '| export');\n }\n // Page Embed.\n if (enabledPremiumPlugins.indexOf('pageembed') !== -1) {\n plugins += ` pageembed`;\n toolbar = addToolbarButton(toolbar, 'content', 'pageembed', 'tiny_media_video');\n }\n // Advanced Typography.\n if (enabledPremiumPlugins.indexOf('typography') !== -1) {\n plugins += ` typography`;\n toolbar = addToolbarButton(toolbar, 'premium_b', 'typography');\n }\n // Case Change.\n if (enabledPremiumPlugins.indexOf('casechange') !== -1) {\n plugins += ` casechange`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'casechange');\n }\n // Checklist.\n if (enabledPremiumPlugins.indexOf('checklist') !== -1) {\n plugins += ` checklist`;\n toolbar = addToolbarButton(toolbar, 'lists', 'checklist');\n }\n // Spell Checker Pro.\n if (enabledPremiumPlugins.indexOf('tinymcespellchecker') !== -1) {\n plugins += ` tinymcespellchecker`;\n menu = addMenubarItem(menu, 'tools', 'spellcheckdialog', 'spellcheckerlanguage');\n contextmenu = addContextmenuItem(contextmenu, 'spellchecker');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'spellcheckdialog');\n }\n // Spelling Autocorrect.\n if (enabledPremiumPlugins.indexOf('autocorrect') !== -1) {\n plugins += ` autocorrect`;\n menu = addMenubarItem(menu, 'tools', '| autocorrect capitalization', 'spellcheckdialog');\n }\n // Permanent Pen.\n if (enabledPremiumPlugins.indexOf('permanentpen') !== -1) {\n plugins += ` permanentpen`;\n menu = addMenubarItem(menu, 'format', '| permanentpen configurepermanentpen');\n toolbar = addToolbarButton(toolbar, 'premium_a', 'permanentpen');\n contextmenu = addContextmenuItem(contextmenu, 'configurepermanentpen');\n }\n // Format Painter.\n if (enabledPremiumPlugins.indexOf('formatpainter') !== -1) {\n plugins += ` formatpainter`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'formatpainter');\n }\n // Link Checker.\n if (enabledPremiumPlugins.indexOf('linkchecker') !== -1) {\n plugins += ` linkchecker`;\n contextmenu = addContextmenuItem(contextmenu, 'linkchecker');\n }\n // Table of Contents.\n if (enabledPremiumPlugins.indexOf('tableofcontents') !== -1) {\n plugins += ` tableofcontents`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'tableofcontents');\n }\n // Footnotes.\n if (enabledPremiumPlugins.indexOf('footnotes') !== -1) {\n plugins += ` footnotes`;\n toolbar = addToolbarButton(toolbar, 'premium_a', 'footnotes');\n menu = addMenubarItem(menu, 'insert', 'footnotes', 'tableofcontents');\n }\n // Powerpaste.\n if (enabledPremiumPlugins.indexOf('powerpaste') !== -1) {\n plugins += ` powerpaste`;\n }\n\n return {\n plugins,\n toolbar,\n menu,\n contextmenu,\n ...pluginsettings\n };\n};\n"],"names":["instanceConfig","options","enabledPremiumPlugins","split","plugins","menu","toolbar","configureToolbar","contextmenu","indexOf","editimage_toolbar"],"mappings":"mOAyCyB,CAACA,eAAgBC,iBAIhCC,uBADgB,0CAA8BD,UACR,gCAAoB,sBAAuB,mBAAmBE,MAAM,SAE5GC,QAAUJ,eAAeI,QACzBC,KAAOL,eAAeK,KACtBC,QAfkBA,CAAAA,UAEtBA,SAAU,4BAAkBA,QAAS,YAAa,YAAY,IACpD,4BAAkBA,QAAS,YAAa,cAAc,IAYlDC,CAAiBP,eAAeM,SAC1CE,YAAcR,eAAeQ,mBAIkB,IAA/CN,sBAAsBO,QAAQ,cAC9BL,qBACAC,MAAO,yBAAeA,KAAM,QAAS,yBAA0B,kBAGf,IAAhDH,sBAAsBO,QAAQ,eAC9BL,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,oBAG5DN,eAAeU,kBAAoB,iDAGU,IAA7CR,sBAAsBO,QAAQ,YAC9BL,mBACAC,MAAO,yBAAeA,KAAM,QAAS,cAGW,IAAhDH,sBAAsBO,QAAQ,eAC9BL,sBACAE,SAAU,2BAAiBA,QAAS,UAAW,YAAa,sBAGX,IAAjDJ,sBAAsBO,QAAQ,gBAC9BL,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,gBAGA,IAAjDJ,sBAAsBO,QAAQ,gBAC9BL,uBACAE,SAAU,2BAAiBA,QAAS,YAAa,gBAGD,IAAhDJ,sBAAsBO,QAAQ,eAC9BL,sBACAE,SAAU,2BAAiBA,QAAS,QAAS,eAGa,IAA1DJ,sBAAsBO,QAAQ,yBAC9BL,gCACAC,MAAO,yBAAeA,KAAM,QAAS,mBAAoB,wBACzDG,aAAc,6BAAmBA,YAAa,gBAC9CF,SAAU,2BAAiBA,QAAS,YAAa,sBAGC,IAAlDJ,sBAAsBO,QAAQ,iBAC9BL,wBACAC,MAAO,yBAAeA,KAAM,QAAS,+BAAgC,sBAGlB,IAAnDH,sBAAsBO,QAAQ,kBAC9BL,yBACAC,MAAO,yBAAeA,KAAM,SAAU,wCACtCC,SAAU,2BAAiBA,QAAS,YAAa,gBACjDE,aAAc,6BAAmBA,YAAa,2BAGM,IAApDN,sBAAsBO,QAAQ,mBAC9BL,0BACAE,SAAU,2BAAiBA,QAAS,YAAa,mBAGC,IAAlDJ,sBAAsBO,QAAQ,iBAC9BL,wBACAI,aAAc,6BAAmBA,YAAa,iBAGQ,IAAtDN,sBAAsBO,QAAQ,qBAC9BL,4BACAE,SAAU,2BAAiBA,QAAS,YAAa,qBAGD,IAAhDJ,sBAAsBO,QAAQ,eAC9BL,sBACAE,SAAU,2BAAiBA,QAAS,YAAa,aACjDD,MAAO,yBAAeA,KAAM,SAAU,YAAa,qBAGF,IAAjDH,sBAAsBO,QAAQ,gBAC9BL,wBAGG,CACHA,QAAAA,QACAE,QAAAA,QACAD,KAAAA,KACAG,YAAAA"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/premium/amd/src/configuration.js b/lib/editor/tiny/plugins/premium/amd/src/configuration.js
index 1b2cfa7e14c57..6b5f9460acadb 100644
--- a/lib/editor/tiny/plugins/premium/amd/src/configuration.js
+++ b/lib/editor/tiny/plugins/premium/amd/src/configuration.js
@@ -27,6 +27,10 @@ import {
addToolbarSection,
addContextmenuItem
} from 'editor_tiny/utils';
+import {
+ getInitialPluginConfiguration,
+ getPluginOptionName
+} from 'editor_tiny/options';
const configureToolbar = (toolbar) => {
// Add premium toolbar sections to house all the plugins with no natural home.
@@ -35,9 +39,12 @@ const configureToolbar = (toolbar) => {
return toolbar;
};
-export const configure = (instanceConfig) => {
- // There is some manipulating of the plugin menu, toolbar, context and quickbar items.
- // This was necessary to enhance user experience and closer align to the Tiny demo site.
+export const configure = (instanceConfig, options) => {
+ // Get the namespaced options for Tiny Premium before they are officially initialised.
+ // Due to the timing the plugin options are available, we need to get at the options in this slightly unconventional way.
+ const pluginOptions = getInitialPluginConfiguration(options);
+ const enabledPremiumPlugins = pluginOptions[getPluginOptionName('tiny_premium/plugin', 'premiumplugins')].split(',');
+
let plugins = instanceConfig.plugins;
let menu = instanceConfig.menu;
let toolbar = configureToolbar(instanceConfig.toolbar);
@@ -45,71 +52,87 @@ export const configure = (instanceConfig) => {
let pluginsettings = {};
// Advanced Table.
- plugins += ` advtable`;
- menu = addMenubarItem(menu, 'table', '| advtablerownumbering', 'advtablesort');
-
+ if (enabledPremiumPlugins.indexOf('advtable') !== -1) {
+ plugins += ` advtable`;
+ menu = addMenubarItem(menu, 'table', '| advtablerownumbering', 'advtablesort');
+ }
// Enhanced Image Editing.
- plugins += ` editimage`;
- toolbar = addToolbarButton(toolbar, 'content', 'editimage', 'tiny_media_image');
- // Remove the duplicate image button from the quickbar toolbar by redefining the values without 'imageoptions'.
- // eslint-disable-next-line camelcase
- instanceConfig.editimage_toolbar = 'rotateleft rotateright flipv fliph editimage';
-
+ if (enabledPremiumPlugins.indexOf('editimage') !== -1) {
+ plugins += ` editimage`;
+ toolbar = addToolbarButton(toolbar, 'content', 'editimage', 'tiny_media_image');
+ // Remove the duplicate image button from the quickbar toolbar by redefining the values without 'imageoptions'.
+ // eslint-disable-next-line camelcase
+ instanceConfig.editimage_toolbar = 'rotateleft rotateright flipv fliph editimage';
+ }
// Export.
- plugins += ` export`;
- menu = addMenubarItem(menu, 'tools', '| export');
-
+ if (enabledPremiumPlugins.indexOf('export') !== -1) {
+ plugins += ` export`;
+ menu = addMenubarItem(menu, 'tools', '| export');
+ }
// Page Embed.
- plugins += ` pageembed`;
- toolbar = addToolbarButton(toolbar, 'content', 'pageembed', 'tiny_media_video');
-
+ if (enabledPremiumPlugins.indexOf('pageembed') !== -1) {
+ plugins += ` pageembed`;
+ toolbar = addToolbarButton(toolbar, 'content', 'pageembed', 'tiny_media_video');
+ }
// Advanced Typography.
- plugins += ` typography`;
- toolbar = addToolbarButton(toolbar, 'premium_b', 'typography');
-
+ if (enabledPremiumPlugins.indexOf('typography') !== -1) {
+ plugins += ` typography`;
+ toolbar = addToolbarButton(toolbar, 'premium_b', 'typography');
+ }
// Case Change.
- plugins += ` casechange`;
- toolbar = addToolbarButton(toolbar, 'premium_a', 'casechange');
-
+ if (enabledPremiumPlugins.indexOf('casechange') !== -1) {
+ plugins += ` casechange`;
+ toolbar = addToolbarButton(toolbar, 'premium_a', 'casechange');
+ }
// Checklist.
- plugins += ` checklist`;
- toolbar = addToolbarButton(toolbar, 'lists', 'checklist');
-
+ if (enabledPremiumPlugins.indexOf('checklist') !== -1) {
+ plugins += ` checklist`;
+ toolbar = addToolbarButton(toolbar, 'lists', 'checklist');
+ }
// Spell Checker Pro.
- plugins += ` tinymcespellchecker`;
- menu = addMenubarItem(menu, 'tools', 'spellcheckdialog', 'spellcheckerlanguage');
- contextmenu = addContextmenuItem(contextmenu, 'spellchecker');
- toolbar = addToolbarButton(toolbar, 'premium_a', 'spellcheckdialog');
-
+ if (enabledPremiumPlugins.indexOf('tinymcespellchecker') !== -1) {
+ plugins += ` tinymcespellchecker`;
+ menu = addMenubarItem(menu, 'tools', 'spellcheckdialog', 'spellcheckerlanguage');
+ contextmenu = addContextmenuItem(contextmenu, 'spellchecker');
+ toolbar = addToolbarButton(toolbar, 'premium_a', 'spellcheckdialog');
+ }
// Spelling Autocorrect.
- plugins += ` autocorrect`;
- menu = addMenubarItem(menu, 'tools', '| autocorrect capitalization', 'spellcheckdialog');
-
+ if (enabledPremiumPlugins.indexOf('autocorrect') !== -1) {
+ plugins += ` autocorrect`;
+ menu = addMenubarItem(menu, 'tools', '| autocorrect capitalization', 'spellcheckdialog');
+ }
// Permanent Pen.
- plugins += ` permanentpen`;
- menu = addMenubarItem(menu, 'format', '| permanentpen configurepermanentpen');
- toolbar = addToolbarButton(toolbar, 'premium_a', 'permanentpen');
- contextmenu = addContextmenuItem(contextmenu, 'configurepermanentpen');
-
+ if (enabledPremiumPlugins.indexOf('permanentpen') !== -1) {
+ plugins += ` permanentpen`;
+ menu = addMenubarItem(menu, 'format', '| permanentpen configurepermanentpen');
+ toolbar = addToolbarButton(toolbar, 'premium_a', 'permanentpen');
+ contextmenu = addContextmenuItem(contextmenu, 'configurepermanentpen');
+ }
// Format Painter.
- plugins += ` formatpainter`;
- toolbar = addToolbarButton(toolbar, 'premium_a', 'formatpainter');
-
+ if (enabledPremiumPlugins.indexOf('formatpainter') !== -1) {
+ plugins += ` formatpainter`;
+ toolbar = addToolbarButton(toolbar, 'premium_a', 'formatpainter');
+ }
// Link Checker.
- plugins += ` linkchecker`;
- contextmenu = addContextmenuItem(contextmenu, 'linkchecker');
-
+ if (enabledPremiumPlugins.indexOf('linkchecker') !== -1) {
+ plugins += ` linkchecker`;
+ contextmenu = addContextmenuItem(contextmenu, 'linkchecker');
+ }
// Table of Contents.
- plugins += ` tableofcontents`;
- toolbar = addToolbarButton(toolbar, 'premium_a', 'tableofcontents');
-
+ if (enabledPremiumPlugins.indexOf('tableofcontents') !== -1) {
+ plugins += ` tableofcontents`;
+ toolbar = addToolbarButton(toolbar, 'premium_a', 'tableofcontents');
+ }
// Footnotes.
- plugins += ` footnotes`;
- toolbar = addToolbarButton(toolbar, 'premium_a', 'footnotes');
- menu = addMenubarItem(menu, 'insert', 'footnotes', 'tableofcontents');
-
+ if (enabledPremiumPlugins.indexOf('footnotes') !== -1) {
+ plugins += ` footnotes`;
+ toolbar = addToolbarButton(toolbar, 'premium_a', 'footnotes');
+ menu = addMenubarItem(menu, 'insert', 'footnotes', 'tableofcontents');
+ }
// Powerpaste.
- plugins += ` powerpaste`;
+ if (enabledPremiumPlugins.indexOf('powerpaste') !== -1) {
+ plugins += ` powerpaste`;
+ }
return {
plugins,
diff --git a/lib/editor/tiny/plugins/premium/classes/local/admin_setting_tiny_premium_plugins.php b/lib/editor/tiny/plugins/premium/classes/local/admin_setting_tiny_premium_plugins.php
new file mode 100644
index 0000000000000..0a1c78af70087
--- /dev/null
+++ b/lib/editor/tiny/plugins/premium/classes/local/admin_setting_tiny_premium_plugins.php
@@ -0,0 +1,151 @@
+.
+
+namespace tiny_premium\local;
+
+use tiny_premium\manager;
+
+/**
+ * Admin setting for managing Tiny Premium plugins.
+ *
+ * @package tiny_premium
+ * @copyright 2024 David Woloszyn
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class admin_setting_tiny_premium_plugins extends \admin_setting {
+
+ /**
+ * Calls parent::__construct with specific arguments.
+ */
+ public function __construct() {
+ $this->nosave = true;
+ parent::__construct(
+ name: 'tiny_premium/premiumplugins',
+ visiblename: new \lang_string('premiumplugins', 'tiny_premium'),
+ description: new \lang_string('premiumplugins_desc', 'tiny_premium'),
+ defaultsetting: '',
+ );
+ }
+
+ /**
+ * Always returns true.
+ *
+ * @return bool
+ */
+ public function get_setting(): bool {
+ return true;
+ }
+
+ /**
+ * Always returns '' and doesn't write anything.
+ *
+ * @param mixed $data
+ * @return string Always returns ''
+ */
+ public function write_setting($data): string {
+ return '';
+ }
+
+ /**
+ * Builds the HTML to display the Tiny Premium plugins table.
+ *
+ * @param mixed $data Unused
+ * @param string $query
+ * @return string highlight
+ */
+ public function output_html($data, $query=''): string {
+ global $OUTPUT;
+
+ $return = '';
+
+ // Warn users about an empty API key when displaying enabled plugins.
+ if (empty(get_config('tiny_premium', 'apikey')) && !empty(manager::get_enabled_plugins())) {
+ $return .= \core\notification::warning(get_string('emptyapikeywarning', 'tiny_premium'));
+ }
+
+ $return .= $OUTPUT->box_start('generalbox');
+ $return .= $OUTPUT->heading(get_string('premiumplugins', 'tiny_premium'), 3);
+ $return .= \html_writer::tag('p', get_string('premiumplugins_desc', 'tiny_premium'));
+ $return .= $this->define_manage_tiny_premium_plugins_table();
+ $return .= $OUTPUT->box_end();
+
+ return highlight($query, $return);
+ }
+
+ /**
+ * Defines table for managing Tiny Premium plugins.
+ *
+ * @return string HTML for table
+ */
+ public function define_manage_tiny_premium_plugins_table(): string {
+ global $OUTPUT;
+ $sesskey = sesskey();
+
+ // Set up table.
+ $table = new \html_table();
+ $table->id = 'managetinypremiumpluginstable';
+ $table->attributes['class'] = 'admintable generaltable';
+ $table->head = [
+ get_string('name'),
+ get_string('enable'),
+ ];
+ $table->colclasses = [
+ 'leftalign',
+ 'centeralign',
+ ];
+ $table->data = [];
+
+ // Keep enabled plugins on top.
+ $plugins = manager::get_plugins();
+ $enabledplugins = manager::get_enabled_plugins();
+ $disabledplugins = array_diff($plugins, $enabledplugins);
+ $plugins = array_merge($enabledplugins, $disabledplugins);
+
+ foreach ($plugins as $plugin) {
+
+ $pluginname = get_string('premiumplugin:' . $plugin, 'tiny_premium');
+
+ // Determine plugin actions.
+ if (manager::is_plugin_enabled($plugin)) {
+ $action = 'disable';
+ $icon = $OUTPUT->pix_icon('t/hide', get_string('disableplugin', 'core_admin', $pluginname));
+ $class = '';
+ } else {
+ $action = 'enable';
+ $icon = $OUTPUT->pix_icon('t/show', get_string('enableplugin', 'core_admin', $pluginname));
+ $class = 'dimmed_text';
+ }
+
+ // Prepare a link to perform the action.
+ $hideshowurl = new \moodle_url('/lib/editor/tiny/plugins/premium/pluginsettings.php', [
+ 'action' => $action,
+ 'plugin' => $plugin,
+ 'sesskey' => $sesskey,
+ ]);
+ $hideshowlink = \html_writer::link($hideshowurl, $icon);
+
+ // Populate table row.
+ $row = new \html_table_row([
+ $pluginname,
+ $hideshowlink,
+ ]);
+ $row->attributes['class'] = $class;
+ $table->data[] = $row;
+ }
+
+ return \html_writer::table($table);
+ }
+}
diff --git a/lib/editor/tiny/plugins/premium/classes/manager.php b/lib/editor/tiny/plugins/premium/classes/manager.php
new file mode 100644
index 0000000000000..03f8b0664f187
--- /dev/null
+++ b/lib/editor/tiny/plugins/premium/classes/manager.php
@@ -0,0 +1,109 @@
+.
+
+namespace tiny_premium;
+
+/**
+ * Tiny Premium manager.
+ *
+ * @package tiny_premium
+ * @copyright 2024 David Woloszyn
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class manager {
+
+ /**
+ * Get all Tiny Premium plugins currently supported.
+ *
+ * The plugin identifiers are taken from Tiny Cloud (https://www.tiny.cloud/docs/tinymce/6/plugins/#premium-plugins).
+ *
+ * @return array The array of plugins.
+ */
+ public static function get_plugins(): array {
+ return [
+ 'advtable',
+ 'typography',
+ 'casechange',
+ 'checklist',
+ 'editimage',
+ 'export',
+ 'footnotes',
+ 'formatpainter',
+ 'linkchecker',
+ 'pageembed',
+ 'permanentpen',
+ 'powerpaste',
+ 'tinymcespellchecker',
+ 'autocorrect',
+ 'tableofcontents',
+ ];
+ }
+
+ /**
+ * Get enabled Tiny Premium plugins.
+ *
+ * @return array The array of enabled plugins.
+ */
+ public static function get_enabled_plugins(): array {
+ $plugins = self::get_plugins();
+ $enabledplugins = [];
+ foreach ($plugins as $plugin) {
+ if (self::is_plugin_enabled($plugin)) {
+ $enabledplugins[] = $plugin;
+ }
+ }
+ return $enabledplugins;
+ }
+
+ /**
+ * Check if a Tiny Premium plugin is enabled in config.
+ *
+ * @param string $plugin The plugin to check.
+ * @return bool Return true if enabled.
+ */
+ public static function is_plugin_enabled(string $plugin): bool {
+ $config = get_config('tiny_premium_' . $plugin, 'enabled');
+ return ($config == 1);
+ }
+
+ /**
+ * Set a new value for a Tiny Premium plugin config.
+ *
+ * @param array $data The data to set.
+ * @param string $plugin The plugin to use.
+ */
+ public static function set_plugin_config(array $data, string $plugin): void {
+ // Check this is a valid premium plugin.
+ if (!in_array($plugin, self::get_plugins())) {
+ return;
+ }
+
+ $plugin = 'tiny_premium_' . $plugin;
+
+ foreach ($data as $key => $newvalue) {
+ // Get the old value for the log.
+ $oldvalue = get_config($plugin, $key) ?? null;
+ add_to_config_log($key, $oldvalue, $newvalue, $plugin);
+
+ // If we are disabling the plugin, remove it, otherwise, set the new value.
+ if ($key === 'enabled' && $newvalue == 0) {
+ unset_config($key, $plugin);
+ } else {
+ set_config($key, $newvalue, $plugin);
+ }
+ }
+ }
+}
diff --git a/lib/editor/tiny/plugins/premium/classes/plugininfo.php b/lib/editor/tiny/plugins/premium/classes/plugininfo.php
index 9c7ccdea441cd..17634a81df82c 100644
--- a/lib/editor/tiny/plugins/premium/classes/plugininfo.php
+++ b/lib/editor/tiny/plugins/premium/classes/plugininfo.php
@@ -19,6 +19,8 @@
use context;
use editor_tiny\editor;
use editor_tiny\plugin;
+use editor_tiny\plugin_with_configuration;
+use tiny_premium\manager;
/**
* Tiny Premium plugin.
@@ -27,7 +29,7 @@
* @copyright 2023 David Woloszyn
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class plugininfo extends plugin {
+class plugininfo extends plugin implements plugin_with_configuration {
/**
* Determine if the plugin should be enabled by checking the capability and if the Tiny Premium API key is set.
@@ -46,4 +48,24 @@ public static function is_enabled(
): bool {
return has_capability('tiny/premium:accesspremium', $context) && (get_config('tiny_premium', 'apikey') != false);
}
+
+ /**
+ * Get a list of enabled Tiny Premium plugins set by the admin.
+ *
+ * @param context $context The context that the editor is used within
+ * @param array $options The options passed in when requesting the editor
+ * @param array $fpoptions The filepicker options passed in when requesting the editor
+ * @param editor|null $editor The editor instance in which the plugin is initialised
+ * @return array
+ */
+ public static function get_plugin_configuration_for_context(
+ context $context,
+ array $options,
+ array $fpoptions,
+ ?editor $editor = null
+ ): array {
+ return [
+ 'premiumplugins' => implode(',', manager::get_enabled_plugins()),
+ ];
+ }
}
diff --git a/lib/editor/tiny/plugins/premium/db/upgrade.php b/lib/editor/tiny/plugins/premium/db/upgrade.php
new file mode 100644
index 0000000000000..486d5ee661bf1
--- /dev/null
+++ b/lib/editor/tiny/plugins/premium/db/upgrade.php
@@ -0,0 +1,55 @@
+.
+//
+
+/**
+ * Upgrade code for tiny_premium.
+ *
+ * @package tiny_premium
+ * @copyright 2024 David Woloszyn
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Function to upgrade tiny_premium.
+ *
+ * @param int $oldversion the version we are upgrading from
+ * @return bool result
+ */
+function xmldb_tiny_premium_upgrade($oldversion) {
+ // Automatically generated Moodle v4.1.0 release upgrade line.
+ // Put any upgrade step following this.
+
+ // Automatically generated Moodle v4.2.0 release upgrade line.
+ // Put any upgrade step following this.
+
+ // Automatically generated Moodle v4.3.0 release upgrade line.
+ // Put any upgrade step following this.
+ if ($oldversion < 2024042201) {
+
+ // Only enable the premium plugins if we have an API key.
+ if (!empty(get_config('tiny_premium', 'apikey'))) {
+ $premiumplugins = \tiny_premium\manager::get_plugins();
+ foreach ($premiumplugins as $plugin) {
+ \tiny_premium\manager::set_plugin_config(['enabled' => 1], $plugin);
+ };
+ }
+
+ upgrade_plugin_savepoint(true, 2024042201, 'tiny', 'premium');
+ }
+
+ return true;
+}
diff --git a/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php b/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php
index 917aeda955ef0..bddbda2c960f2 100644
--- a/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php
+++ b/lib/editor/tiny/plugins/premium/lang/en/tiny_premium.php
@@ -25,8 +25,28 @@
defined('MOODLE_INTERNAL') || die();
-$string['pluginname'] = 'TinyMCE Premium';
$string['apikey'] = 'API key';
-$string['apikey_desc'] = 'Your API key is available on your Tiny Cloud account page if you have purchased a subscription, or if you are on a free trial.
See the list of available TinyMCE Premium features for Moodle in the documentation TinyMCE editor.
';
+$string['apikey_desc'] = 'Your API key is available on your Tiny Cloud account page if you have purchased a subscription, or if you are on a free trial.';
+$string['emptyapikeywarning'] = 'Enabled TinyMCE Premium plugins will not be available until an API key is added.';
+$string['helplinktext'] = 'Premium plugins';
+$string['pluginname'] = 'TinyMCE Premium';
+$string['pluginnotfound'] = 'Tiny Premium plugin {$a} not found.';
$string['premium:accesspremium'] = 'Access TinyMCE Premium features';
-$string['privacy:metadata'] = 'The TinyMCE Premium plugin does not store any personal data.';
+$string['premiumplugin:advtable'] = 'Advanced Table';
+$string['premiumplugin:autocorrect'] = 'Spelling Autocorrect';
+$string['premiumplugin:casechange'] = 'Case Change';
+$string['premiumplugin:checklist'] = 'Checklist';
+$string['premiumplugin:editimage'] = 'Enhanced Image Editing';
+$string['premiumplugin:export'] = 'Export';
+$string['premiumplugin:footnotes'] = 'Footnotes';
+$string['premiumplugin:formatpainter'] = 'Format Painter';
+$string['premiumplugin:linkchecker'] = 'Link Checker';
+$string['premiumplugin:pageembed'] = 'Page Embed';
+$string['premiumplugin:permanentpen'] = 'Permanent Pen';
+$string['premiumplugin:powerpaste'] = 'Powerpaste';
+$string['premiumplugin:tableofcontents'] = 'Table of Contents';
+$string['premiumplugin:tinymcespellchecker'] = 'Spell Checker Pro';
+$string['premiumplugin:typography'] = 'Advanced Typography';
+$string['premiumplugins'] = 'Premium plugins';
+$string['premiumplugins_desc'] = 'Access to TinyMCE Premium plugins requires an API key. Not all listed plugins may be available with your TinyMCE Premium subscription. You can check available plugins on your Tiny Cloud account page.';
+$string['privacy:metadata'] = 'The Tiny premium plugin for TinyMCE does not store any personal data.';
diff --git a/lib/editor/tiny/plugins/premium/pluginsettings.php b/lib/editor/tiny/plugins/premium/pluginsettings.php
new file mode 100644
index 0000000000000..4111246556f45
--- /dev/null
+++ b/lib/editor/tiny/plugins/premium/pluginsettings.php
@@ -0,0 +1,74 @@
+.
+
+/**
+ * TinyMCE Premium plugins configuration page.
+ *
+ * @package tiny_premium
+ * @copyright 2024 David Woloszyn
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../../config.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$action = required_param('action', PARAM_ALPHANUMEXT);
+$plugin = required_param('plugin', PARAM_ALPHANUMEXT);
+
+$syscontext = context_system::instance();
+$PAGE->set_url('/lib/editor/tiny/plugins/premium/pluginsettings.php');
+$PAGE->set_context($syscontext);
+
+require_admin();
+require_sesskey();
+
+$return = new moodle_url('/admin/settings.php', ['section' => 'tiny_premium_settings']);
+
+// Get all Tiny Premium plugins.
+$premiumplugins = \tiny_premium\manager::get_plugins();
+if (!in_array($plugin, $premiumplugins)) {
+ throw new moodle_exception('pluginnotfound', 'tiny_premium', $return, $plugin);
+}
+
+// Get enabled Tiny Premium plugins.
+$enabledplugins = \tiny_premium\manager::get_enabled_plugins();
+$pluginname = get_string('premiumplugin:' . $plugin, 'tiny_premium');
+
+switch ($action) {
+ case 'disable':
+ if (in_array($plugin, $enabledplugins)) {
+ \tiny_premium\manager::set_plugin_config(['enabled' => 0], $plugin);
+
+ \core\notification::add(
+ get_string('plugin_disabled', 'core_admin', $pluginname),
+ \core\notification::SUCCESS
+ );
+ }
+ break;
+
+ case 'enable':
+ if (!in_array($plugin, $enabledplugins)) {
+ \tiny_premium\manager::set_plugin_config(['enabled' => 1], $plugin);
+
+ \core\notification::add(
+ get_string('plugin_enabled', 'core_admin', $pluginname),
+ \core\notification::SUCCESS
+ );
+ }
+ break;
+}
+
+redirect($return);
diff --git a/lib/editor/tiny/plugins/premium/readme_moodle.txt b/lib/editor/tiny/plugins/premium/readme_moodle.txt
index d0afd906ebb8a..f175412625ebf 100644
--- a/lib/editor/tiny/plugins/premium/readme_moodle.txt
+++ b/lib/editor/tiny/plugins/premium/readme_moodle.txt
@@ -4,9 +4,12 @@ A request to Tiny Cloud is made in the plugin.js file of this plugin.
This request passes the Tiny Premium API key as part of a URL.
The URL also contains the major version of Tiny and may need to be updated.
-The URL looks like this: https://cdn.tiny.cloud/1/YOUR_API_KEY/tinymce/6/plugins.min.js
-
-Notice that the version (6) is baked into the URL and may need revision.
+The URL looks like this: https://cdn.tiny.cloud/1/YOUR_API_KEY/tinymce/VERSION/plugins.min.js
When upgrading, check Tiny Cloud's documentation regarding the correct API URL
to use. Go to https://www.tiny.cloud/docs/tinymce
+
+TinyMCE Premium plugins can be individually enabled/disabled by admins.
+Each release of TinyMCE may have a different selection of plugins available.
+When upgrading, please check the list of available TinyMCE Premium plugins and update the list
+with the revisions (lib/editor/tiny/plugins/premium/classes/manager.php).
diff --git a/lib/editor/tiny/plugins/premium/settings.php b/lib/editor/tiny/plugins/premium/settings.php
index 737ed7ceeafb1..128b3debb998f 100644
--- a/lib/editor/tiny/plugins/premium/settings.php
+++ b/lib/editor/tiny/plugins/premium/settings.php
@@ -28,6 +28,7 @@
if ($hassiteconfig) {
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
if ($ADMIN->fulltree) {
+ // Set API key.
$setting = new admin_setting_configpasswordunmask(
'tiny_premium/apikey',
get_string('apikey', 'tiny_premium'),
@@ -35,5 +36,8 @@
'',
);
$settings->add($setting);
+
+ // Set individual Tiny Premium plugins.
+ $settings->add(new \tiny_premium\local\admin_setting_tiny_premium_plugins());
}
}
diff --git a/lib/editor/tiny/plugins/premium/tests/behat/tiny_premium_settings.feature b/lib/editor/tiny/plugins/premium/tests/behat/tiny_premium_settings.feature
new file mode 100644
index 0000000000000..e60ce3aee15f7
--- /dev/null
+++ b/lib/editor/tiny/plugins/premium/tests/behat/tiny_premium_settings.feature
@@ -0,0 +1,23 @@
+@editor @editor_tiny
+Feature: Check the features of the TinyMCE Premium settings
+ In order to use TinyMCE Premium features
+ As an admin
+ I need TinyMCE Premium settings to be configured correctly
+
+ Background:
+ Given I am logged in as "admin"
+ And I navigate to "Plugins > Text editors > TinyMCE editor > TinyMCE Premium" in site administration
+
+ @javascript
+ Scenario: I can see a warning banner when I enable a TinyMCE premium plugin without an API key set
+ When I click on "Enable Advanced Table" "link"
+ Then I should see "Advanced Table enabled."
+ And I should see "Enabled TinyMCE Premium plugins will not be available until an API key is added."
+
+ @javascript
+ Scenario: I cannot see a warning banner when I enable a TinyMCE premium plugin with an API key set
+ Given the following config values are set as admin:
+ | apikey | "123456" | tiny_premium |
+ When I click on "Enable Advanced Table" "link"
+ Then I should see "Advanced Table enabled."
+ And I should not see "Enabled TinyMCE Premium plugins will not be available until an API key is added."
diff --git a/lib/editor/tiny/plugins/premium/tests/manager_test.php b/lib/editor/tiny/plugins/premium/tests/manager_test.php
new file mode 100644
index 0000000000000..bf438e4a1b605
--- /dev/null
+++ b/lib/editor/tiny/plugins/premium/tests/manager_test.php
@@ -0,0 +1,73 @@
+.
+
+namespace tiny_premium;
+
+/**
+ * Manager tests class for tiny_premium.
+ *
+ * @package tiny_premium
+ * @category test
+ * @copyright 2024 David Woloszyn
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+final class manager_test extends \advanced_testcase {
+
+ /**
+ * Test the getting of all available Tiny Premium plugins.
+ *
+ * @covers \tiny_premium\manager
+ */
+ public function test_get_plugins(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Check all Tiny Premium plugins are returned.
+ $premiumplugins = manager::get_plugins();
+ $this->assertCount(15, $premiumplugins);
+ }
+
+ /**
+ * Test the getting and setting of enabled Tiny Premium plugins.
+ *
+ * @covers \tiny_premium\manager
+ */
+ public function test_get_and_set_enabled_plugins(): void {
+ $this->resetAfterTest();
+ $this->setAdminUser();
+
+ // Check all enabled Tiny Premium plugins are returned (all disabled by default).
+ $enabledpremiumplugins = manager::get_enabled_plugins();
+ $this->assertCount(0, $enabledpremiumplugins);
+
+ // Enable a couple premium plugins.
+ manager::set_plugin_config(['enabled' => 1], 'advtable');
+ manager::set_plugin_config(['enabled' => 1], 'formatpainter');
+ // Check the premium plugins are enabled.
+ $enabledpremiumplugins = manager::get_enabled_plugins();
+ $this->assertCount(2, $enabledpremiumplugins);
+ $this->assertTrue(manager::is_plugin_enabled('advtable'));
+ $this->assertTrue(manager::is_plugin_enabled('formatpainter'));
+
+ // Disable a premium plugin.
+ manager::set_plugin_config(['enabled' => 0], 'advtable');
+ // Check the correct premium plugins are enabled.
+ $enabledpremiumplugins = manager::get_enabled_plugins();
+ $this->assertCount(1, $enabledpremiumplugins);
+ $this->assertFalse(manager::is_plugin_enabled('advtable'));
+ $this->assertTrue(manager::is_plugin_enabled('formatpainter'));
+ }
+}
diff --git a/lib/editor/tiny/plugins/premium/version.php b/lib/editor/tiny/plugins/premium/version.php
index e6caa3110eb24..bd70eb2f3fe7d 100644
--- a/lib/editor/tiny/plugins/premium/version.php
+++ b/lib/editor/tiny/plugins/premium/version.php
@@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2024042200;
+$plugin->version = 2024042201;
$plugin->requires = 2024041600;
$plugin->component = 'tiny_premium';
diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php
index eadf44544d369..d4edea5323141 100644
--- a/lib/outputrenderers.php
+++ b/lib/outputrenderers.php
@@ -421,25 +421,12 @@ public function should_display_navbar_logo() {
}
/**
- * Whether we should display the main logo.
* @deprecated since Moodle 4.0
- * @todo final deprecation. To be removed in Moodle 4.4 MDL-73165.
- * @param int $headinglevel The heading level we want to check against.
- * @return bool
*/
- public function should_display_main_logo($headinglevel = 1) {
- debugging('should_display_main_logo() is deprecated and will be removed in Moodle 4.4.', DEBUG_DEVELOPER);
- // Only render the logo if we're on the front page or login page and the we have a logo.
- $logo = $this->get_logo_url();
- if ($headinglevel == 1 && !empty($logo)) {
- if ($this->page->pagelayout == 'frontpage' || $this->page->pagelayout == 'login') {
- return true;
- }
- }
-
- return false;
+ #[\core\attribute\deprecated(null, reason: 'It is no longer used', since: '4.0', final: true)]
+ public function should_display_main_logo() {
+ \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
}
-
}
diff --git a/lib/phpunit/classes/util.php b/lib/phpunit/classes/util.php
index 22454da39f52e..0e0bb2d1cb678 100644
--- a/lib/phpunit/classes/util.php
+++ b/lib/phpunit/classes/util.php
@@ -990,6 +990,16 @@ protected static function pad(string $string, int $level): string {
return str_repeat(" ", $level * 2) . "{$string}\n";
}
+ /**
+ * Normalise any text to always use unix line endings (line-feeds).
+ *
+ * @param string $text The text to normalize
+ * @return string
+ */
+ public static function normalise_line_endings(string $text): string {
+ return str_replace(["\r\n", "\r"], "\n", $text);
+ }
+
/**
* Get the coverage config for the supplied includelist and excludelist configuration.
*
diff --git a/lib/searchlib.php b/lib/searchlib.php
index c158c78ecf537..c2caf7ae79fa6 100644
--- a/lib/searchlib.php
+++ b/lib/searchlib.php
@@ -381,23 +381,6 @@ function plainstring($content){
}
}
-/**
- * Primitive function to generate a SQL string from a parse tree
- * using TEXT indexes. If searches aren't suitable to use TEXT
- * this function calls the default search_generate_SQL() one.
- *
- * @deprecated since Moodle 2.9 MDL-48939
- * @todo MDL-48940 This will be deleted in Moodle 3.2
- * @see search_generate_SQL()
- */
-function search_generate_text_SQL($parsetree, $datafield, $metafield, $mainidfield, $useridfield,
- $userfirstnamefield, $userlastnamefield, $timefield, $instancefield) {
- debugging('search_generate_text_SQL() is deprecated, please use search_generate_SQL() instead.', DEBUG_DEVELOPER);
-
- return search_generate_SQL($parsetree, $datafield, $metafield, $mainidfield, $useridfield,
- $userfirstnamefield, $userlastnamefield, $timefield, $instancefield);
-}
-
/**
* Primitive function to generate a SQL string from a parse tree.
* Parameters:
diff --git a/lib/tests/component_test.php b/lib/tests/component_test.php
index 20768882138eb..97b6a31fdc1be 100644
--- a/lib/tests/component_test.php
+++ b/lib/tests/component_test.php
@@ -26,7 +26,7 @@
*
* @covers \core_component
*/
-class component_test extends advanced_testcase {
+final class component_test extends advanced_testcase {
/**
* To be changed if number of subsystems increases/decreases,
* this is defined here to annoy devs that try to add more without any thinking,
@@ -575,8 +575,10 @@ public function test_get_plugin_list_with_file(): void {
$this->assertEquals([], array_keys($list));
}
+ /**
+ * Tests for get_component_classes_in_namespace.
+ */
public function test_get_component_classes_in_namespace(): void {
-
// Unexisting.
$this->assertCount(0, core_component::get_component_classes_in_namespace('core_unexistingcomponent', 'something'));
$this->assertCount(0, core_component::get_component_classes_in_namespace('auth_cas', 'something'));
@@ -586,36 +588,6 @@ public function test_get_component_classes_in_namespace(): void {
$this->assertCount(0, core_component::get_component_classes_in_namespace('core_user', 'course'));
$this->assertCount(0, core_component::get_component_classes_in_namespace('mod_forum', 'output\\emaildigest'));
$this->assertCount(0, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\emaildigest'));
- $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', 'output\\email'));
- $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\email'));
- $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', 'output\\email\\'));
- $this->assertCount(2, core_component::get_component_classes_in_namespace('mod_forum', '\\output\\email\\'));
-
- // Prefix with backslash if it doesn\'t come prefixed.
- $this->assertCount(1, core_component::get_component_classes_in_namespace('auth_cas', 'task'));
- $this->assertCount(1, core_component::get_component_classes_in_namespace('auth_cas', '\\task'));
-
- // Core as a component works, the function can normalise the component name.
- $this->assertCount(7, core_component::get_component_classes_in_namespace('core', 'update'));
- $this->assertCount(7, core_component::get_component_classes_in_namespace('', 'update'));
- $this->assertCount(7, core_component::get_component_classes_in_namespace('moodle', 'update'));
-
- // Multiple levels.
- $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile\\'));
- $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', 'output\\myprofile\\'));
- $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', '\\output\\myprofile'));
- $this->assertCount(5, core_component::get_component_classes_in_namespace('core_user', 'output\\myprofile'));
-
- // Without namespace it returns classes/ classes.
- $this->assertCount(8, core_component::get_component_classes_in_namespace('tool_mobile', ''));
- $this->assertCount(2, core_component::get_component_classes_in_namespace('tool_filetypes'));
-
- // When no component is specified, classes are returned for the namespace in all components.
- // (We don't assert exact amounts here as the count of `output` classes will change depending on plugins installed).
- $this->assertGreaterThan(
- count(\core_component::get_component_classes_in_namespace('core', 'output')),
- count(\core_component::get_component_classes_in_namespace(null, 'output'))
- );
// Without either a component or namespace it returns an empty array.
$this->assertEmpty(\core_component::get_component_classes_in_namespace());
@@ -623,6 +595,120 @@ public function test_get_component_classes_in_namespace(): void {
$this->assertEmpty(\core_component::get_component_classes_in_namespace(null, ''));
}
+ /**
+ * Test that the get_component_classes_in_namespace() function returns classes in the correct namespace.
+ *
+ * @dataProvider get_component_classes_in_namespace_provider
+ * @param array $methodargs
+ * @param string $expectedclassnameformat
+ */
+ public function test_get_component_classes_in_namespace_provider(
+ array $methodargs,
+ string $expectedclassnameformat,
+ ): void {
+ $classlist = core_component::get_component_classes_in_namespace(...$methodargs);
+ $this->assertGreaterThan(0, count($classlist));
+
+ foreach (array_keys($classlist) as $classname) {
+ $this->assertStringMatchesFormat($expectedclassnameformat, $classname);
+ }
+ }
+
+ /**
+ * Data provider for get_component_classes_in_namespace tests.
+ *
+ * @return array
+ */
+ public static function get_component_classes_in_namespace_provider(): array {
+ return [
+ // Matches the last namespace level name not partials.
+ [
+ ['mod_forum', 'output\\email'],
+ 'mod_forum\output\email\%s',
+ ],
+ [
+ ['mod_forum', '\\output\\email'],
+ 'mod_forum\output\email\%s',
+ ],
+ [
+ ['mod_forum', 'output\\email\\'],
+ 'mod_forum\output\email\%s',
+ ],
+ [
+ ['mod_forum', '\\output\\email\\'],
+ 'mod_forum\output\email\%s',
+ ],
+ // Prefix with backslash if it doesn\'t come prefixed.
+ [
+ ['auth_cas', 'task'],
+ 'auth_cas\task\%s',
+ ],
+ [
+ ['auth_cas', '\\task'],
+ 'auth_cas\task\%s',
+ ],
+
+ // Core as a component works, the function can normalise the component name.
+ [
+ ['core', 'update'],
+ 'core\update\%s',
+ ],
+ [
+ ['', 'update'],
+ 'core\update\%s',
+ ],
+ [
+ ['moodle', 'update'],
+ 'core\update\%s',
+ ],
+
+ // Multiple levels.
+ [
+ ['core_user', '\\output\\myprofile\\'],
+ 'core_user\output\myprofile\%s',
+ ],
+ [
+ ['core_user', 'output\\myprofile\\'],
+ 'core_user\output\myprofile\%s',
+ ],
+ [
+ ['core_user', '\\output\\myprofile'],
+ 'core_user\output\myprofile\%s',
+ ],
+ [
+ ['core_user', 'output\\myprofile'],
+ 'core_user\output\myprofile\%s',
+ ],
+
+ // Without namespace it returns classes/ classes.
+ [
+ ['tool_mobile', ''],
+ 'tool_mobile\%s',
+ ],
+ [
+ ['tool_filetypes'],
+ 'tool_filetypes\%s',
+ ],
+
+ // Multiple levels.
+ [
+ ['core_user', '\\output\\myprofile\\'],
+ 'core_user\output\myprofile\%s',
+ ],
+
+ // When no component is specified, classes are returned for the namespace in all components.
+ // (We don't assert exact amounts here as the count of `output` classes will change depending on plugins installed).
+ [
+ ['core', 'output'],
+ 'core\%s',
+ ],
+ [
+ [null, 'output'],
+ '%s',
+ ],
+ ];
+ }
+
/**
* Data provider for classloader test
*/
diff --git a/lib/tests/report_helper_test.php b/lib/tests/report_helper_test.php
deleted file mode 100644
index f6cab8c765506..0000000000000
--- a/lib/tests/report_helper_test.php
+++ /dev/null
@@ -1,78 +0,0 @@
-.
-
-/**
- * Tests for report_helper.
- *
- * @package core
- * @category test
- * @copyright 2021 Sujith Haridasan
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace core;
-
-use moodle_url;
-use core\report_helper;
-
-/**
- * Tests the functions for report_helper class.
- */
-class report_helper_test extends \advanced_testcase {
- /**
- * Data provider for testing selected report for same and different courses
- *
- * @return array
- */
- public function data_selected_report(): array {
- return [
- ['course_url_id' => [
- ['url' => '/test', 'id' => 1],
- ['url' => '/foo', 'id' => 1]]
- ],
- ['course_url_id' => [
- ['url' => '/test', 'id' => 1],
- ['url' => '/foo/bar', 'id' => 2]]
- ]
- ];
- }
-
- /**
- * Testing selected report saved in $USER session.
- *
- * @dataProvider data_selected_report
- * @param array $courseurlid The array has both course url and course id
- */
- public function test_save_selected_report(array $courseurlid): void {
- global $USER;
-
- $url1 = new moodle_url($courseurlid[0]['url']);
- $courseid1 = $courseurlid[0]['id'];
- report_helper::save_selected_report($courseid1, $url1);
- $this->assertDebuggingCalled('save_selected_report() has been deprecated because it is no ' .
- 'longer used and will be removed in future versions of Moodle');
-
- $this->assertEquals($USER->course_last_report[$courseid1], $url1);
-
- $url2 = new moodle_url($courseurlid[1]['url']);
- $courseid2 = $courseurlid[1]['id'];
- report_helper::save_selected_report($courseid2, $url2);
- $this->assertDebuggingCalled('save_selected_report() has been deprecated because it is no ' .
- 'longer used and will be removed in future versions of Moodle');
-
- $this->assertEquals($USER->course_last_report[$courseid2], $url2);
- }
-}
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 49ec5e12f8e23..ec9a1ea4af149 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -1,6 +1,13 @@
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
+=== 4.5 ===
+
+* Final deprecation and removal of the function core_text::reset_caches().
+* The previously deprecated function `search_generate_text_SQL` has been removed and can no longer be used.
+* The following previously deprecated methods have been removed and can no longer be used:
+ - `renderer_base::should_display_main_logo`
+
=== 4.4 ===
* New modinfo methods related to delegated sections (sections controlled by a component):
diff --git a/lib/womenslib.php b/lib/womenslib.php
deleted file mode 100644
index e577d94c0bb02..0000000000000
--- a/lib/womenslib.php
+++ /dev/null
@@ -1,29 +0,0 @@
-.
-
-/**
- * @package moodlecore
- * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/** Include config.pgp */
-include('../config.php');
-
-$PAGE->set_url('/lib/womenslib.php');
-
-redirect('http://en.wikipedia.org/wiki/Women%27s_liberation');
\ No newline at end of file
diff --git a/media/player/videojs/db/upgrade.php b/media/player/videojs/db/upgrade.php
index 4964964f9ebd4..b5d8ae3fef52c 100644
--- a/media/player/videojs/db/upgrade.php
+++ b/media/player/videojs/db/upgrade.php
@@ -58,5 +58,8 @@ function xmldb_media_videojs_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/message/output/email/db/upgrade.php b/message/output/email/db/upgrade.php
index 96a2bd3f9fd01..1a080fcd6c63a 100644
--- a/message/output/email/db/upgrade.php
+++ b/message/output/email/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_message_email_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/message/output/popup/db/upgrade.php b/message/output/popup/db/upgrade.php
index e41d3f76bc28a..dac0a0909d858 100644
--- a/message/output/popup/db/upgrade.php
+++ b/message/output/popup/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_message_popup_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/assign/db/upgrade.php b/mod/assign/db/upgrade.php
index e35248ff86417..1d99bf7f492df 100644
--- a/mod/assign/db/upgrade.php
+++ b/mod/assign/db/upgrade.php
@@ -63,5 +63,8 @@ function xmldb_assign_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2023103000, 'assign');
}
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/assign/feedback/comments/db/upgrade.php b/mod/assign/feedback/comments/db/upgrade.php
index b77eccb3de4cb..db24c1ec11991 100644
--- a/mod/assign/feedback/comments/db/upgrade.php
+++ b/mod/assign/feedback/comments/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_assignfeedback_comments_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/assign/feedback/editpdf/db/upgrade.php b/mod/assign/feedback/editpdf/db/upgrade.php
index 4ab535ffb9c03..c8a8ee4b60ddc 100644
--- a/mod/assign/feedback/editpdf/db/upgrade.php
+++ b/mod/assign/feedback/editpdf/db/upgrade.php
@@ -48,5 +48,8 @@ function xmldb_assignfeedback_editpdf_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/assign/feedback/file/db/upgrade.php b/mod/assign/feedback/file/db/upgrade.php
index f9a4e5f2a84c2..64272266c0e81 100644
--- a/mod/assign/feedback/file/db/upgrade.php
+++ b/mod/assign/feedback/file/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_assignfeedback_file_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/assign/lang/en/assign.php b/mod/assign/lang/en/assign.php
index 327d31a6197d5..8a384234f3f9a 100644
--- a/mod/assign/lang/en/assign.php
+++ b/mod/assign/lang/en/assign.php
@@ -574,9 +574,9 @@
$string['submissionsclosed'] = 'Submissions closed';
$string['submissionsettings'] = 'Submission settings';
$string['submissionstatement'] = 'Submission statement';
-$string['submissionstatement_help'] = 'Assignment submission confirmation statement';
$string['submissionstatementdefault'] = 'This submission is my own work, except where I have acknowledged the use of the works of other people.';
$string['submissionstatement_help'] = 'Statement that each student must accept in order to submit their work.';
+$string['submissionstatementrequired'] = 'You are required to agree to this statement before you can submit.';
$string['submissionstatementteamsubmission'] = 'Group submission statement';
$string['submissionstatementteamsubmissiondefault'] = 'This submission is the work of my group, except where we have acknowledged the use of the works of other people.';
$string['submissionstatementteamsubmission_help'] = 'Statement that each student must accept in order to submit the work of their group.';
diff --git a/mod/assign/lib.php b/mod/assign/lib.php
index 7049d8845901c..c0cb2a6e3ef63 100644
--- a/mod/assign/lib.php
+++ b/mod/assign/lib.php
@@ -1400,6 +1400,8 @@ function assign_pluginfile($course,
function mod_assign_output_fragment_gradingpanel($args) {
global $CFG;
+ \core\session\manager::write_close(); // No changes to session in this function.
+
$context = $args['context'];
if ($context->contextlevel != CONTEXT_MODULE) {
diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php
index cf80496e413ed..684da36963754 100644
--- a/mod/assign/locallib.php
+++ b/mod/assign/locallib.php
@@ -8147,7 +8147,8 @@ public function add_submission_form_elements(MoodleQuickForm $mform, stdClass $d
// Only show submission statement if we are editing our own submission.
if ($requiresubmissionstatement && !$draftsenabled && $userid == $USER->id) {
$mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
- $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
+ $mform->addRule('submissionstatement', get_string('submissionstatementrequired', 'mod_assign'),
+ 'required', null, 'client');
}
$this->add_plugin_submission_elements($submission, $mform, $data, $userid);
diff --git a/mod/assign/submission/comments/db/upgrade.php b/mod/assign/submission/comments/db/upgrade.php
index 9fc6a20c7adc4..e25ebebc63748 100644
--- a/mod/assign/submission/comments/db/upgrade.php
+++ b/mod/assign/submission/comments/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_assignsubmission_comments_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/assign/submission/file/db/upgrade.php b/mod/assign/submission/file/db/upgrade.php
index fcc9be2af6895..610e084dfeccf 100644
--- a/mod/assign/submission/file/db/upgrade.php
+++ b/mod/assign/submission/file/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_assignsubmission_file_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/assign/submission/onlinetext/db/upgrade.php b/mod/assign/submission/onlinetext/db/upgrade.php
index ae0df7a7e2d2b..e83a5de6982f4 100644
--- a/mod/assign/submission/onlinetext/db/upgrade.php
+++ b/mod/assign/submission/onlinetext/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_assignsubmission_onlinetext_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/assign/submissionconfirmform.php b/mod/assign/submissionconfirmform.php
index 1508026a8506e..b8c440c1b3436 100644
--- a/mod/assign/submissionconfirmform.php
+++ b/mod/assign/submissionconfirmform.php
@@ -48,7 +48,8 @@ public function definition() {
if ($requiresubmissionstatement) {
$mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
- $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
+ $mform->addRule('submissionstatement', get_string('submissionstatementrequired', 'mod_assign'),
+ 'required', null, 'client');
}
$mform->addElement('static', 'confirmmessage', '', get_string('confirmsubmission', 'mod_assign'));
diff --git a/mod/assign/tests/behat/group_submission.feature b/mod/assign/tests/behat/group_submission.feature
index 92483c3570afa..2376fc0defcfa 100644
--- a/mod/assign/tests/behat/group_submission.feature
+++ b/mod/assign/tests/behat/group_submission.feature
@@ -58,7 +58,7 @@ Feature: Group assignment submissions
And I should see "This submission is the work of my group, except where we have acknowledged the use of the works of other people."
And I press "Continue"
And I should see "Confirm submission"
- And I should see "- Required"
+ And I should see "You are required to agree to this statement before you can submit."
And I set the field "submissionstatement" to "1"
And I press "Continue"
And I should see "Submitted for grading" in the "Submission status" "table_row"
diff --git a/mod/assign/tests/behat/submission_statement.feature b/mod/assign/tests/behat/submission_statement.feature
index ddce1a10c2f42..2f4e006951833 100644
--- a/mod/assign/tests/behat/submission_statement.feature
+++ b/mod/assign/tests/behat/submission_statement.feature
@@ -34,7 +34,7 @@ Feature: In an assignment, teacher can require submission statements
Then I should see "This submission is my own work, except where I have acknowledged the use of the works of other people."
And I press "Continue"
And I should see "Confirm submission"
- And I should see "- Required"
+ And I should see "You are required to agree to this statement before you can submit."
And I set the field "submissionstatement" to "1"
And I press "Continue"
And I should see "Submitted for grading" in the "Submission status" "table_row"
diff --git a/mod/bigbluebuttonbn/db/upgrade.php b/mod/bigbluebuttonbn/db/upgrade.php
index 0223fa905e82e..7d107c7502903 100644
--- a/mod/bigbluebuttonbn/db/upgrade.php
+++ b/mod/bigbluebuttonbn/db/upgrade.php
@@ -73,6 +73,9 @@ function xmldb_bigbluebuttonbn_upgrade($oldversion = 0) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/bigbluebuttonbn/tests/task/base_send_notification_test.php b/mod/bigbluebuttonbn/tests/task/base_send_notification_test.php
index 5ef3850081a34..c21082a383556 100644
--- a/mod/bigbluebuttonbn/tests/task/base_send_notification_test.php
+++ b/mod/bigbluebuttonbn/tests/task/base_send_notification_test.php
@@ -53,9 +53,7 @@ private function get_mock(): base_send_notification {
*/
private function get_instance_reflection(): \ReflectionMethod {
$rc = new \ReflectionClass(base_send_notification::class);
- $rcm = $rc->getMethod('get_instance');
- $rcm->setAccessible(true);
- return $rcm;
+ return $rc->getMethod('get_instance');
}
/**
diff --git a/mod/book/db/upgrade.php b/mod/book/db/upgrade.php
index c2b260eb1175f..8ba5706d7539d 100644
--- a/mod/book/db/upgrade.php
+++ b/mod/book/db/upgrade.php
@@ -37,5 +37,8 @@ function xmldb_book_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/chat/db/upgrade.php b/mod/chat/db/upgrade.php
index e8aaf6d6e8dd4..d9366519db6ab 100644
--- a/mod/chat/db/upgrade.php
+++ b/mod/chat/db/upgrade.php
@@ -32,5 +32,8 @@ function xmldb_chat_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/choice/db/upgrade.php b/mod/choice/db/upgrade.php
index 6de8108176967..249efbfc81688 100644
--- a/mod/choice/db/upgrade.php
+++ b/mod/choice/db/upgrade.php
@@ -49,5 +49,8 @@ function xmldb_choice_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/data/classes/local/importer/csv_entries_importer.php b/mod/data/classes/local/importer/csv_entries_importer.php
index a3b9064d1cad2..6fbc193839fe8 100644
--- a/mod/data/classes/local/importer/csv_entries_importer.php
+++ b/mod/data/classes/local/importer/csv_entries_importer.php
@@ -133,7 +133,15 @@ public function import_csv(stdClass $cm, stdClass $data, string $encoding, strin
$authorid = $author->id;
}
}
- if ($recordid = data_add_record($data, 0, $authorid)) { // Add instance to data_record.
+
+ // Determine presence of "approved" field within the record to import.
+ $approved = true;
+ if (array_key_exists(get_string('approved', 'data'), $fieldnames)) {
+ $approvedindex = $fieldnames[get_string('approved', 'data')];
+ $approved = !empty($record[$approvedindex]);
+ }
+
+ if ($recordid = data_add_record($data, 0, $authorid, $approved)) { // Add instance to data_record.
foreach ($fields as $field) {
$fieldid = $fieldnames[$field->field->name];
if (isset($record[$fieldid])) {
diff --git a/mod/data/db/upgrade.php b/mod/data/db/upgrade.php
index 32718e31e2e9d..9b90b57aac2fc 100644
--- a/mod/data/db/upgrade.php
+++ b/mod/data/db/upgrade.php
@@ -79,5 +79,8 @@ function xmldb_data_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2023100901, 'data');
}
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/data/field.php b/mod/data/field.php
index 171e4aa055b0c..d8c66abc2f104 100644
--- a/mod/data/field.php
+++ b/mod/data/field.php
@@ -417,9 +417,9 @@
$OUTPUT->render_from_template('core/action_menu', $actionmenutemplate)
];
- if (!empty($missingfieldtypes)) {
- echo $OUTPUT->notification(get_string('missingfieldtypes', 'data') . html_writer::alist($missingfieldtypes));
- }
+ }
+ if (!empty($missingfieldtypes)) {
+ echo $OUTPUT->notification(get_string('missingfieldtypes', 'data') . html_writer::alist($missingfieldtypes));
}
echo html_writer::table($table);
diff --git a/mod/data/lib.php b/mod/data/lib.php
index 7715a3e4279e7..3bc9676dbe265 100644
--- a/mod/data/lib.php
+++ b/mod/data/lib.php
@@ -1156,9 +1156,11 @@ function data_numentries($data, $userid=null) {
* @param object $data
* @param int $groupid
* @param int $userid
+ * @param bool $approved If specified, and the user has the capability to approve entries, then this value
+ * will be used as the approved status of the new record
* @return bool
*/
-function data_add_record($data, $groupid = 0, $userid = null) {
+function data_add_record($data, $groupid = 0, $userid = null, bool $approved = true) {
global $USER, $DB;
$cm = get_coursemodule_from_instance('data', $data->id);
@@ -1170,7 +1172,7 @@ function data_add_record($data, $groupid = 0, $userid = null) {
$record->groupid = $groupid;
$record->timecreated = $record->timemodified = time();
if (has_capability('mod/data:approve', $context)) {
- $record->approved = 1;
+ $record->approved = $approved;
} else {
$record->approved = 0;
}
diff --git a/mod/data/tests/entries_import_test.php b/mod/data/tests/entries_import_test.php
index 66482dcbdde5f..8c2dd51d971a1 100644
--- a/mod/data/tests/entries_import_test.php
+++ b/mod/data/tests/entries_import_test.php
@@ -27,10 +27,12 @@
*
* @package mod_data
* @category test
+ * @covers \mod_data\local\importer\entries_importer
+ * @covers \mod_data\local\importer\csv_entries_importer
* @copyright 2019 Tobias Reischmann
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class entries_import_test extends \advanced_testcase {
+final class entries_import_test extends \advanced_testcase {
/**
* Set up function.
@@ -271,11 +273,74 @@ public function test_import_with_field_username_without_userdata(): void {
}
/**
- * Tests the import including files from a zip archive.
+ * Data provider for {@see test_import_without_approved}
+ *
+ * @return array[]
+ */
+ public static function import_without_approved_provider(): array {
+ return [
+ 'Teacher can approve entries' => ['teacher', [1, 1]],
+ 'Student cannot approve entries' => ['student', [0, 0]],
+ ];
+ }
+
+ /**
+ * Test importing file without approved status column
+ *
+ * @param string $user
+ * @param int[] $expected
+ *
+ * @dataProvider import_without_approved_provider
+ */
+ public function test_import_without_approved(string $user, array $expected): void {
+ $testdata = $this->get_test_data();
+ ['data' => $data, 'cm' => $cm] = $testdata;
+
+ $this->setUser($testdata[$user]);
+
+ $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import.csv', 'test_data_import.csv');
+ $importer->import_csv($cm, $data, 'UTF-8', 'comma');
+
+ $records = $this->get_data_records($data->id);
+ $this->assertEquals($expected, array_column($records, 'approved'));
+ }
+
+ /**
+ * Data provider for {@see test_import_with_approved}
*
- * @covers \mod_data\local\importer\entries_importer
- * @covers \mod_data\local\importer\csv_entries_importer
- * @return void
+ * @return array[]
+ */
+ public static function import_with_approved_provider(): array {
+ return [
+ 'Teacher can approve entries' => ['teacher', [1, 0]],
+ 'Student cannot approve entries' => ['student', [0, 0]],
+ ];
+ }
+
+ /**
+ * Test importing file with approved status column
+ *
+ * @param string $user
+ * @param int[] $expected
+ *
+ * @dataProvider import_with_approved_provider
+ */
+ public function test_import_with_approved(string $user, array $expected): void {
+ $testdata = $this->get_test_data();
+ ['data' => $data, 'cm' => $cm] = $testdata;
+
+ $this->setUser($testdata[$user]);
+
+ $importer = new csv_entries_importer(__DIR__ . '/fixtures/test_data_import_with_approved.csv',
+ 'test_data_import_with_approved.csv');
+ $importer->import_csv($cm, $data, 'UTF-8', 'comma');
+
+ $records = $this->get_data_records($data->id);
+ $this->assertEquals($expected, array_column($records, 'approved'));
+ }
+
+ /**
+ * Tests the import including files from a zip archive.
*/
public function test_import_with_files(): void {
[
@@ -323,10 +388,6 @@ public function test_import_with_files(): void {
/**
* Tests the import including files from a zip archive.
- *
- * @covers \mod_data\local\importer\entries_importer
- * @covers \mod_data\local\importer\csv_entries_importer
- * @return void
*/
public function test_import_with_files_missing_file(): void {
[
@@ -377,8 +438,6 @@ private function get_data_records(int $dataid): array {
/**
* Tests if the amount of imported records is counted properly.
*
- * @covers \mod_data\local\importer\csv_entries_importer::import_csv
- * @covers \mod_data\local\importer\csv_entries_importer::get_added_records_messages
* @dataProvider get_added_record_messages_provider
* @param string $datafilecontent the content of the datafile to test as string
* @param int $expectedcount the expected count of messages depending on the datafile content
diff --git a/mod/data/tests/fixtures/test_data_import_with_approved.csv b/mod/data/tests/fixtures/test_data_import_with_approved.csv
new file mode 100644
index 0000000000000..4a66bae07e5aa
--- /dev/null
+++ b/mod/data/tests/fixtures/test_data_import_with_approved.csv
@@ -0,0 +1,3 @@
+ID,Param2,Approved
+1,"My first entry",1
+2,"My second entry",0
diff --git a/mod/data/upgrade.txt b/mod/data/upgrade.txt
index 24d03ae0e6091..cb658a91a2301 100644
--- a/mod/data/upgrade.txt
+++ b/mod/data/upgrade.txt
@@ -1,6 +1,9 @@
This files describes API changes in /mod/data - plugins,
information provided here is intended especially for developers.
+=== 4.5 ===
+* The `data_add_record` method accepts a new `$approved` parameter to set the corresponding state of the new record
+
=== 4.4 ===
* The following behat steps are now deprecated. Use data generators instead:
- I add a ":fieldtype" field to ":activityname" database and I fill the form with:
diff --git a/mod/feedback/db/upgrade.php b/mod/feedback/db/upgrade.php
index db9af4ae70b01..8f6a5ae665af9 100644
--- a/mod/feedback/db/upgrade.php
+++ b/mod/feedback/db/upgrade.php
@@ -49,5 +49,8 @@ function xmldb_feedback_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/folder/db/upgrade.php b/mod/folder/db/upgrade.php
index 472407fe9a349..695191b3c8963 100644
--- a/mod/folder/db/upgrade.php
+++ b/mod/folder/db/upgrade.php
@@ -52,5 +52,8 @@ function xmldb_folder_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/forum/db/upgrade.php b/mod/forum/db/upgrade.php
index f28b2d2f83c6d..223f14f02ceb7 100644
--- a/mod/forum/db/upgrade.php
+++ b/mod/forum/db/upgrade.php
@@ -61,5 +61,8 @@ function xmldb_forum_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/forum/lib.php b/mod/forum/lib.php
index f8746ba995f38..77c61cdd0e22e 100644
--- a/mod/forum/lib.php
+++ b/mod/forum/lib.php
@@ -3107,7 +3107,10 @@ function forum_add_discussion($discussion, $mform=null, $unused=null, $userid=nu
}
if (isset($discussion->tags)) {
- core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id, context_module::instance($cm->id), $discussion->tags);
+ $tags = is_array($discussion->tags) ? $discussion->tags : explode(',', $discussion->tags);
+
+ core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id,
+ context_module::instance($cm->id), $tags);
}
if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
diff --git a/mod/glossary/db/upgrade.php b/mod/glossary/db/upgrade.php
index a7d93da22cbff..781db646855ad 100644
--- a/mod/glossary/db/upgrade.php
+++ b/mod/glossary/db/upgrade.php
@@ -49,5 +49,8 @@ function xmldb_glossary_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/glossary/tests/behat/glossary_display_formats.feature b/mod/glossary/tests/behat/glossary_display_formats.feature
new file mode 100644
index 0000000000000..2a0f0ae764df0
--- /dev/null
+++ b/mod/glossary/tests/behat/glossary_display_formats.feature
@@ -0,0 +1,99 @@
+@mod @mod_glossary
+Feature: Glossary can be set to various display formats
+ In order to display different glossary formats
+ As a teacher
+ I can set the glossary activity display format
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | One | teacher1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ Given the following "activities" exist:
+ | activity | course | name |
+ | glossary | C1 | Glossary 1 |
+ And the following "mod_glossary > entries" exist:
+ | glossary | concept | definition |
+ | Glossary 1 | Entry 1 | Entry 1 definition |
+ | Glossary 1 | Entry 2 | Entry 2 definition |
+
+ Scenario: Glossary display format is entry list style
+ Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1
+ And I set the following fields to these values:
+ | displayformat | entrylist |
+ When I press "Save and display"
+ # Confirm that glossary display format is entry list.
+ # In this format, the concept definitions are not displayed.
+ Then I should not see "by Admin User"
+ And I should not see "Entry 1 definition"
+ And I should not see "Entry 2 definition"
+ And ".entrylist" "css_element" should exist
+
+ Scenario: Glossary display format is FAQ-style
+ Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1
+ And I set the following fields to these values:
+ | displayformat | faq |
+ When I press "Save and display"
+ # Confirm that glossary format is FAQ.
+ # In this format, the words Question and Answer are displayed.
+ Then I should see "Question:"
+ And I should see "Answer:"
+ And ".faq" "css_element" should exist
+
+ @_file_upload @javascript
+ Scenario: Glossary display format is full without author style
+ Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1
+ And I set the following fields to these values:
+ | displayformat | fullwithoutauthor |
+ And I press "Save and display"
+ And I press "Add entry"
+ # Add an entry with an attachment.
+ And I set the following fields to these values:
+ | Concept | Entry 3 |
+ | Definition | Entry 3 definition |
+ | Attachment | lib/tests/fixtures/gd-logo.png |
+ When I press "Save changes"
+ # Confirm that glossary format is full without author style.
+ # In this format, the image link should exist and author's name should not be visible.
+ Then "gd-logo.png" "link" should exist
+ And I should not see "by Admin User"
+ And ".fullwithoutauthor" "css_element" should exist
+
+ @_file_upload @javascript
+ Scenario: Glossary display format is encyclopedia style
+ Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1
+ And I set the following fields to these values:
+ | displayformat | encyclopedia |
+ And I press "Save and display"
+ And I press "Add entry"
+ # Add an entry with an attachment.
+ And I set the following fields to these values:
+ | Concept | Entry 3 |
+ | Definition | Entry 3 definition |
+ | Attachment | lib/tests/fixtures/gd-logo.png |
+ When I press "Save changes"
+ # Confirm that glossary format is encyclopedia.
+ # In this format, the image element should be displayed.
+ Then "//img[contains(@src, 'gd-logo.png')]" "xpath_element" should exist
+ And ".encyclopedia" "css_element" should exist
+
+ Scenario Outline: Glossary display format can be set to dictionary, continuous and full with author
+ Given I am on the "Glossary 1" "glossary activity editing" page logged in as teacher1
+ # Assign the corresponding display format to glossary activity.
+ And I set the following fields to these values:
+ | displayformat | |
+ When I press "Save and display"
+ # Confirm that glossary format is the display format set in the previous step.
+ Then I should "by Admin User"
+ And "." "css_element" should exist
+
+ Examples:
+ | display_format | visibility |
+ | dictionary | not see |
+ | continuous | not see |
+ | fullwithauthor | see |
diff --git a/mod/glossary/tests/generator/lib.php b/mod/glossary/tests/generator/lib.php
index 56debf62ccaeb..67a3be40ebbc5 100644
--- a/mod/glossary/tests/generator/lib.php
+++ b/mod/glossary/tests/generator/lib.php
@@ -188,6 +188,16 @@ public function create_entry(array $data): stdClass {
$DB->insert_record('glossary_entries_categories', ['entryid' => $id, 'categoryid' => $categoryid]);
}
- return $DB->get_record('glossary_entries', array('id' => $id), '*', MUST_EXIST);
+ $entries = $DB->get_record('glossary_entries', ['id' => $id], '*', MUST_EXIST);
+
+ if (isset($record['tags'])) {
+ $cm = get_coursemodule_from_instance('glossary', $glossary->id);
+ $tags = is_array($record['tags']) ? $record['tags'] : explode(',', $record['tags']);
+
+ core_tag_tag::set_item_tags('mod_glossary', 'glossary_entries', $id,
+ context_module::instance($cm->id), $tags);
+ }
+
+ return $entries;
}
}
diff --git a/mod/h5pactivity/db/upgrade.php b/mod/h5pactivity/db/upgrade.php
index 46512f2fc95fb..31b7c7d2ad511 100644
--- a/mod/h5pactivity/db/upgrade.php
+++ b/mod/h5pactivity/db/upgrade.php
@@ -70,5 +70,8 @@ function xmldb_h5pactivity_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/h5pactivity/tests/behat/h5pactivity_availability.feature b/mod/h5pactivity/tests/behat/h5pactivity_availability.feature
new file mode 100644
index 0000000000000..b627a3a40d43e
--- /dev/null
+++ b/mod/h5pactivity/tests/behat/h5pactivity_availability.feature
@@ -0,0 +1,41 @@
+@mod @mod_h5pactivity @core_5hp
+Feature: Control H5P activity availability for students
+ In order to restrict student access to H5P activity
+ As a teacher
+ I need to control the availability of the H5P activity
+
+ Background:
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ And the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ And the following "activities" exist:
+ | activity | course | name |
+ | h5pactivity | C1 | H5P Test |
+
+ @javascript
+ Scenario Outline: Restrict H5P activity access by date
+ Given I am on the "H5P Test" "h5pactivity activity editing" page logged in as teacher1
+ And I expand all fieldsets
+ And I click on "Add restriction..." "button"
+ And I click on "Date" "button" in the "Add restriction..." "dialogue"
+ And I set the following fields to these values:
+ | Direction | from |
+ | x[day] | 1 |
+ | x[month] | 1 |
+ | x[year] | |
+ And I press "Save and return to course"
+ When I am on the "Course 1" course page logged in as student1
+ Then I see "Available from"
+
+ Examples:
+ | year | fromvisibility |
+ | ## -1 year ## %Y ## | should not |
+ | ## +1 year ## %Y ## | should |
diff --git a/mod/imscp/db/upgrade.php b/mod/imscp/db/upgrade.php
index ceb73199d81d5..6b51ba8f9fcba 100644
--- a/mod/imscp/db/upgrade.php
+++ b/mod/imscp/db/upgrade.php
@@ -36,5 +36,8 @@ function xmldb_imscp_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/label/db/upgrade.php b/mod/label/db/upgrade.php
index c6edb5550e7c5..e0d32cbe8d272 100644
--- a/mod/label/db/upgrade.php
+++ b/mod/label/db/upgrade.php
@@ -97,5 +97,8 @@ function xmldb_label_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/lesson/db/upgrade.php b/mod/lesson/db/upgrade.php
index 21afebb6ca788..eb412b7fa07de 100644
--- a/mod/lesson/db/upgrade.php
+++ b/mod/lesson/db/upgrade.php
@@ -57,5 +57,8 @@ function xmldb_lesson_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/lti/db/upgrade.php b/mod/lti/db/upgrade.php
index 01f94cb5f95a9..cd1b205ea35b3 100644
--- a/mod/lti/db/upgrade.php
+++ b/mod/lti/db/upgrade.php
@@ -119,5 +119,8 @@ function xmldb_lti_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/lti/service/gradebookservices/db/upgrade.php b/mod/lti/service/gradebookservices/db/upgrade.php
index 6f811f46bc5ed..a4419f0bf13b5 100644
--- a/mod/lti/service/gradebookservices/db/upgrade.php
+++ b/mod/lti/service/gradebookservices/db/upgrade.php
@@ -62,5 +62,8 @@ function xmldb_ltiservice_gradebookservices_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/page/db/upgrade.php b/mod/page/db/upgrade.php
index 95a862e44e701..1c904d41241cd 100644
--- a/mod/page/db/upgrade.php
+++ b/mod/page/db/upgrade.php
@@ -52,5 +52,8 @@ function xmldb_page_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/quiz/accessrule/seb/db/upgrade.php b/mod/quiz/accessrule/seb/db/upgrade.php
index f6529564f8f69..27ff6254b5a39 100644
--- a/mod/quiz/accessrule/seb/db/upgrade.php
+++ b/mod/quiz/accessrule/seb/db/upgrade.php
@@ -43,5 +43,8 @@ function xmldb_quizaccess_seb_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php
index 21833b7fb14b2..fa22cf4cf5f79 100644
--- a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php
+++ b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php
@@ -443,7 +443,7 @@ protected function process_quiz_question_instance($data) {
}
if (isset($data->quizgradeitemid)) {
- $data->quizgradeitemid = $this->get_mappingid('quiz_grade_items', $data->quizgradeitemid);
+ $data->quizgradeitemid = $this->get_mappingid('quiz_grade_item', $data->quizgradeitemid);
}
$data->quizid = $this->get_new_parentid('quiz');
diff --git a/mod/quiz/classes/external.php b/mod/quiz/classes/external.php
index dd4a95eca80c6..685d409632a2a 100644
--- a/mod/quiz/classes/external.php
+++ b/mod/quiz/classes/external.php
@@ -1491,6 +1491,10 @@ public static function get_attempt_review($attemptid, $page = -1) {
$page = 'all';
}
+ // Make sure all users associated to the attempt steps are loaded. Otherwise, this will
+ // trigger a debugging message.
+ $attemptobj->preload_all_attempt_step_users();
+
// Prepare the output.
$result = [];
$result['attempt'] = $attemptobj->get_attempt();
diff --git a/mod/quiz/classes/question/bank/preview_action_column.php b/mod/quiz/classes/question/bank/preview_action_column.php
index 3ef99150ed7ee..7babf66f9fdd0 100644
--- a/mod/quiz/classes/question/bank/preview_action_column.php
+++ b/mod/quiz/classes/question/bank/preview_action_column.php
@@ -31,14 +31,22 @@ public function get_extra_classes(): array {
return ['iconcol'];
}
+ #[\Override]
public function get_title(): string {
return ' ';
}
+ #[\Override]
public function get_name() {
return 'previewquestionaction';
}
+ #[\Override]
+ public function get_default_width(): int {
+ return 45;
+ }
+
+ #[\Override]
protected function display_content($question, $rowclasses) {
global $PAGE;
if (!question_has_capability_on($question, 'use')) {
diff --git a/mod/quiz/classes/question/bank/question_name_text_column.php b/mod/quiz/classes/question/bank/question_name_text_column.php
index 9e6e568d029f7..41d4bad8cb208 100644
--- a/mod/quiz/classes/question/bank/question_name_text_column.php
+++ b/mod/quiz/classes/question/bank/question_name_text_column.php
@@ -27,10 +27,18 @@
*/
class question_name_text_column extends question_name_column {
+ #[\Override]
public function get_name(): string {
return 'questionnametext';
}
+ #[\Override]
+ public function get_default_width(): int {
+ // In the places this is used, this seems to make it use all the available space, without overflowing.
+ return 800;
+ }
+
+ #[\Override]
protected function display_content($question, $rowclasses): void {
echo \html_writer::start_tag('div');
$labelfor = $this->label_for($question);
@@ -44,6 +52,7 @@ protected function display_content($question, $rowclasses): void {
echo \html_writer::end_tag('div');
}
+ #[\Override]
public function get_required_fields(): array {
$fields = parent::get_required_fields();
$fields[] = 'q.questiontext';
@@ -52,6 +61,7 @@ public function get_required_fields(): array {
return $fields;
}
+ #[\Override]
public function load_additional_data(array $questions) {
parent::load_additional_data($questions);
parent::load_question_tags($questions);
diff --git a/mod/quiz/classes/question/bank/random_question_view.php b/mod/quiz/classes/question/bank/random_question_view.php
index 57bfb70ff6aaa..19c5a4ddead0a 100644
--- a/mod/quiz/classes/question/bank/random_question_view.php
+++ b/mod/quiz/classes/question/bank/random_question_view.php
@@ -16,6 +16,8 @@
namespace mod_quiz\question\bank;
+use qbank_viewquestiontype\question_type_column;
+
/**
* Subclass to customise the view of the question bank for the quiz editing screen.
*
@@ -25,24 +27,16 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class random_question_view extends custom_view {
- /**
- * Init required columns.
- *
- * @return void
- */
- protected function init_required_columns(): void {
- // Override core columns.
- $this->corequestionbankcolumns = [
- 'question_type_column',
- 'question_name_text_column',
- 'preview_action_column'
+ #[\Override]
+ protected function get_question_bank_plugins(): array {
+ return [
+ new question_type_column($this),
+ new question_name_text_column($this),
+ new preview_action_column($this),
];
}
- /**
- * Prints the table of questions in a category with interactions
- *
- * @param \context $catcontext*/
+ #[\Override]
protected function display_bottom_controls(\context $catcontext): void {
}
}
diff --git a/mod/quiz/db/upgrade.php b/mod/quiz/db/upgrade.php
index a296b7020b4da..4748c3ad20011 100644
--- a/mod/quiz/db/upgrade.php
+++ b/mod/quiz/db/upgrade.php
@@ -132,5 +132,8 @@ function xmldb_quiz_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2023112402, 'quiz');
}
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/quiz/lib.php b/mod/quiz/lib.php
index 36a613b7aa439..98ac5626f2060 100644
--- a/mod/quiz/lib.php
+++ b/mod/quiz/lib.php
@@ -2405,6 +2405,9 @@ function mod_quiz_output_fragment_add_random_question_form($args) {
'questioncategoryoptions' => $catoptions,
];
+ $helpicon = new \help_icon('parentcategory', 'question');
+ $data['questioncategoryhelp'] = $helpicon->export_for_template($renderer);
+
$result = $OUTPUT->render_from_template('mod_quiz/add_random_question_form', $data);
return $result;
@@ -2434,24 +2437,24 @@ function mod_quiz_core_calendar_get_event_action_string(string $eventtype): stri
}
/**
- * Delete question reference data.
+ * Delete all question references for a quiz.
*
* @param int $quizid The id of quiz.
*/
function quiz_delete_references($quizid): void {
global $DB;
- $slots = $DB->get_records('quiz_slots', ['quizid' => $quizid]);
- foreach ($slots as $slot) {
- $params = [
- 'itemid' => $slot->id,
- 'component' => 'mod_quiz',
- 'questionarea' => 'slot'
- ];
- // Delete any set references.
- $DB->delete_records('question_set_references', $params);
- // Delete any references.
- $DB->delete_records('question_references', $params);
- }
+
+ $cm = get_coursemodule_from_instance('quiz', $quizid);
+ $context = context_module::instance($cm->id);
+
+ $conditions = [
+ 'usingcontextid' => $context->id,
+ 'component' => 'mod_quiz',
+ 'questionarea' => 'slot',
+ ];
+
+ $DB->delete_records('question_references', $conditions);
+ $DB->delete_records('question_set_references', $conditions);
}
/**
diff --git a/mod/quiz/report/overview/db/upgrade.php b/mod/quiz/report/overview/db/upgrade.php
index 621d4d9adde1c..b608b162760a5 100644
--- a/mod/quiz/report/overview/db/upgrade.php
+++ b/mod/quiz/report/overview/db/upgrade.php
@@ -36,5 +36,8 @@ function xmldb_quiz_overview_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/quiz/report/statistics/db/upgrade.php b/mod/quiz/report/statistics/db/upgrade.php
index fefbfbaf371eb..0520dc77f292c 100644
--- a/mod/quiz/report/statistics/db/upgrade.php
+++ b/mod/quiz/report/statistics/db/upgrade.php
@@ -35,5 +35,8 @@ function xmldb_quiz_statistics_upgrade($oldversion) {
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
+ // Automatically generated Moodle v4.4.0 release upgrade line.
+ // Put any upgrade step following this.
+
return true;
}
diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css
index 0cf4f33774ce0..64d2683b0d098 100644
--- a/mod/quiz/styles.css
+++ b/mod/quiz/styles.css
@@ -559,7 +559,6 @@ table.quizreviewsummary td.cell {
}
#page-mod-quiz-edit .section-heading .instancesectioncontainer {
- font-size: 24px;
display: inline;
}
diff --git a/mod/quiz/templates/add_random_question_form.mustache b/mod/quiz/templates/add_random_question_form.mustache
index 6a95c3c3f9729..5cf4e428c1bd5 100644
--- a/mod/quiz/templates/add_random_question_form.mustache
+++ b/mod/quiz/templates/add_random_question_form.mustache
@@ -33,11 +33,24 @@
{"name": "Category 3", "value": "2"}
]
},
+ "questioncategoryhelp": {
+ "title": "Help with something",
+ "text": "Help with something",
+ "url": "http://example.org/help",
+ "linktext": "",
+ "icon": {
+ "extraclasses": "iconhelp",
+ "attributes": [
+ {"name": "src", "value": "../../../pix/help.svg"},
+ {"name": "alt", "value": "Help icon"}
+ ]
+ }
+ },
"questionbank": "Question bank HTML goes here
"
}
}}
-