|
diff --git a/public/group/templates/comboboxsearch/resultitem.mustache b/public/group/templates/comboboxsearch/resultitem.mustache
index 2dd40d997d28c..cccccbfa5169f 100644
--- a/public/group/templates/comboboxsearch/resultitem.mustache
+++ b/public/group/templates/comboboxsearch/resultitem.mustache
@@ -30,13 +30,18 @@
}}
{{
-
+
+
-
-
+
+
{{name}}
+ {{^participation}}
+
+ {{#str}} nonparticipation, group {{/str}}
+
+ {{/participation}}
{{/content}}
{{/core/local/comboboxsearch/resultitem}}
diff --git a/public/group/templates/comboboxsearch/resultset.mustache b/public/group/templates/comboboxsearch/resultset.mustache
index fe077b9841d0b..b87b90c99818e 100644
--- a/public/group/templates/comboboxsearch/resultset.mustache
+++ b/public/group/templates/comboboxsearch/resultset.mustache
@@ -28,12 +28,20 @@
{
"id": 2,
"name": "Foo bar",
- "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2"
+ "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=2",
+ "participation": true
},
{
"id": 3,
"name": "Bar Foo",
- "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3"
+ "link": "http://foo.bar/grade/report/grader/index.php?id=42&userid=3",
+ "participation": true
+ },
+ {
+ "id": 4,
+ "name": "Baz Bar Foo",
+ "link": "http://foo.bar/grade/report/grader/index.php?id=43&userid=2",
+ "participation": false
}
],
"instance": 25,
diff --git a/public/group/tests/behat/group_description_picture.feature b/public/group/tests/behat/group_description_picture.feature
index 64b2d9e5cc4fa..aeb59cc09a871 100644
--- a/public/group/tests/behat/group_description_picture.feature
+++ b/public/group/tests/behat/group_description_picture.feature
@@ -21,15 +21,17 @@ Feature: The description and picture of a group can be viewed by students and te
@javascript @_file_upload
Scenario: A student can see the group description and picture when visible groups are set. Teachers can see group details.
- Given I am on the "Course 1" "course editing" page logged in as "teacher1"
+ Given the "multilang" filter is "on"
+ And the "multilang" filter applies to "content and headings"
+ And I am on the "Course 1" "course editing" page logged in as "teacher1"
And I set the following fields to these values:
| Group mode | Visible groups |
And I press "Save and display"
And I am on the "Course 1" "groups" page
And I press "Create group"
And I set the following fields to these values:
- | Group name | Group A |
- | Group description | Description for Group A |
+ | Group name | Group A & < > " 'Gruppe A |
+ | Group description | Description for Group A |
# Upload group picture
And I upload "lib/tests/fixtures/gd-logo.png" file to "New picture" filemanager
And I press "Save changes"
@@ -58,7 +60,7 @@ Feature: The description and picture of a group can be viewed by students and te
And I click on "Student 1" "link" in the "participants" "table"
And I click on "Group A" "link"
# As student, confirm that group description and picture is displayed
- Then I should see "Description for Group A"
+ And I should see "Description for Group A"
And "//img[@class='grouppicture']" "xpath_element" should exist
And I am on the "Course 1" course page logged in as student2
And I navigate to course participants
@@ -66,6 +68,14 @@ Feature: The description and picture of a group can be viewed by students and te
And I click on "Group B" "link"
And I should see "Student 2" in the "participants" "table"
And ".groupinfobox" "css_element" should not exist
+ # As teacher, confirm that the group picture in the edit form has the correct alt text
+ And I am on the "Course 1" "groups" page logged in as teacher1
+ And I set the field "groups" to "Group A"
+ And I press "Edit group settings"
+ Then the "alt" attribute of "//*[contains(@data-name, 'currentpicture')]//img" "xpath_element" should contain "Group A & <"
+ But the "alt" attribute of "//*[contains(@data-name, 'currentpicture')]//img" "xpath_element" should not contain "span"
+ And the "title" attribute of "//*[contains(@data-name, 'currentpicture')]//img" "xpath_element" should contain "Group A & <"
+ But the "title" attribute of "//*[contains(@data-name, 'currentpicture')]//img" "xpath_element" should not contain "span"
@javascript @_file_upload
Scenario: A student can not see the group description and picture when separate groups are set. Teachers can see group details.
diff --git a/public/h5p/h5plib/v127/joubel/core/h5p.classes.php b/public/h5p/h5plib/v127/joubel/core/h5p.classes.php
index a21354f71a5b8..13e9b0947a2b4 100644
--- a/public/h5p/h5plib/v127/joubel/core/h5p.classes.php
+++ b/public/h5p/h5plib/v127/joubel/core/h5p.classes.php
@@ -683,7 +683,7 @@ class H5PValidator {
// Schemas used to validate the h5p files
private $h5pRequired = array(
- 'title' => '/^.{1,255}$/',
+ 'title' => '/^.{1,255}$/u',
'language' => '/^[-a-zA-Z]{1,10}$/',
'preloadedDependencies' => array(
'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
diff --git a/public/h5p/h5plib/v127/joubel/core/readme_moodle.txt b/public/h5p/h5plib/v127/joubel/core/readme_moodle.txt
index 862029dcc9b04..2c5c2f3ad29db 100644
--- a/public/h5p/h5plib/v127/joubel/core/readme_moodle.txt
+++ b/public/h5p/h5plib/v127/joubel/core/readme_moodle.txt
@@ -44,7 +44,10 @@ The library needs to be saved in the database first before creating the files, b
5. Check if new methods have been added to any of the interfaces. If that's the case, implement them in the proper class. For
instance, if a new method is added to h5p-file-storage.interface.php, it should be implemented in h5p/classes/file_storage.php.
-6. Open js/h5p.js and in function contentUserDataAjax() add the following patch:
+6. In the H5PValidator class in core/h5p.classes.php, ensure $h5pRequired['title'] regular expression contains "u" modifier
+ until https://github.com/h5p/h5p-php-library/issues/276 is resolved and the library is upgraded to a version containing that fix
+
+7. Open js/h5p.js and in function contentUserDataAjax() add the following patch:
function contentUserDataAjax(contentId, dataType, subContentId, done, data, preload, invalidate, async) {
if (H5PIntegration.user === undefined) {
// Not logged in, no use in saving.
diff --git a/public/h5p/js/embed.js b/public/h5p/js/embed.js
index 72ce9aa8bb5aa..6013e31c047f8 100644
--- a/public/h5p/js/embed.js
+++ b/public/h5p/js/embed.js
@@ -226,6 +226,48 @@ document.onreadystatechange = async() => {
const Pending = await getPendingClass();
var resizePending = new Pending('core_h5p/iframe:resize');
+ // Track when the embedded H5P content is fully attached.
+ const contentLoadedPending = new Pending('core_h5p/iframe:contentLoaded');
+ let contentLoaded = false;
+ const markContentLoaded = function() {
+ const body = iFrame?.contentDocument?.body;
+ const hasContent = body && body.querySelector('.h5p-container, .h5p-content');
+
+ // Check that H5P instance is actually ready with proper initialization
+ if (contentLoaded || !hasContent || !H5P?.instances?.[0]) {
+ return false;
+ }
+
+ contentLoaded = true;
+ contentLoadedPending.resolve();
+ H5PEmbedCommunicator.send('contentLoaded');
+ return true;
+ };
+
+ // If the content is already there, mark it immediately.
+ markContentLoaded();
+
+ // Observe the iframe document for the first appearance of the H5P container.
+ if (!contentLoaded && iFrame.contentDocument?.body) {
+ const contentObserver = new MutationObserver(function() {
+ if (markContentLoaded()) {
+ contentObserver.disconnect();
+ }
+ });
+ contentObserver.observe(iFrame.contentDocument.body, {childList: true, subtree: true});
+ }
+
+ // Extended fallback timeout to ensure pending resolves even if content detection fails.
+ if (!contentLoaded) {
+ setTimeout(function() {
+ if (!contentLoaded) {
+ contentLoaded = true;
+ contentLoadedPending.resolve();
+ H5PEmbedCommunicator.send('contentLoaded');
+ }
+ }, 1000);
+ }
+
H5P.on(instance, 'resize', function() {
if (H5P.isFullscreen) {
return; // Skip iframe resize.
@@ -249,6 +291,8 @@ document.onreadystatechange = async() => {
}, 150);
});
+ H5P.externalDispatcher.on('domChanged', markContentLoaded);
+
// Get emitted xAPI data.
H5P.externalDispatcher.on('xAPI', function(event) {
statementPosted = false;
diff --git a/public/install.php b/public/install.php
index 60a15e8c8d464..9554e80fd9a6d 100644
--- a/public/install.php
+++ b/public/install.php
@@ -557,6 +557,7 @@
'dirroot' => get_string('dirroot', 'install'),
'dataroot' => get_string('dataroot', 'install'));
+ $stageclass = "alert-info";
$sub = '';
foreach ($paths as $path=>$name) {
$sub .= '- '.$name.'
- '.get_string('pathssub'.$path, 'install').'
';
@@ -566,7 +567,22 @@
}
$sub .= ' ';
- install_print_header($config, get_string('paths', 'install'), get_string('pathshead', 'install'), $sub);
+ $warnings = '';
+ $wwwroot = $CFG->wwwroot;
+ if (str_ends_with($CFG->wwwroot, '/public')) {
+ $wwwroot = substr($CFG->wwwroot, 0, -7);
+ $warnings .= '' . get_string('webservernotconfigured', 'install') . '';
+ $warnings .= '' . get_string('webserverconfigproblemdescription', 'install', s($wwwroot)) . '';
+ }
+
+ install_print_header(
+ $config,
+ get_string('paths', 'install'),
+ get_string('pathshead', 'install'),
+ $sub,
+ $stageclass,
+ $warnings,
+ );
$strwwwroot = get_string('wwwroot', 'install');
$strdirroot = get_string('dirroot', 'install');
@@ -606,7 +622,8 @@
}
}
- install_print_footer($config);
+ $requiresreload = ($warnings !== '');
+ install_print_footer($config, $requiresreload);
die;
}
diff --git a/public/install/lang/ar/install.php b/public/install/lang/ar/install.php
index 6f488b269d1c4..ce1e271bf3c30 100644
--- a/public/install/lang/ar/install.php
+++ b/public/install/lang/ar/install.php
@@ -69,6 +69,8 @@
$string['pathswrongadmindir'] = 'مجلد المشرف غير موجود';
$string['phpextension'] = 'إمتداد PHP {$a}';
$string['phpversion'] = 'إصدار PHP';
+$string['webserverconfigproblemdescription'] = 'إن مخدمك ليس مهيئًا لمنع الوصول إلى الملفات الواقعة خارج المجلد /public. يرجى الرجوع إلى https://moodledev.io/docs/5.1/guides/restructure للاطلاع على تفاصيل كيفية تهيئة مخدمك. بمجرد إعادة تهيئته، ترجى زيارة مجلد الويب الأساسي.';
+$string['webservernotconfigured'] = 'مخدم الويب غير مهيأ';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'أنت تشاهد هذه الصفحة لأنك تمكنت بنجاح من تنصيب وإطلاق
حزمة {$a->packname} {$a->packversion} في حاسبتك. تهانينا!';
diff --git a/public/install/lang/ca/error.php b/public/install/lang/ca/error.php
index c505e773aca94..e68c9449884e2 100644
--- a/public/install/lang/ca/error.php
+++ b/public/install/lang/ca/error.php
@@ -33,7 +33,7 @@
L\'administrador del lloc hauria de verificar la configuració de la base de dades. ';
$string['cannotcreatelangdir'] = 'No s\'ha pogut crear el directori d\'idiomes';
$string['cannotcreatetempdir'] = 'No s\'ha pogut crear el directori temporal';
-$string['cannotdownloadcomponents'] = 'No s\'han pogut baixar components';
+$string['cannotdownloadcomponents'] = 'No s\'han pogut descarregar els components';
$string['cannotdownloadzipfile'] = 'No s\'ha pogut baixar el fitxer ZIP';
$string['cannotfindcomponent'] = 'No s\'ha pogut trobar el component';
$string['cannotsavemd5file'] = 'No s\'ha pogut desar el fitxer md5';
@@ -46,6 +46,6 @@
$string['missingrequiredfield'] = 'Falta algun camp necessari';
$string['remotedownloaderror'] = 'No s\'ha pogut baixar el component al vostre servidor. Verifiqueu els paràmetres del servidor intermediari. Es recomana vivament l\'extensió cURL de PHP.
Haureu de baixar manualment el fitxer {$a->url}, copiar-lo a la ubicació «{$a->dest}» del vostre servidor i descomprimir-lo allà. ';
-$string['wrongdestpath'] = 'El camí de destinació és erroni';
-$string['wrongsourcebase'] = 'L\'adreça (URL) base de la font és errònia';
+$string['wrongdestpath'] = 'El camí de destinació és incorrecte';
+$string['wrongsourcebase'] = 'L\'URL base de la font és errònia';
$string['wrongzipfilename'] = 'El nom del fitxer ZIP és erroni';
diff --git a/public/install/lang/cs/install.php b/public/install/lang/cs/install.php
index 524141805716c..083599878dcaf 100644
--- a/public/install/lang/cs/install.php
+++ b/public/install/lang/cs/install.php
@@ -68,6 +68,8 @@
$string['pathswrongadmindir'] = 'Adresář pro správu serveru (admin) neexistuje';
$string['phpextension'] = '{$a} PHP rozšíření';
$string['phpversion'] = 'Verze PHP';
+$string['webserverconfigproblemdescription'] = 'Váš webový server není nakonfigurován tak, aby bránil přístupu k souborům mimo adresář /public. Podrobnosti o správné konfiguraci webového serveru naleznete v dokumentaci Aktualizace - Restrukturalizace adresářů kódu. Po rekonfiguraci znovu navštivte kořenový adresář webu.';
+$string['webservernotconfigured'] = 'Webový server není nakonfigurován';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Tuto stránku vidíte, protože jste úspěšně nainstalovali a spustili balíček {$a->packname} {$a->packversion}. Gratulujeme!';
$string['welcomep30'] = 'Tato verze {$a->installername} obsahuje aplikace k vytvoření prostředí, ve kterém bude provozován váš Moodle. Jmenovitě se jedná o:';
diff --git a/public/install/lang/de/install.php b/public/install/lang/de/install.php
index 27f7ed932adb5..a76ea17ab2d4e 100644
--- a/public/install/lang/de/install.php
+++ b/public/install/lang/de/install.php
@@ -64,6 +64,8 @@
$string['pathswrongadmindir'] = 'Das Admin-Verzeichnis existiert nicht';
$string['phpextension'] = 'PHP-Extension {$a}';
$string['phpversion'] = 'PHP-Version';
+$string['webserverconfigproblemdescription'] = 'Ihr Webserver ist nicht so konfiguriert, dass der Zugriff auf Dateien außerhalb des Verzeichnisses "/public" verhindert wird. Weitere Details zur Konfiguration Ihres Webservers finden Sie unter Upgrading - Code directories restructure . Nach der Neukonfiguration rufen Sie bitte das WebRoot-Verzeichnis erneut auf.';
+$string['webservernotconfigured'] = 'Webserver nicht konfiguriert';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Sie haben das Paket {$a->packname} {$a->packversion} erfolgreich auf Ihrem Computer installiert.';
$string['welcomep30'] = 'Diese Version von {$a->installername} enthält folgende Anwendungen, mit denen Sie Moodle ausführen können:';
diff --git a/public/install/lang/en/install.php b/public/install/lang/en/install.php
index 70a7b0a5a6053..db9a42a2d4eb8 100644
--- a/public/install/lang/en/install.php
+++ b/public/install/lang/en/install.php
@@ -71,6 +71,8 @@
$string['pathswrongadmindir'] = 'Admin directory does not exist';
$string['phpextension'] = '{$a} PHP extension';
$string['phpversion'] = 'PHP version';
+$string['webserverconfigproblemdescription'] = 'Your web server is not configured to prevent access to files outside the /public directory. For details of how to configure your web server correctly, see the documentation Upgrading - Code directories restructure. Once reconfigured, revisit the web root.';
+$string['webservernotconfigured'] = 'Web server not configured';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'You are seeing this page because you have successfully installed and
launched the {$a->packname} {$a->packversion} package in your computer. Congratulations!';
diff --git a/public/install/lang/es/install.php b/public/install/lang/es/install.php
index 9f2007aa00d5e..b6b2a96f074da 100644
--- a/public/install/lang/es/install.php
+++ b/public/install/lang/es/install.php
@@ -69,6 +69,8 @@
$string['pathswrongadmindir'] = 'El directorio admin no existe';
$string['phpextension'] = 'Extensión PHP {$a}';
$string['phpversion'] = 'Versión PHP';
+$string['webserverconfigproblemdescription'] = 'Su servidor web no está configurado para evitar acceso a archivos fuera del directorio /public. Para detalles sobre cómo configurar su servidor web correctamente, consulte la documentación Upgrading - Code directories restructure. Una vez reconfigurado, vuelva a visitar la raíz de la web.';
+$string['webservernotconfigured'] = 'Servidor web no configurado';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Si está viendo esta página es porque ha podido ejecutar el paquete {$a->packname} {$a->packversion} satisfactoriamente en su ordenador. ¡Enhorabuena!';
$string['welcomep30'] = 'Esta versión de {$a->installername} incluye las aplicaciones necesarias para que Moodle funcione en su computadora, principalmente:';
diff --git a/public/install/lang/es_mx/install.php b/public/install/lang/es_mx/install.php
index ad08126041245..b464af097ba73 100644
--- a/public/install/lang/es_mx/install.php
+++ b/public/install/lang/es_mx/install.php
@@ -69,6 +69,8 @@
$string['pathswrongadmindir'] = 'El directorio admin no existe';
$string['phpextension'] = 'Extensión PHP {$a}';
$string['phpversion'] = 'Versión PHP';
+$string['webserverconfigproblemdescription'] = 'Su servidor web no está configurado para impedir el acceso a archivos fuera del directorio /public. Para conocer los detalles acerca de cómo configurar correctamente su servidor web, consulte la documentación en Upgrading - Code directories restructure. Una vez que haya reconfigurado esto, vuelva a visitar la raíz del servidor web.';
+$string['webservernotconfigured'] = 'Servidor web no cnfigurado';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Si está viendo esta página es porque ha podido instalar y ejecutar exitosamente el paquete {$a->packname} {$a->packversion} en su computadora. !Enhorabuena!';
$string['welcomep30'] = 'Esta versión de {$a->installername} incluye las aplicaciones necesarias para que Moodle funcione en su computadora, principalmente:';
diff --git a/public/install/lang/es_ve/admin.php b/public/install/lang/es_ve/admin.php
new file mode 100644
index 0000000000000..fbb7d90f7421a
--- /dev/null
+++ b/public/install/lang/es_ve/admin.php
@@ -0,0 +1,41 @@
+.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link https://moodledev.io/general/projects/api/amos}) using the
+ * list of strings defined in public/install/stringnames.txt file.
+ *
+ * @package installer
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['clianswerno'] = 'n';
+$string['cliansweryes'] = 's';
+$string['cliincorrectvalueerror'] = 'Error, valor incorrecto "{$a->value}" para "{$a->option}"';
+$string['cliincorrectvalueretry'] = 'Valor incorrecto, por favor intenta de nuevo';
+$string['clitypevalue'] = 'escribe el valor';
+$string['clitypevaluedefault'] = 'escribe el valor, presiona Enter para usar el valor predeterminado ({$a})';
+$string['cliunknowoption'] = 'Opciones no reconocidas:
+ {$a}
+Por favor usa la opción --help.';
+$string['cliyesnoprompt'] = 'escribe s (significa sí) o n (significa no)';
diff --git a/public/install/lang/eu/install.php b/public/install/lang/eu/install.php
index 442fd0b086c33..3b650e7f1d2bf 100644
--- a/public/install/lang/eu/install.php
+++ b/public/install/lang/eu/install.php
@@ -68,6 +68,8 @@
$string['pathswrongadmindir'] = 'Admin direktorioa ez da existitzen';
$string['phpextension'] = '{$a} PHP hedapena';
$string['phpversion'] = 'PHP bertsioa';
+$string['webserverconfigproblemdescription'] = 'Zure web zerbitzaria ez dago /public direktoriotik kanpoko fitxategietara sarbidea saihesteko. Zure web zerbitzaria konfiguratzeko xehetasunetarako egin klik hemen: Upgrading - Code directories restructure. Behin konfigurazioa aldatzean, joan berriz webgunearen jatorrira.';
+$string['webservernotconfigured'] = 'Web zerbitzaria konfiguratu gabe';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Orri hau ikusten baduzu {$a->packname} {$a->packversion} paketea zure ordenagailuan instalatu ahal izan duzu. Zorionak!';
$string['welcomep30'] = '{$a->installername}ren bertsio honek Moodle-k
diff --git a/public/install/lang/fo/admin.php b/public/install/lang/fo/admin.php
index a619c73b71275..01b938c7f34db 100644
--- a/public/install/lang/fo/admin.php
+++ b/public/install/lang/fo/admin.php
@@ -31,3 +31,4 @@
$string['clianswerno'] = '';
$string['cliansweryes'] = '';
+$string['cliincorrectvalueretry'] = 'Skeivt virði, vinarliga royn aftur';
diff --git a/public/install/lang/fo/install.php b/public/install/lang/fo/install.php
new file mode 100644
index 0000000000000..aaac9482e4d21
--- /dev/null
+++ b/public/install/lang/fo/install.php
@@ -0,0 +1,32 @@
+.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link https://moodledev.io/general/projects/api/amos}) using the
+ * list of strings defined in public/install/stringnames.txt file.
+ *
+ * @package installer
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
diff --git a/public/install/lang/fr/install.php b/public/install/lang/fr/install.php
index 18c6aca7307fb..f544aa94134b6 100644
--- a/public/install/lang/fr/install.php
+++ b/public/install/lang/fr/install.php
@@ -68,6 +68,8 @@
$string['pathswrongadmindir'] = 'Le dossier d’administration n’existe pas';
$string['phpextension'] = 'Extension PHP {$a}';
$string['phpversion'] = 'Version de PHP';
+$string['webserverconfigproblemdescription'] = 'Votre serveur web n’est pas configuré pour accéder aux fichiers en dehors du dossier /public. Pour des informations sur la façon de configurer votre serveur web correctement, veuillez vous référer à la documentation (en anglais). Une fois celui-ci configuré, visitez de nouveau la racine du site web.';
+$string['webservernotconfigured'] = 'Serveur web non configuré';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Vous voyez cette page, car vous avez installé Moodle correctement et lancé le logiciel {$a->packname} {$a->packversion} sur votre ordinateur. Félicitations !';
$string['welcomep30'] = 'Cette version de {$a->installername} comprend des logiciels qui créent un environnement dans lequel Moodle va fonctionner, à savoir :';
diff --git a/public/install/lang/gd/admin.php b/public/install/lang/gd/admin.php
index 124a40695dc5d..441dc07546859 100644
--- a/public/install/lang/gd/admin.php
+++ b/public/install/lang/gd/admin.php
@@ -35,3 +35,4 @@
{$a}
Cleachd— an roghainn airson cuideachadh';
$string['environmentrequireversion'] = 'tha tionndadh {$a->needed} deatamach agus tha thusa a’ ruith {$a->current}';
+$string['upgradekeyset'] = 'Iuchair àrdachaidh (fàg bàn gus nach tèid a shuidheachadh)';
diff --git a/public/install/lang/gd/error.php b/public/install/lang/gd/error.php
new file mode 100644
index 0000000000000..ad545637d35a5
--- /dev/null
+++ b/public/install/lang/gd/error.php
@@ -0,0 +1,51 @@
+.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link https://moodledev.io/general/projects/api/amos}) using the
+ * list of strings defined in public/install/stringnames.txt file.
+ *
+ * @package installer
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['cannotcreatedboninstall'] = 'Chan urrainn an stòr-dàta a chruthachadh.
+Chan eil an stòr-dàta a chaidh a shònrachadh ann am bith agus chan eil cead aig an neach-cleachdaidh tugta an stòr-dàta a chruthachadh.
+Bu chòir do rianaire na làraich rèiteachadh an stòr-dàta a dhearbhadh. ';
+$string['cannotcreatelangdir'] = 'Chan urrainn iùl-lann cànain a chruthachadh';
+$string['cannotcreatetempdir'] = 'Chan urrainn iùl-lann shealach a chruthachadh';
+$string['cannotdownloadcomponents'] = 'Chan urrainn co-phàirtean a luchdachadh a-nuas';
+$string['cannotdownloadzipfile'] = 'Chan urrainn faidhle ZIP a luchdachadh a-nuas';
+$string['cannotfindcomponent'] = 'Chan urrainn a’ cho-phàirt a lorg';
+$string['cannotsavemd5file'] = 'Chan urrainn faidhle md5 a shàbhaladh';
+$string['cannotsavezipfile'] = 'Chan urrainn faidhle ZIP a shàbhaladh';
+$string['cannotunzipfile'] = 'Chan urrainn am faidhle siopach fhosgladh';
+$string['componentisuptodate'] = 'Tha a’ cho-phàirt ris an latha';
+$string['dmlexceptiononinstall'] = 'Tha mearachd stòir-dàta air tachairt \\[{$a->errorcode}]. {$a->debuginfo} ';
+$string['downloadedfilecheckfailed'] = 'Dh’fhàillig sgrùdadh an fhaidhle a chaidh a luchdachadh a-nuas';
+$string['missingrequiredfield'] = 'Tha raon deatamach air choireigin a dhìth';
+$string['remotedownloaderror'] = 'Dh’fhàillig luchdachadh na co-phàirte a-nuas chun fhrithealaiche agad. Dearbh na suidheachaidhean procsaidh: thathas a’ moladh an leudachain PHP cURL gu mòr.
+Feumaidh tu am faidhle {$a->url} a luchdachadh a-nuas le làimh, lethbhreac dheth a chur ann an "{$a->dest}" san fhrithealaiche agad agus an siopadh fhosgladh an sin. ';
+$string['wrongdestpath'] = 'Slighe ceann-uidhe ceàrr';
+$string['wrongsourcebase'] = 'Bun-thùs URL ceàrr';
+$string['wrongzipfilename'] = 'Ainm ceàrr don fhaidhle ZIP';
diff --git a/public/install/lang/he/error.php b/public/install/lang/he/error.php
index f4bb04b906f41..76371f6ce2e75 100644
--- a/public/install/lang/he/error.php
+++ b/public/install/lang/he/error.php
@@ -29,6 +29,9 @@
defined('MOODLE_INTERNAL') || die();
+$string['cannotcreatedboninstall'] = 'לא ניתן ליצור את מסד הנתונים.
+מסד הנתונים שצוין אינו קיים, ולמשתמש שצוין אין הרשאה ליצור את מסד הנתונים.
+מנהל האתר צריך לבדוק את הגדרות מסד הנתונים. ';
$string['cannotcreatelangdir'] = 'לא ניתן ליצור סיפריית שפה.';
$string['cannotcreatetempdir'] = 'לא ניתן ליצור סיפרייה זמנית.';
$string['cannotdownloadcomponents'] = 'לא ניתן להוריד רכיבים.';
@@ -38,6 +41,7 @@
$string['cannotsavezipfile'] = 'לא ניתן לשמור קובץ ZIP';
$string['cannotunzipfile'] = 'לא ניתן לפתוח את קובץ ה-ZIP.';
$string['componentisuptodate'] = 'הרכיב מעודכן.';
+$string['dmlexceptiononinstall'] = 'אירעה שגיאת מסד נתונים [{$a->errorcode}]. {$a->debuginfo} ';
$string['downloadedfilecheckfailed'] = 'הקובץ אשר ירד נמצא שגוי';
$string['invalidmd5'] = 'md5 לא חוקי';
$string['missingrequiredfield'] = 'חסר שדה נדרש כלשהו';
diff --git a/public/install/lang/hi/admin.php b/public/install/lang/hi/admin.php
index fdd865a83f277..05dc6fa96657b 100644
--- a/public/install/lang/hi/admin.php
+++ b/public/install/lang/hi/admin.php
@@ -39,3 +39,6 @@
{$a}
कृपया --help विकल्प का उपयोग करें।';
$string['cliyesnoprompt'] = 'टाइप Y (का मतलब है हाँ) या N (का मतलब है नहीं )';
+$string['environmentrequireinstall'] = 'स्थापित और सक्षम होना चाहिए';
+$string['environmentrequireversion'] = 'version {$a->needed} की आवश्यकता है और आप {$a-> current} चला रहे हैं';
+$string['upgradekeyset'] = 'उन्नयन कुंजी (इसे सेट न करने के लिए खाली छोड़ दें)';
diff --git a/public/install/lang/hi/error.php b/public/install/lang/hi/error.php
new file mode 100644
index 0000000000000..c391867b9a4c9
--- /dev/null
+++ b/public/install/lang/hi/error.php
@@ -0,0 +1,50 @@
+.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link https://moodledev.io/general/projects/api/amos}) using the
+ * list of strings defined in public/install/stringnames.txt file.
+ *
+ * @package installer
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['cannotcreatedboninstall'] = ' डेटाबेस नहीं बना सकता है। निर्दिष्ट डेटाबेस मौजूद नहीं है और दिए गए उपयोगकर्ता के पास डेटाबेस बनाने की अनुमति नहीं है। साइट प्रशासक को डेटाबेस विन्यास को सत्यापित करना चाहिए। ';
+$string['cannotcreatelangdir'] = 'लैंग निर्देशिका नहीं बना सकता है';
+$string['cannotcreatetempdir'] = 'अस्थायी निर्देशिका नहीं बना सकता है';
+$string['cannotdownloadcomponents'] = 'घटकों को डाउनलोड नहीं कर सकते';
+$string['cannotdownloadzipfile'] = 'ज़िप दाखिल करना डाउनलोड नहीं कर सकते';
+$string['cannotfindcomponent'] = 'अवयव नहीं मिल रहा है';
+$string['cannotsavemd5file'] = 'md5 दाखिल करना को सहेज नहीं सकते';
+$string['cannotsavezipfile'] = 'ज़िप दाखिल करना को सहेज नहीं सकते';
+$string['cannotunzipfile'] = 'दाखिल करना को अनज़िप नहीं कर सकते';
+$string['componentisuptodate'] = 'अवयव अद्यतित है';
+$string['dmlexceptiononinstall'] = 'डेटाबेस में त्रुटि आ गई है [{$a->errorcode}]। {$a->debuginfo} ';
+$string['downloadedfilecheckfailed'] = 'डाउनलोड की गई दाखिल करना जाँच विफल रही';
+$string['invalidmd5'] = 'चेक चर गलत था-फिर से कोशिश करें';
+$string['missingrequiredfield'] = 'कुछ अपेक्षित क्षेत्र गायब है';
+$string['remotedownloaderror'] = 'आपके सर्वर पर कंपोनेंट डाउनलोड करने में विफलता मिली। कृपया प्रॉक्सी सेटिंग्स की जाँच करें; PHP cURL एक्सटेंशन का उपयोग करने की अत्यधिक अनुशंसा की जाती है।
+आपको {$a->url} फ़ाइल को मैन्युअल रूप से डाउनलोड करना होगा, इसे अपने सर्वर के "{$a->dest}" फ़ोल्डर में कॉपी करना होगा और वहाँ इसे अनज़िप करना होगा। ';
+$string['wrongdestpath'] = 'गलत लक्ष्य मार्ग';
+$string['wrongsourcebase'] = 'गलत उद्गम URL मूल';
+$string['wrongzipfilename'] = 'गलत ज़िप दाखिल करना नाम';
diff --git a/public/install/lang/hi/install.php b/public/install/lang/hi/install.php
new file mode 100644
index 0000000000000..d4da99ce937f6
--- /dev/null
+++ b/public/install/lang/hi/install.php
@@ -0,0 +1,74 @@
+.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link https://moodledev.io/general/projects/api/amos}) using the
+ * list of strings defined in public/install/stringnames.txt file.
+ *
+ * @package installer
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['admindirname'] = 'प्रशासक निर्देशिका';
+$string['availablelangs'] = 'उपलब्ध भाषा पैक';
+$string['chooselanguagehead'] = 'एक भाषा चुनें';
+$string['chooselanguagesub'] = 'कृपया अधिष्ठापन के लिए एक भाषा चुनें। इस भाषा का उपयोग साइट के लिए चूकना भाषा के रूप में भी किया जाएगा, हालांकि इसे बाद में बदला जा सकता है।';
+$string['clialreadyconfigured'] = 'कॉन्फ़िगरेशन फ़ाइल config.php पहले से मौजूद है। कृपया इस साइट के लिए Moodle इंस्टॉल करने के लिए admin/cli/install_database.php का उपयोग करें।';
+$string['clialreadyinstalled'] = 'कॉन्फ़िगरेशन फ़ाइल config.php पहले से मौजूद है। कृपया इस साइट के लिए Moodle इंस्टॉल करने के लिए admin/cli/install_database.php का उपयोग करें।';
+$string['cliinstallheader'] = 'मूडल {$a} कमांड लाइन अधिष्ठापन कार्यक्रम';
+$string['clitablesexist'] = 'डेटाबेस तालिकाएँ पहले से मौजूद हैं; CLI अधिष्ठापन जारी नहीं रह सकती है।';
+$string['databasehost'] = 'डेटाबेस मेजबान';
+$string['databasename'] = 'डेटाबेस का नाम';
+$string['databasetypehead'] = 'डेटाबेस चालक चुनें';
+$string['dataroot'] = 'डेटा निर्देशिका';
+$string['datarootpermission'] = 'डेटा निर्देशिकाओं की अनुमति';
+$string['dbprefix'] = 'तालिका उपसर्ग';
+$string['dirroot'] = 'मूडल निर्देशिका';
+$string['environmenthead'] = 'अपने पर्यावरण की जाँच करें।....';
+$string['environmentsub2'] = 'प्रत्येक मूडल रिलीज में कुछ न्यूनतम, कम से कम PHP संस्करण अपेक्षा, आवश्यकता और कई अनिवार्य पी. एच. पी. विस्तार होते हैं। प्रत्येक स्थापना और उन्नयन करना से पहले पूर्ण पर्अथवावरण जांच की जाती है। यदि आप नहीं जानकारी कि नअथवा संस्करण कैसे स्थापित करना है अथवा PHP विस्तार को सक्षम करना है तो कृपअथवा सर्वर प्रशासक से संपर्क करें।';
+$string['errorsinenvironment'] = 'पर्यावरण जांच विफल रही!';
+$string['installation'] = 'अधिष्ठापन';
+$string['langdownloaderror'] = 'दुर्भाग्य से "{$a}" भाषा डाउनलोड नहीं की जा सकी। अधिष्ठापन प्रक्रम अंग्रेजी में जारी रहेगी।';
+$string['paths'] = 'रास्ते';
+$string['pathserrcreatedataroot'] = 'डेटा निर्देशिका ({$a-> dataroot}) इंस्टॉलर द्वारा नहीं बनाई जा सकती है।';
+$string['pathshead'] = 'रास्तों की पुष्टि';
+$string['pathsrodataroot'] = 'डेटारूट निर्देशिका लिखित नहीं है।';
+$string['pathsroparentdataroot'] = 'मूल निर्देशिका ({$a-> parent}) लिखने योग्य नहीं है। डेटा निर्देशिका ({$a-> dataroot}) इंस्टॉलर द्वारा नहीं बनाई जा सकती है।';
+$string['pathssubadmindir'] = 'बहुत कम वेबहोस्ट नियंत्रण कक्ष अथवा किसी चीज़ तक पहुँचने के लिए आपके लिए एक विशेष URL के रूप में प्रशासक का उपयोग करते हैं। दुर्भाग्य से यह मूडल प्रशासक पृष्ठों के लिए स्तर स्थान के साथ संघर्ष करता है। आप अपने इंस्टॉलेशन में प्रशासक निर्देशिका का नाम बदलकर और उस नए नाम को यहाँ रखकर इसे निश्चित करना सकते हैं। उदाहरण के लिए: moodleadmin यह मूडल में प्रशासक लिंक को निश्चित करनाेगा।';
+$string['pathssubdataroot'] = ' एक निर्देशिका जहाँ मूडल उपयोगकर्ताओं द्वारा अपलोड की गई सभी दाखिल करना सामग्री को संग्रहीत करेगा। यह निर्देशिका वेब सर्वर उपयोगकर्ता (आमतौर पर \'www-data\', \'कोई नहीं\' अथवा \'अपाचे\') द्वारा पढ़ने योग्य और लिखने योग्य दोनों होनी चाहिए। यह सीधे वेब पर सुलभ नहीं होना चाहिए। यदि निर्देशिका वर्तमान में मौजूद नहीं है, तो अधिष्ठापन प्रक्रम इसे बनाने का प्रअथवास करेगी। ';
+$string['pathssubdirroot'] = ' मॉड्यूल कोड वाली निर्देशिका का पूरा मार्ग। ';
+$string['pathssubwwwroot'] = ' पूरा सम्बोधन जहाँ मूडल तक पहुँचा जाएगा i.e। वह सम्बोधन जिसे उपयोगकर्ता मूडल तक पहुँचने के लिए अपने ब्राउज़र के पते की पट्टी में दर्ज करेंगे। कई पतों का उपयोग करके मूडल तक पहुँचना संभव नहीं है। यदि आपकी साइट कई पतों के माध्यम से सुलभ है तो सबसे आसान का चयन करें और अन्य पतों में से प्रत्येक के लिए एक स्थायी पुनर्निर्देश स्थापित करें। यदि आपकी साइट इंटरनेट और आंतरिक नेटवर्क (जिसे कभी-कभी इंट्रानेट कहा जाता है) दोनों से सुलभ है, तो यहाँ सार्वजनिक पते का उपयोग करें। यदि चालू, प्रचलित सम्बोधन सही नहीं है, तो कृपया अपने ब्राउज़र के पते की पट्टी में यूआरएल बदलें और स्थापना को फिर से शुरू करें। ';
+$string['pathsunsecuredataroot'] = 'डेटारूट स्थान सुरक्षित नहीं है';
+$string['pathswrongadmindir'] = 'प्रशासक निर्देशिका मौजूद नहीं है';
+$string['phpextension'] = '{$a} PHP विस्तार';
+$string['phpversion'] = 'PHP संस्करण';
+$string['welcomep20'] = 'आप इस पृष्ठ को इसलिए देख रहे हैं क्योंकि आपने अपने कंप्यूटर में {$a->packname} {$a->packversion} पैकेज को सफलतापूर्वक स्थापित और लॉन्च कर लिया है।
+बधाई हो!';
+$string['welcomep30'] = 'इस {$a->installername} रिलीज़ में निम्नलिखित एप्लिकेशन शामिल हैं:
+एक ऐसा वातावरण बनाने के लिए जिसमें Moodle काम करेगा, अर्थात्:';
+$string['welcomep40'] = 'इस पैकेज में Moodle {$a->moodlerelease} ({$a->moodleversion}) भी शामिल है।';
+$string['welcomep50'] = 'इस पैकेज में शामिल सभी अनुप्रयोगों का उपयोग उनके संबंधित लाइसेंसों द्वारा नियंत्रित होता है। संपूर्ण {$a->installername} पैकेज ओपन सोर्स है और इसे GPL लाइसेंस के अंतर्गत वितरित किया जाता है।';
+$string['welcomep60'] = 'निम्नलिखित पृष्ठों में दिए गए आसान चरणों का पालन करके आप अपने कंप्यूटर पर मूडल को कॉन्फ़िगर और सेट अप कर सकते हैं।
+आप डिफ़ॉल्ट सेटिंग्स को स्वीकार कर सकते हैं या चाहें तो उन्हें अपनी आवश्यकताओं के अनुसार बदल सकते हैं।';
+$string['welcomep70'] = 'मूडल के सेटअप के साथ जारी रखने के लिए नीचे दिए गए "अगले" बटन पर क्लिक करें।';
+$string['wwwroot'] = 'वेब सम्बोधन';
diff --git a/public/install/lang/id/install.php b/public/install/lang/id/install.php
index 0b26c296f425e..42bf3fc59ffb7 100644
--- a/public/install/lang/id/install.php
+++ b/public/install/lang/id/install.php
@@ -62,6 +62,8 @@
$string['pathswrongadmindir'] = 'Direktori admin tidak ada';
$string['phpextension'] = 'Ekstensi PHP {$a}';
$string['phpversion'] = 'Versi PHP';
+$string['webserverconfigproblemdescription'] = 'Server web Anda tidak dikonfigurasi untuk mencegah akses ke berkas di luar direktori /public. Untuk rincian tentang cara mengkonfigurasi server web Anda dengan benar, lihat dokumentasi Pembaruan - Struktur ulang direktori kode. Setelah dikonfigurasi ulang, kunjungi kembali root web.';
+$string['webservernotconfigured'] = 'Server web belum dikonfigurasi';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Anda melihat halaman ini karena Anda telah berhasil memasang dan meluncurkan paket {$a->packname} {$a->packversion} di komputer Anda. Selamat!';
$string['welcomep30'] = 'Rilis {$a->installername} ini mencakup aplikasi untuk menciptakan lingkungan tempat Moodle yang akan digunakan, yaitu:';
diff --git a/public/install/lang/it/install.php b/public/install/lang/it/install.php
index 17578fe31cc76..deaaeb2375ff9 100644
--- a/public/install/lang/it/install.php
+++ b/public/install/lang/it/install.php
@@ -71,6 +71,8 @@
$string['pathswrongadmindir'] = 'La cartella Admin non esiste';
$string['phpextension'] = '{$a} estensioni PHP';
$string['phpversion'] = 'Versione PHP';
+$string['webserverconfigproblemdescription'] = 'Il server web non è configurato per impedire l\'accesso ai file esterni alla cartella /public. Consultare https://moodledev.io/docs/5.1/guides/restructure per i dettagli su come configurare il tuo server web. Una volta riconfigurato, visita nuovamente la cartella radice del sito web.';
+$string['webservernotconfigured'] = 'Web server non configurato';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Se vedi questa pagina hai installato correttamente e lanciato il pacchetto {$a->packname} {$a->packversion}. Complimenti!';
$string['welcomep30'] = 'La release di {$a->installername} include l\'applicazione per creare l\'ambiente necessario a far girare Moodle:';
diff --git a/public/install/lang/ja/install.php b/public/install/lang/ja/install.php
index 133e14a3d682a..b66eee649767d 100644
--- a/public/install/lang/ja/install.php
+++ b/public/install/lang/ja/install.php
@@ -68,6 +68,8 @@
$string['pathswrongadmindir'] = 'adminディレクトリが存在しません。';
$string['phpextension'] = '{$a} PHP拡張モジュール';
$string['phpversion'] = 'PHPバージョン';
+$string['webserverconfigproblemdescription'] = 'あなたのウェブサーバは「/public」ディレクトリ外のファイルへのアクセスを防止するよう設定されていません。あなたのウェブサーバを正しく設定する方法の詳細に関してMoodleをアップグレードする - コードディレクトリの再構成ドキュメンテーションをご覧ください。再設定後、ウェブルートに再度アクセスしてください。';
+$string['webservernotconfigured'] = 'ウェブサーバが設定されていません。';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'インストール正常完了後、あなたのコンピュータで{$a->packname} {$a->packversion}パッケージが起動されたため、このページが表示されています。おめでとうございます!';
$string['welcomep30'] = 'このリリース{$a->installername}にはMoodleで環境を作成するアプリケーションが含まれています。すなわち:';
diff --git a/public/install/lang/no/install.php b/public/install/lang/no/install.php
index d628a1aec62b6..2d5d841521e3d 100644
--- a/public/install/lang/no/install.php
+++ b/public/install/lang/no/install.php
@@ -67,6 +67,8 @@
$string['pathswrongadmindir'] = 'Adminkatalog finnes ikke';
$string['phpextension'] = '{$a} PHP-tillegg';
$string['phpversion'] = 'PHP versjon';
+$string['webserverconfigproblemdescription'] = 'Nettserveren din er ikke konfigurert til å forhindre tilgang til filer utenfor /public-katalogen. Se https://moodledev.io/docs/5.1/guides/restructure for detaljer om hvordan du konfigurerer nettserveren din. Når den er konfigurert på nytt, gå tilbake til web root.';
+$string['webservernotconfigured'] = 'Webserver ikke konfigurert';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Du ser denne siden fordi du nå har fullført installeringen og kjøringen av pakken {$a->packname} {$a->packversion} på datamaskinen din. Gratulerer!';
$string['welcomep30'] = 'Denne versjonen av {$a->installername} inneholder programmer for å lage et miljø som Moodle jobber i, nemlig:';
diff --git a/public/install/lang/oc_prv/langconfig.php b/public/install/lang/oc_prv/langconfig.php
new file mode 100644
index 0000000000000..089bc7d5d176e
--- /dev/null
+++ b/public/install/lang/oc_prv/langconfig.php
@@ -0,0 +1,32 @@
+.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link https://moodledev.io/general/projects/api/amos}) using the
+ * list of strings defined in public/install/stringnames.txt file.
+ *
+ * @package installer
+ * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['thislanguage'] = 'Provençal Occitan';
diff --git a/public/install/lang/pt/install.php b/public/install/lang/pt/install.php
index ebfb04ba15a3b..48e91e4c9d9d2 100644
--- a/public/install/lang/pt/install.php
+++ b/public/install/lang/pt/install.php
@@ -65,6 +65,8 @@
$string['pathswrongadmindir'] = 'A pasta admin não existe';
$string['phpextension'] = 'Extensão {$a} do PHP';
$string['phpversion'] = 'Versão do PHP';
+$string['webserverconfigproblemdescription'] = 'O seu servidor web não está configurado para impedir o acesso a ficheiros fora do diretório /public. Para mais pdetalhes sobre como configurar corretamente o seu servidor web, consulte a página Atualização - Reestruturação dos diretórios de código. Após a reconfiguração, aceda novamente à raiz da web.';
+$string['webservernotconfigured'] = 'Servidor web não configurado';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'A apresentação desta página confirma a correta instalação e ativação do pacote {$a->packname} {$a->packversion} no servidor.';
$string['welcomep30'] = 'Esta versão do pacote {$a->installername} inclui as aplicações necessárias para o correto funcionamento do Moodle, nomeadamente:';
diff --git a/public/install/lang/pt_br/install.php b/public/install/lang/pt_br/install.php
index fb1e37a2179de..c401011444d0a 100644
--- a/public/install/lang/pt_br/install.php
+++ b/public/install/lang/pt_br/install.php
@@ -69,6 +69,8 @@
$string['pathswrongadmindir'] = 'Diretório Admin não existe';
$string['phpextension'] = 'Extensão PHP {$a}';
$string['phpversion'] = 'Versão do PHP';
+$string['webserverconfigproblemdescription'] = 'Seu servidor web não está configurado para impedir o acesso a arquivos fora do diretório /public. Para detalhes de como configurar corretamente seu servidor web, consulte a documentação Atualização - reestruturação dos diretórios de código. Após reconfigurar, revisite a raiz da web.';
+$string['webservernotconfigured'] = 'Servidor web não configurado';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Você está vendo esa página pois instalou com sucesso o pacote{$a->packname} {$a->packversion}. Parabéns!';
$string['welcomep30'] = 'Esta versão do {$a->installername} inclui as aplicações para a criação de um ambiente em que o Moodle possa operar:';
diff --git a/public/install/lang/ru/install.php b/public/install/lang/ru/install.php
index e0fcca19948e4..a533d848b428f 100644
--- a/public/install/lang/ru/install.php
+++ b/public/install/lang/ru/install.php
@@ -67,6 +67,8 @@
$string['pathswrongadmindir'] = 'Каталог admin не существует';
$string['phpextension'] = 'Расширение PHP «{$a}»';
$string['phpversion'] = 'Версия PHP';
+$string['webserverconfigproblemdescription'] = 'Ваш веб-сервер не настроен на предотвращение доступа к файлам вне каталога /public. Подробнее о настройке веб-сервера см. на странице Обновление - Код реструктуризации каталогов. После повторной настройки снова перейдите в корневой каталог веб-сервера.';
+$string['webservernotconfigured'] = 'Веб-сервер не настроен';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Вы видите эту страницу, потому что успешно установили и запустили на своем компьютере набор программ {$a->packname} {$a->packversion}. Поздравляем!';
$string['welcomep30'] = 'Эта версия набора программ {$a->installername} включает следующие программы, необходимые для создания среды, в которой будет работать Moodle:';
diff --git a/public/install/lang/sr_cr/install.php b/public/install/lang/sr_cr/install.php
index 7dd5f35b19620..0ff6cbcac3456 100644
--- a/public/install/lang/sr_cr/install.php
+++ b/public/install/lang/sr_cr/install.php
@@ -69,6 +69,8 @@
$string['pathswrongadmindir'] = 'Админ директоријум не постоји';
$string['phpextension'] = '{$a} PHP екстензија';
$string['phpversion'] = 'PHP верзија';
+$string['webserverconfigproblemdescription'] = 'Ваш веб сервер није конфигурисан да спречи приступ датотекама ван директоријума /public. За детаљније информације о томе како да конфигуришете свој веб сервер погледајте документацију Надоградња - Реструктурирање директоријума. Једном када промените конфигурацију веб сервера поново погледајте његов коренски директоријум.';
+$string['webservernotconfigured'] = 'Веб сервер није конфигурисан';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Ову страницу видите зато што сте успешно инсталирали и покренули {$a->packname} {$a->packversion} пакет на свом серверу. Честитамо!';
$string['welcomep30'] = 'Ово издање {$a->installername} укључује апликације за креирање окружења у којем ће Moodle успешно функционисати, конкретно:';
diff --git a/public/install/lang/sr_lt/install.php b/public/install/lang/sr_lt/install.php
index 40432960d5757..e2c207c8b7511 100644
--- a/public/install/lang/sr_lt/install.php
+++ b/public/install/lang/sr_lt/install.php
@@ -66,6 +66,8 @@
$string['pathswrongadmindir'] = 'Admin direktorijum ne postoji';
$string['phpextension'] = '{$a} PHP ekstenѕija';
$string['phpversion'] = 'PHP verzija';
+$string['webserverconfigproblemdescription'] = 'Vaš veb server nije konfigurisan da spreči pristup datotekama van direktorijuma /public. Za detaljnije informacije o tome kako da konfigurišete svoj veb server pogledajte dokumentaciju Nadogradnja - Restrukturiranje direktorijuma. Jednom kada promenite konfiguraciju veb servera ponovo pogledajte njegov korenski direktorijum.';
+$string['webservernotconfigured'] = 'Veb server nije konfigurisan';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Ovu stranicu vidite zato što ste uspešno instalirali i pokrenuli {$a->packname} {$a->packversion} paket na svom serveru. Čestitamo!';
$string['welcomep30'] = 'Ovo izdanje {$a->installername} uključuje aplikacije za kreiranje okruženja u kojem će Moodle uspešno funkcionisati, konkretno:';
diff --git a/public/install/lang/tr/install.php b/public/install/lang/tr/install.php
index bb13397b29458..f2ed46669dff6 100644
--- a/public/install/lang/tr/install.php
+++ b/public/install/lang/tr/install.php
@@ -69,6 +69,8 @@
$string['pathswrongadmindir'] = 'Yönetici klasörü yok';
$string['phpextension'] = '{$a} PHP eklentisi';
$string['phpversion'] = 'PHP sürümü';
+$string['webserverconfigproblemdescription'] = 'Web sunucunuz, /public dizini dışındaki dosyalara erişimi engelleyecek şekilde yapılandırılmamıştır. Web sunucunuzu nasıl yapılandıracağınızla ilgili ayrıntılar için lütfen https://moodledev.io/docs/5.1/guides/restructure adresine bakın. Yeniden yapılandırdıktan sonra lütfen web kökünü yeniden ziyaret edin.';
+$string['webservernotconfigured'] = 'Web sunucusu yapılandırılmamış';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'Bu sayfayı {$a->packname} {$a->packversion} paketini ilgisayarınıza başarıyla kurduğunuz için görüyorsunuz. Tebrikler!';
$string['welcomep30'] = ' {$a-> installername} \'in bu sürümü, Moodle \'ın çalışacağı bir ortam oluşturmak için uygulamaları içerir:';
diff --git a/public/install/lang/uz/error.php b/public/install/lang/uz/error.php
index c9844a9894c87..0ccd1ca993e28 100644
--- a/public/install/lang/uz/error.php
+++ b/public/install/lang/uz/error.php
@@ -37,3 +37,16 @@
$string['cannotdownloadcomponents'] = 'Komponentlarni yuklab olib bo\'lmadi';
$string['cannotdownloadzipfile'] = 'ZIP faylini yuklab olib bo\'lmadi';
$string['cannotfindcomponent'] = 'Komponentni topib bo\'lmadi';
+$string['cannotsavemd5file'] = 'md5 faylini saqlab bo‘lmadi';
+$string['cannotsavezipfile'] = 'ZIP faylini saqlab bo‘lmadi';
+$string['cannotunzipfile'] = 'Faylni arxivdan chiqarib bo‘lmadi';
+$string['componentisuptodate'] = 'Komponent yangilangan holda';
+$string['dmlexceptiononinstall'] = 'Ma’lumotlar bazasida xatolik yuz berdi [{$a->errorcode}]. {$a->debuginfo} ';
+$string['downloadedfilecheckfailed'] = 'Yuklab olingan faylni tekshirish muvaffaqiyatsiz tugadi';
+$string['invalidmd5'] = 'Tekshiruv o‘zgaruvchisi noto‘g‘ri — qayta urinib ko‘ring';
+$string['missingrequiredfield'] = 'Ba’zi talab qilingan maydonlar mavjud emas';
+$string['remotedownloaderror'] = 'Komponentni serveringizga yuklab olish muvaffaqiyatsiz bo‘ldi. Iltimos, proksi sozlamalarini tekshiring; PHP cURL kengaytmasi tavsiya etiladi.
+Siz {$a->url} faylini qo‘lda yuklab olishingiz, serveringizdagi "{$a->dest}" ga nusxalashingiz va u yerda arxivdan chiqarishingiz kerak. ';
+$string['wrongdestpath'] = 'Noto‘g‘ri manzil yo‘li.';
+$string['wrongsourcebase'] = 'Noto‘g‘ri manba URL bazasi.';
+$string['wrongzipfilename'] = 'Noto‘g‘ri ZIP fayl nomi.';
diff --git a/public/install/lang/uz/install.php b/public/install/lang/uz/install.php
index a039cb388bf82..25bcbc870f9d8 100644
--- a/public/install/lang/uz/install.php
+++ b/public/install/lang/uz/install.php
@@ -31,7 +31,51 @@
$string['admindirname'] = 'Admin katalogi';
$string['availablelangs'] = 'Mavjud til paketlari';
+$string['chooselanguagehead'] = 'Tilni tanlash';
+$string['chooselanguagesub'] = 'Iltimos, o‘rnatish uchun tilni tanlang. Ushbu til saytning standart tili sifatida ham ishlatiladi, ammo keyinchalik o‘zgartirish mumkin.';
+$string['clialreadyconfigured'] = 'config.php konfiguratsiya fayli allaqachon mavjud. Iltimos, ushbu sayt uchun Moodle’ni o‘rnatish uchun admin/cli/install_database.php faylidan foydalaning.';
+$string['clialreadyinstalled'] = 'config.php konfiguratsiya fayli allaqachon mavjud. Iltimos, ushbu sayt uchun Moodle’ni yangilash uchun admin/cli/install_database.php faylidan foydalaning.';
+$string['cliinstallheader'] = 'Moodle {$a} buyruq qatori o‘rnatish dasturi';
+$string['clitablesexist'] = 'Ma’lumotlar bazasi jadvallari allaqachon mavjud; CLI orqali o‘rnatish davom ettirib bo‘lmaydi.';
+$string['databasehost'] = 'Ma’lumotlar bazasi mezboni';
+$string['databasename'] = 'Ma’lumotlar bazasi nomi';
+$string['databasetypehead'] = 'Ma’lumotlar bazasi drayverini tanlang';
$string['dataroot'] = 'Ma’lumotlar katalogi';
+$string['datarootpermission'] = 'Data kataloglari uchun ruxsatlar';
$string['dbprefix'] = 'Jadvallar prefiksi';
$string['dirroot'] = 'Moodle katalogi';
+$string['environmenthead'] = 'Muhitingiz tekshirilmoqda ...';
+$string['environmentsub2'] = 'Har bir Moodle versiyasi minimal PHP versiyasi talabi va majburiy PHP kengaytmalariga ega.
+To‘liq muhit tekshiruvi har bir o‘rnatish yoki yangilashdan oldin amalga oshiriladi. Agar yangi versiyani o‘rnatish yoki PHP kengaytmalarini yoqishni bilmasangiz, server administratoriga murojaat qiling.';
+$string['errorsinenvironment'] = 'Muhitni tekshirish muvaffaqiyatsiz tugadi!';
$string['installation'] = 'O\'rnatish';
+$string['langdownloaderror'] = 'Afsuski, "{$a}" tili yuklab bo‘lmadi. O‘rnatish jarayoni ingliz tilida davom ettiriladi.';
+$string['paths'] = 'O‘tkazib yuborishlar';
+$string['pathserrcreatedataroot'] = 'Ma’lumotlar papkasi ({$a->dataroot}) o‘rnatish dasturi tomonidan yaratilolmayapti.';
+$string['pathshead'] = 'O’tkazib yuborishlarni tasdiqlash';
+$string['pathsrodataroot'] = 'Dataroot papkasiga yozish mumkin emas.';
+$string['pathsroparentdataroot'] = 'Ota papkaga ({$a->parent}) yozish mumkin emas. Shu sababli, ma’lumotlar papkasi ({$a->dataroot}) o‘rnatish dasturi tomonidan yaratilolmayapti.';
+$string['pathssubadmindir'] = 'Faqat juda kam web-hostlar sizga boshqaruv paneliga kirish uchun /admin URL’ini maxsus tarzda ishlatadi. Afsuski, bu Moodle administrator sahifalarining standart joylashuvi bilan to‘qnashadi. Buni tuzatish uchun o‘rnatishingizdagi admin papkasining nomini o‘zgartiring va yangi nomni bu yerga kiriting. Masalan: moodleadmin. Bu Moodle’dagi admin havolalarini tuzatadi.';
+$string['pathssubdataroot'] = 'Moodle foydalanuvchilar tomonidan yuklangan barcha fayl kontentini saqlaydigan papka.
+Ushbu papka veb-server foydalanuvchisi (odatda \'www-data\', \'nobody\' yoki \'apache\') tomonidan o‘qilishi va yozilishi mumkin bo‘lishi kerak.
+U to‘g‘ridan-to‘g‘ri veb orqali kirish mumkin bo‘lmasligi lozim.
+Agar papka hozir mavjud bo‘lmasa, o‘rnatish jarayoni uni yaratishga harakat qiladi. ';
+$string['pathssubdirroot'] = 'Moodle kodi joylashgan papkaning to‘liq yo‘li. ';
+$string['pathssubwwwroot'] = 'Moodle’ga kirish uchun to‘liq manzil, ya’ni foydalanuvchilar Moodle’ga kirish uchun brauzer manzil satriga kiritadigan manzil.
+Moodle’ga bir nechta manzil orqali kirish mumkin emas. Agar saytingiz bir nechta manzil orqali mavjud bo‘lsa, eng osonini tanlang va boshqa manzillar uchun doimiy yo‘naltirishni sozlang.
+Agar saytingiz Internetdan va ichki tarmoqdan (ba’zan Intranet deb ataladi) mavjud bo‘lsa, bu yerda jamoat manzilini ishlating.
+Agar hozirgi manzil noto‘g‘ri bo‘lsa, brauzeringiz manzil satrida URL’ni o‘zgartiring va o‘rnatishni qayta boshlang. ';
+$string['pathsunsecuredataroot'] = 'Dataroot joylashuvi xavfsiz emas.';
+$string['pathswrongadmindir'] = 'Administrator papkasi mavjud emas.';
+$string['phpextension'] = '{$a} PHP kengaytmasi';
+$string['phpversion'] = 'PHP versiyasi';
+$string['webserverconfigproblemdescription'] = 'Veb-serveringiz /public katalogidan tashqaridagi fayllarga kirishni oldini olish uchun sozlanmagan. Veb-serveringizni qanday qilib to\'g\'ri sozlash haqida batafsil ma\'lumot olish uchun Yangilash - Kod kataloglarini qayta tuzish hujjatlariga qarang. Qayta sozlangandan so\'ng, veb-ildizga qayta tashrif buyuring.';
+$string['webservernotconfigured'] = 'Veb-server sozlanmagan';
+$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
+$string['welcomep20'] = 'Siz bu sahifani ko‘rayotganingizning sababi, siz kompyuteringizda {$a->packname} {$a->packversion} paketini muvaffaqiyatli o‘rnatdingiz va ishga tushirdingiz. Tabriklaymiz!';
+$string['welcomep30'] = '{$a->installername} ning ushbu relizi Moodle ishlaydigan muhitni yaratish uchun ilovalarni o‘z ichiga oladi, ya’ni:';
+$string['welcomep40'] = 'Paket shuningdek Moodle {$a->moodlerelease} ({$a->moodleversion}) ni o‘z ichiga oladi.';
+$string['welcomep50'] = 'Ushbu paketdagi barcha ilovalardan foydalanish ularning tegishli litsenziyalari bilan tartibga solinadi. To‘liq {$a->installername} paketi ochiq kodli hisoblanadi va GPL litsenziyasi ostida tarqatiladi.';
+$string['welcomep60'] = 'Keyingi sahifalar kompyuteringizda Moodleni sozlash va o‘rnatish bo‘yicha oson bajariladigan qadamlar orqali sizni yetaklaydi. Siz sukut bo‘yicha sozlamalarni qabul qilishingiz yoki ixtiyoriy ravishda ularni o‘z ehtiyojlaringizga mos ravishda o‘zgartirishingiz mumkin.';
+$string['welcomep70'] = 'Moodleni o‘rnatishni davom ettirish uchun quyidagi "Keyingi" tugmasini bosing.';
+$string['wwwroot'] = 'Veb-manzil';
diff --git a/public/install/stringnames.txt b/public/install/stringnames.txt
index f540aeba3353d..c46aa50613b25 100644
--- a/public/install/stringnames.txt
+++ b/public/install/stringnames.txt
@@ -65,6 +65,8 @@ remotedownloaderror,error
thisdirection,langconfig
thislanguage,langconfig
upgradekeyset,admin
+webservernotconfigured,install
+webserverconfigproblemdescription,install
welcomep10,install
welcomep20,install
welcomep30,install
diff --git a/public/lang/en/access.php b/public/lang/en/access.php
index 15bda41563ae0..465fc0997424b 100644
--- a/public/lang/en/access.php
+++ b/public/lang/en/access.php
@@ -28,7 +28,7 @@
$string['accessstatement'] = 'Accessibility statement';
$string['activitynext'] = 'Next activity';
$string['activityprev'] = 'Previous activity';
-$string['breadcrumb'] = 'Navigation bar';
+$string['breadcrumb'] = 'Breadcrumb';
$string['eventcontextlocked'] = 'Context frozen';
$string['eventcontextunlocked'] = 'Context unfrozen';
$string['hideblocka'] = 'Hide {$a} block';
diff --git a/public/lang/en/admin.php b/public/lang/en/admin.php
index 88b7969d33e84..9f0da38e4fa1e 100644
--- a/public/lang/en/admin.php
+++ b/public/lang/en/admin.php
@@ -419,7 +419,7 @@
$string['configverifychangedemail'] = 'Enables verification of changed email addresses using allowed and denied email domains settings. If this setting is disabled the domains are enforced only when creating new users.';
$string['configvisiblecourses'] = 'Display courses in hidden categories normally';
$string['configwarning'] = 'Be careful modifying these settings - strange values could cause problems.';
-$string['configyuicomboloading'] = 'This options enables combined file loading optimisation for YUI libraries. This setting should be enabled on production sites for performance reasons.';
+$string['configyuicomboloading'] = 'This option enables combined file loading optimisation for YUI libraries. It should be enabled for performance reasons.';
$string['confirmation'] = 'Confirmation';
$string['confirmationpending'] = 'Confirmation pending';
$string['confirmcontextlock'] = '{$a->contextname} is currently unfrozen. Freezing it will make it read-only and prevent users from making changes. Are you sure you wish to continue?';
@@ -834,16 +834,12 @@
Your account with username {$a->username} on server \'{$a->sitename}\'
was locked out after multiple invalid login attempts.
-To unlock the account immediately go to the following address
+To unlock the account immediately, please click the link below:
-{$a->link}
+Unlock account
-In most mail programs, this should appear as a blue link
-which you can just click on. If that doesn\'t work,
-then copy and paste the address into the address
-line at the top of your web browser window.
-If you need help, please contact the site administrator,
+If you need help, please contact the site administrator.
{$a->admin}';
$string['lockoutemailsubject'] = 'Your account on {$a} was locked out';
$string['lockouterrorunlock'] = 'Invalid account unlock information supplied.';
@@ -983,6 +979,7 @@
$string['maxtimelimit_desc'] = 'To restrict the maximum PHP execution time that Moodle will allow without any output being displayed, enter a value in seconds here. 0 means that Moodle default restrictions are used. If you have a front-end server with its own time limit, set this value lower to receive PHP errors in logs. Does not apply to CLI scripts.';
$string['moodlebrandedapp'] = 'Branded Moodle app';
$string['moodlebrandedappreference'] = 'Alternatively, get a Branded Moodle app with your own custom branding.';
+$string['moodlenetremovalwarning'] = 'The MoodleNet service will be shut down on 20 April 2026. If you wish to continue using MoodleNet on your site, install the MoodleNet plugin from the Moodle plugins directory and connect it to a self-hosted MoodleNet instance. Following this, the MoodleNet profile ID field will be removed; please migrate that data if you are using it for other purposes.';
$string['noreplyaddress'] = 'No-reply address';
$string['noreplydomain'] = 'No-reply and domain';
$string['noreplydomaindetail'] = 'Settings for No-reply and configured domains';
diff --git a/public/lang/en/ai.php b/public/lang/en/ai.php
index 07e602c16bf3c..5cacb0a0c8618 100644
--- a/public/lang/en/ai.php
+++ b/public/lang/en/ai.php
@@ -123,6 +123,7 @@
$string['globalratelimit_help'] = 'The number of site-wide requests allowed per hour.';
$string['manageaiplacements'] = 'Manage AI placements';
$string['manageaiproviders'] = 'Manage AI providers';
+$string['noproviderplugins'] = 'There are no provider plugins installed. Install a provider plugin to enable provider instance creation.';
$string['noproviders'] = 'This action is unavailable. No AI providers are configured for this action.';
$string['off'] = 'Off';
$string['on'] = 'On';
diff --git a/public/lang/en/auth.php b/public/lang/en/auth.php
index 696e0b891afa8..69553edc8bf04 100644
--- a/public/lang/en/auth.php
+++ b/public/lang/en/auth.php
@@ -81,10 +81,14 @@
$string['emailupdate'] = 'Email address update';
$string['emailupdatemessage'] = 'Hi {$a->firstname},
-You have requested a change of your email address for your account on {$a->site}. To confirm this change, please go to the following web address:
+You have requested a change of your email address for your account on {$a->site}.
-{$a->url}
-The confirmation link will expire in 10 minutes.
+To confirm this change, please click the link below:
+
+Confirm email change
+
+
+The confirmation link will expire in 10 minutes.
{$a->supportemail}';
$string['emailupdatesuccess'] = 'Email address of user {$a->fullname} was successfully updated to {$a->email}.';
diff --git a/public/lang/en/availability.php b/public/lang/en/availability.php
index 0f6c11ea7963a..acd3ee0db830c 100644
--- a/public/lang/en/availability.php
+++ b/public/lang/en/availability.php
@@ -37,11 +37,11 @@
$string['invalid'] = 'Please set';
$string['itemheading'] = '{$a->number} {$a->type} restriction';
$string['item_unknowntype'] = 'These restrictions use a plugin which is no longer available (if it is okay to remove that restriction, delete it below)';
-$string['shown_individual'] = 'Displayed if student doesn\'t meet this condition';
+$string['shown_individual'] = 'Item name displayed with access restriction information if student doesn\'t meet this condition';
$string['hide_verb'] = 'Click to hide';
$string['show_verb'] = 'Click to display';
$string['hidden_all'] = 'Hidden entirely if student doesn\'t meet the conditions';
-$string['shown_all'] = 'Displayed if student doesn\'t meet the conditions';
+$string['shown_all'] = 'Item name displayed with access restriction information if student doesn\'t meet the conditions';
$string['label_multi'] = 'Required restrictions';
$string['label_sign'] = 'Restriction type';
$string['list_and'] = 'All of:';
diff --git a/public/lang/en/backup.php b/public/lang/en/backup.php
index 57d622281eb0e..70f3dfa59a5b1 100644
--- a/public/lang/en/backup.php
+++ b/public/lang/en/backup.php
@@ -35,8 +35,8 @@
$string['asyncemailenable'] = 'Enable notifications';
$string['asyncemailenabledetail'] = 'If enabled, users will receive a notification when an asynchronous backup or restore completes.';
$string['asyncmessagebody'] = 'Notification';
+$string['asyncmessagebodydefault'] = '{operation} (ID: {backupid}) completed. Access it here: {link}.';
$string['asyncmessagebodydetail'] = 'Notification to send when an asynchronous backup or restore completes.';
-$string['asyncmessagebodydefault'] = 'Your {operation} (ID: {backupid}) has completed successfully. You can access it here: {link}.';
$string['asyncmessagesubject'] = 'Subject';
$string['asyncmessagesubjectdetail'] = 'Notification subject';
$string['asyncmessagesubjectdefault'] = 'Moodle {operation} completed successfully';
diff --git a/public/lang/en/badges.php b/public/lang/en/badges.php
index 2e56d574e7c3e..987dfcea9e650 100644
--- a/public/lang/en/badges.php
+++ b/public/lang/en/badges.php
@@ -596,6 +596,7 @@
$string['visible'] = 'Visible';
$string['version'] = 'Version';
$string['warnexpired'] = ' (This badge has expired!)';
+$string['wrongrole'] = 'The role you want to use is not assigned to you.';
$string['year'] = 'Year(s)';
// Deprecated since Moodle 4.5.
diff --git a/public/lang/en/cache.php b/public/lang/en/cache.php
index 0c788c0a7e38b..aa2561887e5f6 100644
--- a/public/lang/en/cache.php
+++ b/public/lang/en/cache.php
@@ -71,7 +71,6 @@
$string['cachedef_h5p_content_type_translations'] = 'H5P content-type libraries translations';
$string['cachedef_h5p_libraries'] = 'H5P libraries';
$string['cachedef_h5p_library_files'] = 'H5P library files';
-$string['cachedef_hookcallbacks'] = 'Hook callbacks';
$string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
$string['cachedef_langmenu'] = 'List of available languages';
$string['cachedef_license'] = 'List of licences';
diff --git a/public/lang/en/course.php b/public/lang/en/course.php
index ef924edf319ad..1e1d315355ff2 100644
--- a/public/lang/en/course.php
+++ b/public/lang/en/course.php
@@ -33,6 +33,10 @@
$string['activitydate:opened'] = 'Opened:';
$string['activitydate:opens'] = 'Opens:';
$string['activitylink'] = 'Permalink';
+$string['activitynavigation'] = 'Activity navigation';
+$string['activitynavigation:jumptoactivity'] = 'Jump to activity';
+$string['activitynavigation:nexta'] = 'Next activity: {$a}';
+$string['activitynavigation:preva'] = 'Previous activity: {$a}';
$string['addselectedactivity'] = 'Add selected activity';
$string['aria:coursecategory'] = 'Course category';
$string['aria:courseshortname'] = 'Course short name';
@@ -74,7 +78,7 @@
$string['coursetoolong'] = 'The course is too long';
$string['courseurl'] = 'Course URL';
$string['customfield_islocked'] = 'Locked';
-$string['customfield_islocked_help'] = 'If the field is locked, only users with the capability to change locked custom fields (by default users with the default role of manager only) will be able to change it in the course settings.';
+$string['customfield_islocked_help'] = 'If the field is locked, only users with the capability to change locked course custom fields (users with the default role of manager) will be able to change it in the course settings.';
$string['customfield_notvisible'] = 'Nobody';
$string['customfield_visibility'] = 'Visible to';
$string['customfield_visibility_help'] = 'This setting determines who can view the custom field name and value in the list of courses or in the available custom field filter of the Dashboard.';
@@ -153,6 +157,7 @@
$string['sectionlink'] = 'Permalink';
$string['showstartedcoursestask'] = 'Show courses on start date';
$string['submitsearch'] = 'Submit search';
+$string['subsectiondescriptionwarning'] = 'Subsection descriptions will be discontinued in Moodle 5.2. To add a description, use a Text and media area instead.';
$string['supports'] = 'This activity supports';
$string['studentsatriskincourse'] = 'Students at risk in {$a} course';
$string['studentsatriskinfomessage'] = 'Hi {$a->userfirstname},
diff --git a/public/lang/en/customfield.php b/public/lang/en/customfield.php
index 847c015873a48..3a3f85be53117 100644
--- a/public/lang/en/customfield.php
+++ b/public/lang/en/customfield.php
@@ -37,10 +37,13 @@
$string['customfielddata'] = 'Custom fields data';
$string['customfields'] = 'Custom fields';
$string['defaultvalue'] = 'Default value';
+$string['deletecategory'] = 'Delete custom field category: {$a}';
+$string['deletefield'] = 'Delete custom field: {$a}';
$string['description'] = 'Description';
$string['description_help'] = 'The description is displayed in the form below the field.';
$string['edit'] = 'Edit';
$string['editcategoryname'] = 'Edit category name';
+$string['editfield'] = 'Edit custom field: {$a}';
$string['editingfield'] = 'Updating {$a}';
$string['errorfieldtypenotfound'] = 'Field type {$a} not found';
$string['erroruniquevalues'] = 'This value is already used.';
diff --git a/public/lang/en/error.php b/public/lang/en/error.php
index 9c72047d58673..137a924c045ef 100644
--- a/public/lang/en/error.php
+++ b/public/lang/en/error.php
@@ -531,6 +531,7 @@
$string['restrictedcontextexception'] = 'Sorry, execution of external function violates context restriction.';
$string['restricteduser'] = 'Sorry, but your current account "{$a}" is restricted from doing that';
$string['reverseproxyabused'] = 'Reverse proxy enabled so the server cannot be accessed directly. Please contact the server administrator.';
+$string['rootdirpublic'] = 'The Moodle root directory must not be publicly accessible. Please reconfigure your web server to use the `/public` directory instead.';
$string['rpcerror'] = 'Ooops! Your MNET communication has failed! Here\'s that error message to pass on to your administrator: {$a}';
$string['secretalreadyused'] = 'Change password confirmation link was already used, password was not changed';
$string['sectioncantbefound'] = 'This content can\'t be found. It may have been deleted, or the URL may be incorrect. ';
diff --git a/public/lang/en/group.php b/public/lang/en/group.php
index fa9d4ee55224f..8e46a50fcb370 100644
--- a/public/lang/en/group.php
+++ b/public/lang/en/group.php
@@ -178,6 +178,7 @@
$string['messagingdisabled'] = 'Successfully disabled messaging in {$a} group(s)';
$string['messagingenabled'] = 'Successfully enabled messaging in {$a} group(s)';
$string['mygroups'] = 'My groups';
+$string['nonparticipation'] = 'Non-participation';
$string['othergroups'] = 'Other groups';
$string['overview'] = 'Overview';
$string['participation'] = 'Show group in dropdown menu for activities in group mode';
diff --git a/public/lang/en/install.php b/public/lang/en/install.php
index 1e328aacc0d68..02bbc83d29ef2 100644
--- a/public/lang/en/install.php
+++ b/public/lang/en/install.php
@@ -215,6 +215,8 @@
Sessions can be enabled in the php.ini file ... look for the session.auto_start parameter. ';
$string['upgradingqtypeplugin'] = 'Upgrading question/type plugin';
+$string['webserverconfigproblemdescription'] = 'Your web server is not configured to prevent access to files outside the /public directory. For details of how to configure your web server correctly, see the documentation Upgrading - Code directories restructure. Once reconfigured, revisit the web root.';
+$string['webservernotconfigured'] = 'Web server not configured';
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
$string['welcomep20'] = 'You are seeing this page because you have successfully installed and
launched the {$a->packname} {$a->packversion} package in your computer. Congratulations!';
diff --git a/public/lang/en/message.php b/public/lang/en/message.php
index 869db5c3a55e1..121eda2d9733b 100644
--- a/public/lang/en/message.php
+++ b/public/lang/en/message.php
@@ -69,6 +69,7 @@
$string['editmessages'] = 'Edit messages';
$string['emailtagline'] = 'This is a copy of a message sent to you on the site {$a->sitename}. Go to {$a->url} to reply.';
$string['enabled'] = 'Enabled';
+$string['enablenotificationplugin'] = 'Enable notification plugin: {$a}';
$string['errorcallingprocessor'] = 'Error calling defined output';
$string['errorconversationdoesnotexist'] = 'Conversation does not exist';
$string['errormessagetoolong'] = 'The message is longer than the maximum allowed.';
@@ -155,6 +156,7 @@
$string['participants'] = 'Participants';
$string['pendingcontactrequests'] = 'There are {$a} pending contact requests';
$string['permitted'] = 'Permitted';
+$string['preference'] = 'Preference';
$string['privacy'] = 'Privacy';
$string['privacy_desc'] = 'You can restrict who can message you';
$string['privacy:metadata:core_favourites'] = 'The conversations starred by the user';
diff --git a/public/lang/en/moodle.php b/public/lang/en/moodle.php
index b17ff033cd77b..a11f20ce333ad 100644
--- a/public/lang/en/moodle.php
+++ b/public/lang/en/moodle.php
@@ -653,17 +653,12 @@
$string['emailconfirm'] = 'Confirm your account';
$string['emailconfirmation'] = 'Hi {$a->firstname},
-A new account has been requested at \'{$a->sitename}\'
-using your email address.
+A new account has been requested at \'{$a->sitename}\' using your email address.
-To confirm your new account, please go to this web address:
+To confirm your new account, please click the link below:
-{$a->link}
+Confirm your account
-In most mail programs, this should appear as a blue link
-which you can just click on. If that doesn\'t work,
-then cut and paste the address into the address
-line at the top of your web browser window.
If you need help, please contact the site administrator,
{$a->admin}';
@@ -710,20 +705,14 @@
$string['emailonlyallowed'] = 'This email cannot be used. Allowed email domains are: {$a}.';
$string['emailpasswordconfirmation'] = 'Hi {$a->firstname},
-Someone (probably you) has requested a new password for your
-account on \'{$a->sitename}\'.
+Someone (probably you) has requested a new password for your account on \'{$a->sitename}\'.
-To confirm this and have a new password sent to you via email,
-go to the following web address:
+To confirm this and have a new password sent to you via email, please click the link below:
-{$a->link}
+Get a new password
-In most mail programs, this should appear as a blue link
-which you can just click on. If that doesn\'t work,
-then cut and paste the address into the address
-line at the top of your web browser window.
-If you need help, please contact the site administrator,
+If you need help, please contact the site administrator.
{$a->admin}';
$string['emailpasswordconfirmationsubject'] = '{$a}: Change password confirmation';
$string['emailpasswordconfirmmaybesent'] = 'If you supplied a correct username or unique email address then an email should have been sent to you.
@@ -739,19 +728,14 @@
If you continue to have difficulty, contact the site administrator.';
$string['emailpasswordchangeinfo'] = 'Hi {$a->firstname},
-Someone (probably you) has requested a new password for your
-account \'{$a->username}\' on \'{$a->sitename}\'.
+Someone (probably you) has requested a new password for your account \'{$a->username}\' on \'{$a->sitename}\'.
-To change your password, please go to the following web address:
+To change your password, please click the link below:
-{$a->link}
+Change password
-In most mail programs, this should appear as a blue link
-which you can just click on. If that doesn\'t work,
-then cut and paste the address into the address
-line at the top of your web browser window.
-If you need help, please contact the site administrator,
+If you need help, please contact the site administrator.
{$a->admin}';
$string['emailpasswordchangeinfodisabled'] = 'Hi {$a->firstname},
@@ -768,14 +752,17 @@
A password reset was requested for your account \'{$a->username}\' at {$a->sitename}.
-To confirm this request, and set a new password for your account, please go to the following web address:
-{$a->link}
+To confirm this request, and set a new password for your account, please click the link below:
+
+Reset password
+
(This link is valid for {$a->resetminutes} minutes from the time this reset was first requested.)
If this password reset was not requested by you, no action is needed.
-If you need help, please contact the site administrator, {$a->admin}';
+If you need help, please contact the site administrator.
+{$a->admin}';
$string['emailresetconfirmationsubject'] = '{$a}: Password reset request';
$string['emailresetconfirmsent'] = 'An email has been sent to your address at {$a}.
It contains easy instructions to confirm and complete this password change.
@@ -1053,6 +1040,7 @@
$string['hidesettings'] = 'Hide settings';
$string['hideshowblocks'] = 'Hide or show blocks';
$string['hidepopoverwindow'] = 'Hide popover window';
+$string['hidex'] = 'Hide {$a}';
$string['highlight'] = 'Highlight';
$string['highlighted'] = 'Highlighted';
$string['highlightoff'] = 'Unhighlight';
@@ -1256,6 +1244,7 @@
If someone else has already chosen your username then you\'ll have to try again using a different username. ';
$string['loginto'] = 'Log in to {$a}';
$string['loginagain'] = 'Log in again';
+$string['loginrequired'] = 'Login required';
$string['logoof'] = 'Logo of {$a}';
$string['logout'] = 'Log out';
$string['logoutconfirm'] = 'Do you really want to log out?';
@@ -1369,6 +1358,8 @@
$string['missingurl'] = 'Missing URL';
$string['missingusername'] = 'Missing username';
$string['moddoesnotsupporttype'] = 'Module {$a->modname} does not support uploads of type {$a->type}';
+$string['modhidden'] = 'Availability';
+$string['modhidden_help'] = '* Hide on course page: Not available to students. This module cannot be shown to students.';
$string['modhide'] = 'Hide';
$string['modshow'] = 'Show';
$string['modvisible'] = 'Availability';
@@ -1524,10 +1515,10 @@
(You will be prompted to change your password when you log in for the first time.)
-To start using \'{$a->sitename}\', log in at
- {$a->link}
-If you need help, contact the site administrator,
+Click to log in and start using \'{$a->sitename}\'.
+
+If you need help, contact the site administrator.
{$a->signoff}';
$string['newusers'] = 'New users';
$string['newwindow'] = 'New window';
@@ -1626,6 +1617,7 @@
$string['opendrawerblocks'] = 'Open block drawer';
$string['opendrawerindex'] = 'Open course index';
$string['opensinnewwindow'] = 'Opens in new window';
+$string['opensinnewwindowbracketed'] = '(Opens in new window)';
$string['operator_and'] = 'and';
$string['operator_andnot'] = 'and';
$string['operator_or'] = 'or';
@@ -2068,6 +2060,7 @@
$string['showoncoursepage'] = 'Show on course page';
$string['showonly'] = 'Show only';
$string['showperpage'] = 'Show {$a} per page';
+$string['showpopovermenu'] = 'Open popover';
$string['showpopoverwindow'] = 'Show popover window';
$string['showrecent'] = 'Show recent activity';
$string['showreports'] = 'Show activity reports';
@@ -2075,6 +2068,7 @@
$string['showsettings'] = 'Show settings';
$string['showtheselogs'] = 'Show these logs';
$string['showthishelpinlanguage'] = 'Show this help in language: {$a}';
+$string['showx'] = 'Show {$a}';
$string['schedule'] = 'Schedule';
$string['sidepanel'] = 'Side panel';
$string['signoutofotherservices'] = 'Log out of all web apps';
diff --git a/public/lang/en/plugin.php b/public/lang/en/plugin.php
index 824fdfa3c2066..35d65d8e489d6 100644
--- a/public/lang/en/plugin.php
+++ b/public/lang/en/plugin.php
@@ -135,6 +135,8 @@
$string['type_communication_plural'] = 'Communication providers';
$string['type_contenttype'] = 'Content bank';
$string['type_contenttype_plural'] = 'Content bank plugins';
+$string['type_core'] = 'Core sub-system';
+$string['type_core_plural'] = 'Core sub-systems';
$string['type_customfield'] = 'Custom field';
$string['type_customfield_plural'] = 'Custom fields';
$string['type_coursereport'] = 'Course report';
diff --git a/public/lang/en/question.php b/public/lang/en/question.php
index f4ea5608c0250..6924486fe1913 100644
--- a/public/lang/en/question.php
+++ b/public/lang/en/question.php
@@ -211,6 +211,8 @@
$string['importwrongfileencoding'] = 'The file you selected does not use UTF-8 character encoding. {$a} files must use UTF-8.';
$string['importwrongfiletype'] = 'The type of the file you selected ({$a->actualtype}) does not match the type expected by this import format ({$a->expectedtype}).';
$string['invalidarg'] = 'No valid arguments supplied or incorrect server configuration';
+$string['invalidcategory'] = 'Invalid category provided for this question bank view.';
+$string['invalidcategoryeditq'] = 'Invalid category provided for this question bank view. Ask your administrator to try running the question/cli/fix_set_references_category_context.php script.';
$string['invalidcategoryidforparent'] = 'Invalid category id for parent!';
$string['invalidcategoryidtomove'] = 'Invalid category id to move!';
$string['invalidconfirm'] = 'Confirmation string was incorrect';
@@ -252,6 +254,7 @@
$string['movingquestionsandfiles'] = 'Are you sure you want to move question(s) {$a->questions} to context for "{$a->tocontext}"? We have detected {$a->urlcount} files linked from these question(s) in {$a->fromareaname}, would you like to copy or move these to {$a->toareaname}?';
$string['movingquestionsnofiles'] = 'Are you sure you want to move question(s) {$a->questions} to context for "{$a->tocontext}"? There are no files linked from these question(s) in {$a->fromareaname}.';
$string['needtochoosecat'] = 'You need to choose a category to move this question to or press \'cancel\'.';
+$string['nobankpermissions'] = 'You do not have permission to access any question banks on this course.';
$string['nobanks'] = 'This course doesn\'t have any question banks yet.';
$string['nocate'] = 'No such category {$a}!';
$string['nopermissionadd'] = 'You don\'t have permission to add questions here.';
diff --git a/public/lang/en/role.php b/public/lang/en/role.php
index 4efe575eaed5c..2107d3c77cfde 100644
--- a/public/lang/en/role.php
+++ b/public/lang/en/role.php
@@ -157,7 +157,7 @@
$string['confirmunassignyes'] = 'Remove';
$string['confirmunassignno'] = 'Cancel';
$string['contentbank:access'] = 'Access the content bank';
-$string['contentbank:changelockedcustomfields'] = 'Change content bank locked custom fields';
+$string['contentbank:changelockedcustomfields'] = 'Change locked content bank custom fields';
$string['contentbank:configurecustomfields'] = 'Configure content bank custom fields';
$string['contentbank:copyanycontent'] = 'Copy any content in the content bank';
$string['contentbank:copycontent'] = 'Copy content in the content bank';
@@ -181,7 +181,7 @@
$string['course:changecategory'] = 'Change course category';
$string['course:changefullname'] = 'Change course full name';
$string['course:changeidnumber'] = 'Change course ID number';
-$string['course:changelockedcustomfields'] = 'Change locked custom fields';
+$string['course:changelockedcustomfields'] = 'Change locked course custom fields';
$string['course:changeshortname'] = 'Change course short name';
$string['course:changesummary'] = 'Change course summary';
$string['course:configurecustomfields'] = 'Configure course custom fields';
diff --git a/public/lib/UPGRADING.md b/public/lib/UPGRADING.md
index 0ef3262150995..10c0d1c33d0ef 100644
--- a/public/lib/UPGRADING.md
+++ b/public/lib/UPGRADING.md
@@ -1,5 +1,54 @@
# core (subsystem) Upgrade notes
+## 5.1.3+
+
+### Added
+
+- The `core/toast` JS module now accepts a `visuallyHidden` configuration parameter to render visually hidden toast messages for screen reader users.
+
+ For more information see [MDL-87993](https://tracker.moodle.org/browse/MDL-87993)
+
+## 5.1.2
+
+### Added
+
+- There is a new Behat `toast_message` named selector to more easily assert the presence of Toast messages on the page
+
+ For more information see [MDL-87443](https://tracker.moodle.org/browse/MDL-87443)
+
+### Changed
+
+- `\core\output\core_renderer::confirm()`'s `$displayoptions` parameter now also accepts a `headinglevel` option that developers can use to specify the heading level of the confirmation's heading. If not specified, the confirmation heading will be rendered in an `h4` tag.
+
+ For more information see [MDL-87694](https://tracker.moodle.org/browse/MDL-87694)
+
+## 5.1.1
+
+### Added
+
+- Added clean_string() that prevents double escaping in Mustache templates
+
+ For more information see [MDL-87066](https://tracker.moodle.org/browse/MDL-87066)
+
+### Changed
+
+- The Hook Manager now uses localcache instead of caching via MUC.
+
+ For more information see [MDL-87107](https://tracker.moodle.org/browse/MDL-87107)
+
+### Fixed
+
+- `restore_qtype_plugin::unset_excluded_fields` now returns the modified questiondata structure,
+ in order to support structures that contain arrays.
+ If your qtype plugin overrides `restore_qtype_plugin::remove_excluded_question_data` without
+ calling the parent method, you may need to modify your overridden method to use the returned
+ value.
+
+ For more information see [MDL-85975](https://tracker.moodle.org/browse/MDL-85975)
+- When responding to pcntl signals, call existing signal handlers.
+
+ For more information see [MDL-87079](https://tracker.moodle.org/browse/MDL-87079)
+
## 5.1
### Added
diff --git a/public/lib/accesslib.php b/public/lib/accesslib.php
index 3c6df9dfbc574..74208c09e69d5 100644
--- a/public/lib/accesslib.php
+++ b/public/lib/accesslib.php
@@ -2881,12 +2881,12 @@ function get_roles_used_in_context(context $context, $includeparents = true) {
* It is using the CFG->profileroles to limit the list to only interesting roles.
* (The permission tab has full details of user role assignments.)
*
- * @param int $userid
+ * @param int $userid ID of the user whose course roles are filtered by visibility
* @param int $courseid
* @return string
*/
function get_user_roles_in_course($userid, $courseid) {
- global $CFG, $DB;
+ global $CFG, $DB, $USER;
if ($courseid == SITEID) {
$context = context_system::instance();
} else {
@@ -2926,7 +2926,7 @@ function get_user_roles_in_course($userid, $courseid) {
$rolestring = '';
if ($roles = $DB->get_records_sql($sql, $params)) {
- $viewableroles = get_viewable_roles($context, $userid);
+ $viewableroles = get_viewable_roles($context, $USER->id);
$rolenames = array();
foreach ($roles as $roleid => $unused) {
@@ -3384,7 +3384,7 @@ function get_switchable_roles(context $context, $rolenamedisplay = ROLENAME_ALIA
* Gets a list of roles that this user can view in a context
*
* @param context $context a context.
- * @param int $userid id of user.
+ * @param int $userid id of user whose viewable roles we are fetching
* @param int $rolenamedisplay the type of role name to display. One of the
* ROLENAME_X constants. Default ROLENAME_ALIAS.
* @return array an array $roleid => $rolename.
@@ -3796,14 +3796,13 @@ function get_with_capability_join(context $context, $capability, $useridcolumn)
AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
} else {
- $unions[] = "SELECT userid
- FROM {role_assignments}
- WHERE contextid IN ($ctxids) AND roleid IN (" . implode(',', array_keys($needed[$cap])) . ")
- AND userid NOT IN (
- SELECT userid
- FROM {role_assignments}
- WHERE contextid IN ($ctxids)
- AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
+ $unions[] = "SELECT ra.userid
+ FROM {role_assignments} ra
+ LEFT JOIN {role_assignments} rap ON (rap.userid = ra.userid
+ AND rap.contextid IN ($ctxids)
+ AND rap.roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))
+ WHERE ra.contextid IN ($ctxids) AND ra.roleid IN (" . implode(',', array_keys($needed[$cap])) . ")
+ AND rap.id IS NULL";
}
}
}
diff --git a/public/lib/adminlib.php b/public/lib/adminlib.php
index 043bd6ce3f31d..65bc96ae5e3a6 100644
--- a/public/lib/adminlib.php
+++ b/public/lib/adminlib.php
@@ -6838,7 +6838,7 @@ public function output_html($data, $query='') {
$table->head = array(get_string('name'), $strusage, $strversion, $strenable, $strup.'/'.$strdown, $strsettings, $strtest, $struninstall);
$table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign');
$table->id = 'courseenrolmentplugins';
- $table->attributes['class'] = 'admintable table generaltable';
+ $table->attributes['class'] = 'admintable table generaltable table-hover';
$table->data = array();
// Iterate through enrol plugins and add to the display table.
@@ -7407,7 +7407,7 @@ public function output_html($data, $query='') {
$table->head = array($txt->name, $txt->users, $txt->enable, $txt->updown, $txt->settings, $txt->testsettings, $txt->uninstall);
$table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign', 'centeralign');
$table->data = array();
- $table->attributes['class'] = 'admintable table generaltable';
+ $table->attributes['class'] = 'admintable table generaltable table-hover';
$table->id = 'manageauthtable';
//add always enabled plugins first
@@ -7608,7 +7608,7 @@ public function output_html($data, $query='') {
$table->head = array($txt->name, $txt->enable, $txt->updown, $txt->settings, $struninstall);
$table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign');
$table->id = 'antivirusmanagement';
- $table->attributes['class'] = 'admintable table generaltable';
+ $table->attributes['class'] = 'admintable table generaltable table-hover';
$table->data = array();
// Iterate through auth plugins and add to the display table.
@@ -7773,7 +7773,7 @@ public function output_html($data, $query='') {
$table = new html_table();
$table->head = array($txt->name, $txt->enable, $txt->updown, $txt->uninstall, $txt->settings);
$table->align = array('left', 'center', 'center', 'center', 'center');
- $table->attributes['class'] = 'manageformattable table generaltable admintable';
+ $table->attributes['class'] = 'manageformattable table generaltable admintable table-striped table-hover';
$table->data = array();
$cnt = 0;
@@ -7926,7 +7926,7 @@ public function output_html($data, $query='') {
$table = new html_table();
$table->head = array($txt->name, $txt->enable, $txt->uninstall, $txt->settings);
$table->align = array('left', 'center', 'center', 'center');
- $table->attributes['class'] = 'managecustomfieldtable table generaltable admintable';
+ $table->attributes['class'] = 'managecustomfieldtable table generaltable admintable table-striped table-hover';
$table->data = array();
$spacer = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'iconsmall'));
@@ -8052,7 +8052,7 @@ public function output_html($data, $query='') {
$table = new html_table();
$table->head = array($txt->name, $txt->enable, $txt->updown, $txt->uninstall, $txt->settings);
$table->align = array('left', 'center', 'center', 'center', 'center');
- $table->attributes['class'] = 'manageformattable table generaltable admintable';
+ $table->attributes['class'] = 'manageformattable table generaltable admintable table-striped table-hover';
$table->data = array();
$cnt = 0;
@@ -8536,7 +8536,7 @@ public function output_html($data, $query='') {
$table->colclasses = array('leftalign', 'leftalign', 'centeralign',
'centeralign', 'centeralign', 'centeralign', 'centeralign');
$table->id = 'mediaplayerplugins';
- $table->attributes['class'] = 'admintable table generaltable';
+ $table->attributes['class'] = 'admintable table generaltable table-hover';
$table->data = array();
// Iterate through media plugins and add to the display table.
@@ -8718,7 +8718,7 @@ public function output_html($data, $query='') {
$table = new html_table();
$table->head = array($txt->name, $txt->enable, $txt->order, $txt->settings, $txt->uninstall);
$table->align = array('left', 'center', 'center', 'center', 'center');
- $table->attributes['class'] = 'managecontentbanktable table generaltable admintable';
+ $table->attributes['class'] = 'managecontentbanktable table generaltable admintable table-hover';
$table->data = array();
$spacer = $OUTPUT->pix_icon('spacer', '', 'moodle', array('class' => 'iconsmall'));
@@ -10006,7 +10006,7 @@ public function output_html($data, $query='') {
$table->head = array($strservice, $strplugin, $strfunctions, $strusers, $stredit);
$table->colclasses = array('leftalign service', 'leftalign plugin', 'centeralign functions', 'centeralign users', 'centeralign ');
$table->id = 'builtinservices';
- $table->attributes['class'] = 'admintable externalservices table generaltable';
+ $table->attributes['class'] = 'admintable externalservices table generaltable table-hover';
$table->data = array();
// iterate through auth plugins and add to the display table
@@ -10046,7 +10046,7 @@ public function output_html($data, $query='') {
$table->head = array($strservice, $strdelete, $strfunctions, $strusers, $stredit);
$table->colclasses = array('leftalign service', 'leftalign plugin', 'centeralign functions', 'centeralign users', 'centeralign ');
$table->id = 'customservices';
- $table->attributes['class'] = 'admintable externalservices table generaltable';
+ $table->attributes['class'] = 'admintable externalservices table generaltable table-hover';
$table->data = array();
// iterate through auth plugins and add to the display table
@@ -10151,7 +10151,7 @@ public function output_html($data, $query='') {
get_string('description'));
$table->colclasses = array('leftalign step', 'leftalign status', 'leftalign description');
$table->id = 'onesystemcontrol';
- $table->attributes['class'] = 'admintable wsoverview table generaltable';
+ $table->attributes['class'] = 'admintable wsoverview table generaltable table-hover';
$table->data = array();
$return .= $brtag . get_string('onesystemcontrollingdescription', 'webservice')
@@ -10275,7 +10275,7 @@ public function output_html($data, $query='') {
get_string('description'));
$table->colclasses = array('leftalign step', 'leftalign status', 'leftalign description');
$table->id = 'userasclients';
- $table->attributes['class'] = 'admintable wsoverview table generaltable';
+ $table->attributes['class'] = 'admintable wsoverview table generaltable table-hover';
$table->data = array();
$return .= $brtag . get_string('userasclientsdescription', 'webservice') .
@@ -10468,7 +10468,7 @@ public function output_html($data, $query='') {
$table->head = array($strprotocol, $strversion, $strenable, $strsettings);
$table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign');
$table->id = 'webserviceprotocols';
- $table->attributes['class'] = 'admintable table generaltable';
+ $table->attributes['class'] = 'admintable table generaltable table-hover';
$table->data = array();
// iterate through auth plugins and add to the display table
@@ -10623,13 +10623,13 @@ protected function validate($data) {
return $data;
} else if (in_array(strtolower($data), $colornames)) {
return $data;
- } else if (preg_match('/rgb\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\)/i', $data)) {
+ } else if (preg_match('/^rgb\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\)$/i', $data)) {
return $data;
- } else if (preg_match('/rgba\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\, ?\d(\.\d)?\)/i', $data)) {
+ } else if (preg_match('/^rgba\(\d{0,3}%?\, ?\d{0,3}%?, ?\d{0,3}%?\, ?\d(\.\d)?\)$/i', $data)) {
return $data;
- } else if (preg_match('/hsl\(\d{0,3}\, ?\d{0,3}%, ?\d{0,3}%\)/i', $data)) {
+ } else if (preg_match('/^hsl\(\d{0,3}\, ?\d{0,3}%, ?\d{0,3}%\)$/i', $data)) {
return $data;
- } else if (preg_match('/hsla\(\d{0,3}\, ?\d{0,3}%,\d{0,3}%\, ?\d(\.\d)?\)/i', $data)) {
+ } else if (preg_match('/^hsla\(\d{0,3}\, ?\d{0,3}%, ?\d{0,3}%\, ?\d(\.\d)?\)$/i', $data)) {
return $data;
} else if (($data == 'transparent') || ($data == 'currentColor') || ($data == 'inherit')) {
return $data;
@@ -11120,7 +11120,7 @@ public function output_html($data, $query='') {
$table->head = array(get_string('step', 'search'), get_string('status'));
$table->colclasses = array('leftalign step', 'leftalign status');
$table->id = 'searchsetup';
- $table->attributes['class'] = 'admintable table generaltable';
+ $table->attributes['class'] = 'admintable table generaltable table-hover';
$table->data = array();
$return .= $brtag . get_string('searchsetupdescription', 'search') . $brtag . $brtag;
diff --git a/public/lib/amd/build/datafilter.min.js b/public/lib/amd/build/datafilter.min.js
index 98353ea8c493e..dfd985da79b2a 100644
--- a/public/lib/amd/build/datafilter.min.js
+++ b/public/lib/amd/build/datafilter.min.js
@@ -1,3 +1,3 @@
-define("core/datafilter",["exports","core/datafilter/filtertypes/courseid","core/datafilter/filtertype","core/str","core/notification","core/pending","core/datafilter/selectors","core/templates","core/custom_interaction_events","jquery"],(function(_exports,_courseid,_filtertype,_str,_notification,_pending,_selectors,_templates,_custom_interaction_events,_jquery){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_courseid=_interopRequireDefault(_courseid),_filtertype=_interopRequireDefault(_filtertype),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}return _exports.default=class{constructor(filterSet,applyCallback){this.filterSet=filterSet,this.applyCallback=applyCallback,this.activeFilters={courseid:new _courseid.default("courseid",filterSet)}}init(){this.filterSet.querySelector(_selectors.default.filterset.region).addEventListener("click",(e=>{e.target.closest(_selectors.default.filterset.actions.addRow)&&(e.preventDefault(),this.addFilterRow()),e.target.closest(_selectors.default.filterset.actions.applyFilters)&&(e.preventDefault(),this.updateTableFromFilter()),e.target.closest(_selectors.default.filterset.actions.resetFilters)&&(e.preventDefault(),this.removeAllFilters())})),this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist).addEventListener("click",(e=>{e.target.closest(_selectors.default.filter.actions.remove)&&(e.preventDefault(),this.removeOrReplaceFilterRow(e.target.closest(_selectors.default.filter.region),!0))}));let filterRegion=(0,_jquery.default)(this.getFilterRegion());_custom_interaction_events.default.define(filterRegion,[_custom_interaction_events.default.events.accessibleChange]),filterRegion.on(_custom_interaction_events.default.events.accessibleChange,(e=>{const typeField=e.target.closest(_selectors.default.filter.fields.type);if(typeField&&typeField.value){const filter=e.target.closest(_selectors.default.filter.region);this.addFilter(filter,typeField.value)}})),this.filterSet.querySelector(_selectors.default.filterset.fields.join).addEventListener("change",(e=>{this.filterSet.dataset.filterverb=e.target.value}))}getFilterRegion(){return this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist)}addFilterRow(){var _filterdata$rownum;let filterdata=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("core/datafilter:addFilterRow"),rownum=null!==(_filterdata$rownum=filterdata.rownum)&&void 0!==_filterdata$rownum?_filterdata$rownum:1+this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length;return _templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rownum}).then((_ref=>{let{html:html,js:js}=_ref;return _templates.default.appendNodeContents(this.getFilterRegion(),html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((result=>(pendingPromise.resolve(),filterdata.filtertype&&result.forEach((filter=>{this.addFilter(filter,filterdata.filtertype,filterdata.values,filterdata.jointype,filterdata.filteroptions)})),result))).catch(_notification.default.exception)}getFilterDataSource(filterType){return this.filterSet.querySelector(_selectors.default.filterset.regions.datasource).querySelector(_selectors.default.data.fields.byName(filterType))}async addFilter(filterRow,filterType,initialFilterValues,filterJoin,filterOptions){filterRow.dataset.filterType=filterType;const filterDataNode=this.getFilterDataSource(filterType);let Filter=_filtertype.default;if(filterDataNode.dataset.filterTypeClass)try{Filter=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([filterDataNode.dataset.filterTypeClass],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(filterDataNode.dataset.filterTypeClass)):Promise.resolve(_systemImportTransformerGlobalIdentifier[filterDataNode.dataset.filterTypeClass]))}catch(error){_notification.default.exception(error)}this.activeFilters[filterType]=new Filter(filterType,this.filterSet,initialFilterValues,filterOptions);const typeField=filterRow.querySelector(_selectors.default.filter.fields.type);typeField.value=filterType,typeField.disabled="disabled",this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList),filterRow);const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);return isNaN(filterJoin)||(joinField.value=filterJoin),this.updateFiltersOptions(),this.activeFilters[filterType]}getFilterObject(name){return this.activeFilters[name]}removeOrReplaceFilterRow(filterRow,refreshContent){1===this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length?this.replaceFilterRow(filterRow,refreshContent):this.removeFilterRow(filterRow,refreshContent)}async removeFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(filterRow.querySelector(_selectors.default.data.required))return;const hasFilterValue=!!filterRow.querySelector(_selectors.default.filter.fields.type).value;this.removeFilterObject(filterRow.dataset.filterType),filterRow.remove(),this.updateFiltersOptions(),hasFilterValue&&refreshContent&&this.updateTableFromFilter();const filterLegends=await this.getAvailableFilterLegends();this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach(((filterRow,index)=>{filterRow.querySelector("legend").innerText=filterLegends[index]}))}replaceFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],rowNum=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;if(!filterRow.querySelector(_selectors.default.data.required))return this.removeFilterObject(filterRow.dataset.filterType),_templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rowNum}).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNode(filterRow,html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((filterRow=>refreshContent?this.updateTableFromFilter():filterRow)).catch(_notification.default.exception)}removeFilterObject(filterName){if(filterName){const filter=this.getFilterObject(filterName);filter&&(filter.tearDown(),delete this.activeFilters[filterName])}}removeAllFilters(){return this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>this.removeOrReplaceFilterRow(filterRow,!1))),this.updateTableFromFilter()}removeEmptyFilters(){this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>{filterRow.querySelector(_selectors.default.filter.fields.type).value||this.removeOrReplaceFilterRow(filterRow,!1)}))}updateFiltersOptions(){const filters=this.getFilterRegion().querySelectorAll(_selectors.default.filter.region);filters.forEach((filterRow=>{filterRow.querySelectorAll(_selectors.default.filter.fields.type+" option").forEach((option=>{option.value===filterRow.dataset.filterType?(option.classList.remove("hidden"),option.disabled=!1):this.activeFilters[option.value]?(option.classList.add("hidden"),option.disabled=!0):(option.classList.remove("hidden"),option.disabled=!1)}))}));const addRowButton=this.filterSet.querySelector(_selectors.default.filterset.actions.addRow);this.filterSet.querySelectorAll(_selectors.default.data.fields.all).length<=filters.length?addRowButton.setAttribute("disabled","disabled"):addRowButton.removeAttribute("disabled"),1===filters.length?(this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.add("hidden"),this.filterSet.querySelector(_selectors.default.filterset.fields.join).value=2,this.filterSet.dataset.filterverb=2):this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.remove("hidden")}updateTableFromFilter(){let validate=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];const pendingPromise=new _pending.default("core/datafilter:updateTableFromFilter"),filters={};let valid=!0;Object.values(this.activeFilters).forEach((filter=>{validate&&(valid=valid&&filter.validate()),filters[filter.filterValue.name]=filter.filterValue})),validate&&(valid=valid&&document.querySelector(_selectors.default.filter.region).closest("form").reportValidity()),this.applyCallback&&valid?this.applyCallback(filters,pendingPromise):pendingPromise.resolve()}async getAvailableFilterLegends(){const maxFilters=document.querySelector(_selectors.default.data.typeListSelect).length-1;let requests=[];[...Array(maxFilters)].forEach(((_,rowIndex)=>{requests.push({key:"filterrowlegend",component:"core",param:rowIndex+1})}));return await(0,_str.getStrings)(requests).then((fetchedStrings=>fetchedStrings)).catch(_notification.default.exception)}updateJoinList(filterJoinList,filterRow){const regularJoinList=[0,1,2];if(0!==filterJoinList.length){const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);regularJoinList.forEach((join=>{filterJoinList.includes(join)||(joinField.options[join].classList.add("hidden"),joinField.options[join].disabled=!0)})),joinField.options.forEach(((element,index)=>{element.disabled&&(joinField.options[index]=null)})),1===joinField.options.length&&(joinField.hidden=!0)}}},_exports.default}));
+define("core/datafilter",["exports","core/datafilter/filtertype","core/str","core/notification","core/pending","core/datafilter/selectors","core/templates","core/custom_interaction_events","jquery"],(function(_exports,_filtertype,_str,_notification,_pending,_selectors,_templates,_custom_interaction_events,_jquery){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_filtertype=_interopRequireDefault(_filtertype),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}return _exports.default=class{constructor(filterSet,applyCallback){this.filterSet=filterSet,this.applyCallback=applyCallback,this.activeFilters={}}init(){this.filterSet.querySelector(_selectors.default.filterset.region).addEventListener("click",(e=>{e.target.closest(_selectors.default.filterset.actions.addRow)&&(e.preventDefault(),this.addFilterRow()),e.target.closest(_selectors.default.filterset.actions.applyFilters)&&(e.preventDefault(),this.updateTableFromFilter()),e.target.closest(_selectors.default.filterset.actions.resetFilters)&&(e.preventDefault(),this.removeAllFilters())})),this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist).addEventListener("click",(e=>{e.target.closest(_selectors.default.filter.actions.remove)&&(e.preventDefault(),this.removeOrReplaceFilterRow(e.target.closest(_selectors.default.filter.region),!0))}));let filterRegion=(0,_jquery.default)(this.getFilterRegion());_custom_interaction_events.default.define(filterRegion,[_custom_interaction_events.default.events.accessibleChange]),filterRegion.on(_custom_interaction_events.default.events.accessibleChange,(e=>{const typeField=e.target.closest(_selectors.default.filter.fields.type);if(typeField&&typeField.value){const filter=e.target.closest(_selectors.default.filter.region);this.addFilter(filter,typeField.value)}})),this.filterSet.querySelector(_selectors.default.filterset.fields.join).addEventListener("change",(e=>{this.filterSet.dataset.filterverb=e.target.value}))}getFilterRegion(){return this.filterSet.querySelector(_selectors.default.filterset.regions.filterlist)}addFilterRow(){var _filterdata$rownum;let filterdata=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};const pendingPromise=new _pending.default("core/datafilter:addFilterRow"),rownum=null!==(_filterdata$rownum=filterdata.rownum)&&void 0!==_filterdata$rownum?_filterdata$rownum:1+this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length;return _templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rownum}).then((_ref=>{let{html:html,js:js}=_ref;return _templates.default.appendNodeContents(this.getFilterRegion(),html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((result=>(pendingPromise.resolve(),filterdata.filtertype&&result.forEach((filter=>{this.addFilter(filter,filterdata.filtertype,filterdata.values,filterdata.jointype,filterdata.filteroptions)})),result))).catch(_notification.default.exception)}getFilterDataSource(filterType){return this.filterSet.querySelector(_selectors.default.filterset.regions.datasource).querySelector(_selectors.default.data.fields.byName(filterType))}async addFilter(filterRow,filterType,initialFilterValues,filterJoin,filterOptions){filterRow.dataset.filterType=filterType;const filterDataNode=this.getFilterDataSource(filterType);let Filter=_filtertype.default;if(filterDataNode.dataset.filterTypeClass)try{Filter=await("function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([filterDataNode.dataset.filterTypeClass],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(filterDataNode.dataset.filterTypeClass)):Promise.resolve(_systemImportTransformerGlobalIdentifier[filterDataNode.dataset.filterTypeClass]))}catch(error){_notification.default.exception(error)}this.activeFilters[filterType]=new Filter(filterType,this.filterSet,initialFilterValues,filterOptions);const typeField=filterRow.querySelector(_selectors.default.filter.fields.type);typeField.value=filterType,typeField.disabled="disabled",this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList),filterRow);const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);return isNaN(filterJoin)||(joinField.value=filterJoin),this.updateFiltersOptions(),this.activeFilters[filterType]}getFilterObject(name){return this.activeFilters[name]}removeOrReplaceFilterRow(filterRow,refreshContent){1===this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).length?this.replaceFilterRow(filterRow,refreshContent):this.removeFilterRow(filterRow,refreshContent)}async removeFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(filterRow.querySelector(_selectors.default.data.required))return;const hasFilterValue=!!filterRow.querySelector(_selectors.default.filter.fields.type).value;this.removeFilterObject(filterRow.dataset.filterType),filterRow.remove(),this.updateFiltersOptions(),hasFilterValue&&refreshContent&&this.updateTableFromFilter();const filterLegends=await this.getAvailableFilterLegends();this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach(((filterRow,index)=>{filterRow.querySelector("legend").innerText=filterLegends[index]}))}replaceFilterRow(filterRow){let refreshContent=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],rowNum=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;if(!filterRow.querySelector(_selectors.default.data.required))return this.removeFilterObject(filterRow.dataset.filterType),_templates.default.renderForPromise("core/datafilter/filter_row",{rownumber:rowNum}).then((_ref2=>{let{html:html,js:js}=_ref2;return _templates.default.replaceNode(filterRow,html,js)})).then((filterRow=>{const typeList=this.filterSet.querySelector(_selectors.default.data.typeList);return filterRow.forEach((contentNode=>{const contentTypeList=contentNode.querySelector(_selectors.default.filter.fields.type);contentTypeList&&(contentTypeList.innerHTML=typeList.innerHTML)})),filterRow})).then((filterRow=>(this.updateFiltersOptions(),filterRow))).then((filterRow=>refreshContent?this.updateTableFromFilter():filterRow)).catch(_notification.default.exception)}removeFilterObject(filterName){if(filterName){const filter=this.getFilterObject(filterName);filter&&(filter.tearDown(),delete this.activeFilters[filterName])}}removeAllFilters(){return this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>this.removeOrReplaceFilterRow(filterRow,!1))),this.updateTableFromFilter()}removeEmptyFilters(){this.getFilterRegion().querySelectorAll(_selectors.default.filter.region).forEach((filterRow=>{filterRow.querySelector(_selectors.default.filter.fields.type).value||this.removeOrReplaceFilterRow(filterRow,!1)}))}updateFiltersOptions(){const filters=this.getFilterRegion().querySelectorAll(_selectors.default.filter.region);filters.forEach((filterRow=>{filterRow.querySelectorAll(_selectors.default.filter.fields.type+" option").forEach((option=>{option.value===filterRow.dataset.filterType?(option.classList.remove("hidden"),option.disabled=!1):this.activeFilters[option.value]?(option.classList.add("hidden"),option.disabled=!0):(option.classList.remove("hidden"),option.disabled=!1)}))}));const addRowButton=this.filterSet.querySelector(_selectors.default.filterset.actions.addRow);this.filterSet.querySelectorAll(_selectors.default.data.fields.all).length<=filters.length?addRowButton.setAttribute("disabled","disabled"):addRowButton.removeAttribute("disabled"),1===filters.length?(this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.add("hidden"),this.filterSet.querySelector(_selectors.default.filterset.fields.join).value=2,this.filterSet.dataset.filterverb=2):this.filterSet.querySelector(_selectors.default.filterset.regions.filtermatch).classList.remove("hidden")}updateTableFromFilter(){let validate=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];const pendingPromise=new _pending.default("core/datafilter:updateTableFromFilter"),filters={};let valid=!0;Object.values(this.activeFilters).forEach((filter=>{validate&&(valid=valid&&filter.validate()),filters[filter.filterValue.name]=filter.filterValue})),validate&&(valid=valid&&document.querySelector(_selectors.default.filter.region).closest("form").reportValidity()),this.applyCallback&&valid?this.applyCallback(filters,pendingPromise):pendingPromise.resolve()}async getAvailableFilterLegends(){const maxFilters=document.querySelector(_selectors.default.data.typeListSelect).length-1;let requests=[];[...Array(maxFilters)].forEach(((_,rowIndex)=>{requests.push({key:"filterrowlegend",component:"core",param:rowIndex+1})}));return await(0,_str.getStrings)(requests).then((fetchedStrings=>fetchedStrings)).catch(_notification.default.exception)}updateJoinList(filterJoinList,filterRow){const regularJoinList=[0,1,2];if(0!==filterJoinList.length){const joinField=filterRow.querySelector(_selectors.default.filter.fields.join);regularJoinList.forEach((join=>{filterJoinList.includes(join)||(joinField.options[join].classList.add("hidden"),joinField.options[join].disabled=!0)})),joinField.options.forEach(((element,index)=>{element.disabled&&(joinField.options[index]=null)})),1===joinField.options.length&&(joinField.hidden=!0)}}},_exports.default}));
//# sourceMappingURL=datafilter.min.js.map
\ No newline at end of file
diff --git a/public/lib/amd/build/datafilter.min.js.map b/public/lib/amd/build/datafilter.min.js.map
index c51f0a1a00028..1dc0d6fe0301f 100644
--- a/public/lib/amd/build/datafilter.min.js.map
+++ b/public/lib/amd/build/datafilter.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"datafilter.min.js","sources":["../src/datafilter.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 * Data filter management.\n *\n * @module core/datafilter\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport CourseFilter from 'core/datafilter/filtertypes/courseid';\nimport GenericFilter from 'core/datafilter/filtertype';\nimport {getStrings} from 'core/str';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport CustomEvents from 'core/custom_interaction_events';\nimport jQuery from 'jquery';\n\nexport default class {\n\n /**\n * Initialise the filter on the element with the given filterSet and callback.\n *\n * @param {HTMLElement} filterSet The filter element.\n * @param {Function} applyCallback Callback function when updateTableFromFilter\n */\n constructor(filterSet, applyCallback) {\n\n this.filterSet = filterSet;\n this.applyCallback = applyCallback;\n // Keep a reference to all of the active filters.\n this.activeFilters = {\n courseid: new CourseFilter('courseid', filterSet),\n };\n }\n\n /**\n * Initialise event listeners to the filter.\n */\n init() {\n // Add listeners for the main actions.\n this.filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {\n if (e.target.closest(Selectors.filterset.actions.addRow)) {\n e.preventDefault();\n\n this.addFilterRow();\n }\n\n if (e.target.closest(Selectors.filterset.actions.applyFilters)) {\n e.preventDefault();\n this.updateTableFromFilter();\n }\n\n if (e.target.closest(Selectors.filterset.actions.resetFilters)) {\n e.preventDefault();\n\n this.removeAllFilters();\n }\n });\n\n // Add the listener to remove a single filter.\n this.filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {\n if (e.target.closest(Selectors.filter.actions.remove)) {\n e.preventDefault();\n\n this.removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);\n }\n });\n\n // Add listeners for the filter type selection.\n let filterRegion = jQuery(this.getFilterRegion());\n CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);\n filterRegion.on(CustomEvents.events.accessibleChange, e => {\n const typeField = e.target.closest(Selectors.filter.fields.type);\n if (typeField && typeField.value) {\n const filter = e.target.closest(Selectors.filter.region);\n\n this.addFilter(filter, typeField.value);\n }\n });\n\n this.filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {\n this.filterSet.dataset.filterverb = e.target.value;\n });\n }\n\n /**\n * Get the filter list region.\n *\n * @return {HTMLElement}\n */\n getFilterRegion() {\n return this.filterSet.querySelector(Selectors.filterset.regions.filterlist);\n }\n\n /**\n * Add a filter row.\n *\n * @param {Object} filterdata Optional, data for adding for row with an existing filter.\n * @return {Promise}\n */\n addFilterRow(filterdata = {}) {\n const pendingPromise = new Pending('core/datafilter:addFilterRow');\n const rownum = filterdata.rownum ?? 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rownum})\n .then(({html, js}) => {\n const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(result => {\n pendingPromise.resolve();\n\n // If an existing filter is passed in, add it. Otherwise, leave the row empty.\n if (filterdata.filtertype) {\n result.forEach(filter => {\n this.addFilter(filter, filterdata.filtertype, filterdata.values,\n filterdata.jointype, filterdata.filteroptions);\n });\n }\n return result;\n })\n .catch(Notification.exception);\n }\n\n /**\n * Get the filter data source node fro the specified filter type.\n *\n * @param {String} filterType\n * @return {HTMLElement}\n */\n getFilterDataSource(filterType) {\n const filterDataNode = this.filterSet.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));\n }\n\n /**\n * Add a filter to the list of active filters, performing any necessary setup.\n *\n * @param {HTMLElement} filterRow\n * @param {String} filterType\n * @param {Array} initialFilterValues The initially selected values for the filter\n * @param {String} filterJoin\n * @param {Object} filterOptions\n * @returns {Filter}\n */\n async addFilter(filterRow, filterType, initialFilterValues, filterJoin, filterOptions) {\n // Name the filter on the filter row.\n filterRow.dataset.filterType = filterType;\n\n const filterDataNode = this.getFilterDataSource(filterType);\n\n // Instantiate the Filter class.\n let Filter = GenericFilter;\n if (filterDataNode.dataset.filterTypeClass) {\n\n // Ensure the filter class passed through exists, otherwise the filtering will break.\n try {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n } catch (error) {\n Notification.exception(error);\n }\n\n }\n this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues, filterOptions);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.value = filterType;\n typeField.disabled = 'disabled';\n // Update the join list.\n this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList), filterRow);\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n if (!isNaN(filterJoin)) {\n joinField.value = filterJoin;\n }\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n return this.activeFilters[filterType];\n }\n\n /**\n * Get the registered filter class for the named filter.\n *\n * @param {String} name\n * @return {Object} See the Filter class.\n */\n getFilterObject(name) {\n return this.activeFilters[name];\n }\n\n /**\n * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,\n * that it is replaced instead of being removed.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n removeOrReplaceFilterRow(filterRow, refreshContent) {\n const filterCount = this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n if (filterCount === 1) {\n this.replaceFilterRow(filterRow, refreshContent);\n } else {\n this.removeFilterRow(filterRow, refreshContent);\n }\n }\n\n /**\n * Remove the specified filter row and associated class.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n async removeFilterRow(filterRow, refreshContent = true) {\n if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n const hasFilterValue = !!filterType.value;\n\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n // Remove the actual filter HTML.\n filterRow.remove();\n\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n if (hasFilterValue && refreshContent) {\n // Refresh the table if there was any content in this row.\n this.updateTableFromFilter();\n }\n\n // Update filter fieldset legends.\n const filterLegends = await this.getAvailableFilterLegends();\n\n this.getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {\n filterRow.querySelector('legend').innerText = filterLegends[index];\n });\n\n }\n\n /**\n * Replace the specified filter row with a new one.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).\n * @return {Promise}\n */\n replaceFilterRow(filterRow, refreshContent = true, rowNum = 1) {\n if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rowNum})\n .then(({html, js}) => {\n const newContentNodes = Templates.replaceNode(filterRow, html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(filterRow => {\n // Refresh the table.\n if (refreshContent) {\n return this.updateTableFromFilter();\n } else {\n return filterRow;\n }\n })\n .catch(Notification.exception);\n }\n\n /**\n * Remove the Filter Object from the register.\n *\n * @param {string} filterName The name of the filter to be removed\n */\n removeFilterObject(filterName) {\n if (filterName) {\n const filter = this.getFilterObject(filterName);\n if (filter) {\n filter.tearDown();\n\n // Remove from the list of active filters.\n delete this.activeFilters[filterName];\n }\n }\n }\n\n /**\n * Remove all filters.\n *\n * @returns {Promise}\n */\n removeAllFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => this.removeOrReplaceFilterRow(filterRow, false));\n\n // Refresh the table.\n return this.updateTableFromFilter();\n }\n\n /**\n * Remove any empty filters.\n */\n removeEmptyFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n if (!filterType.value) {\n this.removeOrReplaceFilterRow(filterRow, false);\n }\n });\n }\n\n /**\n * Update the list of filter types to filter out those already selected.\n */\n updateFiltersOptions() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');\n options.forEach(option => {\n if (option.value === filterRow.dataset.filterType) {\n option.classList.remove('hidden');\n option.disabled = false;\n } else if (this.activeFilters[option.value]) {\n option.classList.add('hidden');\n option.disabled = true;\n } else {\n option.classList.remove('hidden');\n option.disabled = false;\n }\n });\n });\n\n // Configure the state of the \"Add row\" button.\n // This button is disabled when there is a filter row available for each condition.\n const addRowButton = this.filterSet.querySelector(Selectors.filterset.actions.addRow);\n const filterDataNode = this.filterSet.querySelectorAll(Selectors.data.fields.all);\n if (filterDataNode.length <= filters.length) {\n addRowButton.setAttribute('disabled', 'disabled');\n } else {\n addRowButton.removeAttribute('disabled');\n }\n\n if (filters.length === 1) {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');\n this.filterSet.querySelector(Selectors.filterset.fields.join).value = 2;\n this.filterSet.dataset.filterverb = 2;\n } else {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');\n }\n }\n\n /**\n * Update the Dynamic table based upon the current filter.\n *\n * @param {bool} validate Should we validate the filters? We might want to skip this if the filters won't have changed,\n * for example for pagination/sorting.\n */\n updateTableFromFilter(validate = true) {\n const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');\n\n const filters = {};\n let valid = true;\n Object.values(this.activeFilters).forEach(filter => {\n if (validate) {\n valid = valid && filter.validate();\n }\n filters[filter.filterValue.name] = filter.filterValue;\n });\n if (validate) {\n valid = valid && document.querySelector(Selectors.filter.region).closest('form').reportValidity();\n }\n if (this.applyCallback && valid) {\n this.applyCallback(filters, pendingPromise);\n } else {\n pendingPromise.resolve();\n }\n }\n\n /**\n * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.\n *\n * @return {array}\n */\n async getAvailableFilterLegends() {\n const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;\n let requests = [];\n\n [...Array(maxFilters)].forEach((_, rowIndex) => {\n requests.push({\n \"key\": \"filterrowlegend\",\n \"component\": \"core\",\n // Add 1 since rows begin at 1 (index begins at zero).\n \"param\": rowIndex + 1\n });\n });\n\n const legendStrings = await getStrings(requests)\n .then(fetchedStrings => {\n return fetchedStrings;\n })\n .catch(Notification.exception);\n\n return legendStrings;\n }\n\n /**\n * Update the list of join types for a filter.\n *\n * This will update the list of join types based on the allowed types defined for a filter.\n * If only one type is allowed, the list will be hidden.\n *\n * @param {Array} filterJoinList Array of join types, a subset of the regularJoinList array in this function.\n * @param {Element} filterRow The row being updated.\n */\n updateJoinList(filterJoinList, filterRow) {\n const regularJoinList = [0, 1, 2];\n // If a join list was specified for this filter, find the default join list and disable the options that are not allowed\n // for this filter.\n if (filterJoinList.length !== 0) {\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n // Check each option from the default list, and disable the option in this filter row if it is not allowed\n // for this filter.\n regularJoinList.forEach((join) => {\n if (!filterJoinList.includes(join)) {\n joinField.options[join].classList.add('hidden');\n joinField.options[join].disabled = true;\n }\n });\n // Now remove the disabled options, and hide the select list of there is only one option left.\n joinField.options.forEach((element, index) => {\n if (element.disabled) {\n joinField.options[index] = null;\n }\n });\n if (joinField.options.length === 1) {\n joinField.hidden = true;\n }\n }\n }\n}\n"],"names":["constructor","filterSet","applyCallback","activeFilters","courseid","CourseFilter","init","querySelector","Selectors","filterset","region","addEventListener","e","target","closest","actions","addRow","preventDefault","addFilterRow","applyFilters","updateTableFromFilter","resetFilters","removeAllFilters","regions","filterlist","filter","remove","removeOrReplaceFilterRow","filterRegion","this","getFilterRegion","define","CustomEvents","events","accessibleChange","on","typeField","fields","type","value","addFilter","join","dataset","filterverb","filterdata","pendingPromise","Pending","rownum","querySelectorAll","length","Templates","renderForPromise","then","_ref","html","js","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","innerHTML","updateFiltersOptions","result","resolve","filtertype","values","jointype","filteroptions","catch","Notification","exception","getFilterDataSource","filterType","datasource","byName","initialFilterValues","filterJoin","filterOptions","filterDataNode","Filter","GenericFilter","filterTypeClass","error","disabled","updateJoinList","JSON","parse","joinList","joinField","isNaN","getFilterObject","name","refreshContent","replaceFilterRow","removeFilterRow","required","hasFilterValue","removeFilterObject","filterLegends","getAvailableFilterLegends","index","innerText","rowNum","_ref2","replaceNode","filterName","tearDown","removeEmptyFilters","filters","option","classList","add","addRowButton","all","setAttribute","removeAttribute","filtermatch","validate","valid","Object","filterValue","document","reportValidity","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","filterJoinList","regularJoinList","includes","options","element","hidden"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"datafilter.min.js","sources":["../src/datafilter.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 * Data filter management.\n *\n * @module core/datafilter\n * @copyright 2020 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport GenericFilter from 'core/datafilter/filtertype';\nimport {getStrings} from 'core/str';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from 'core/datafilter/selectors';\nimport Templates from 'core/templates';\nimport CustomEvents from 'core/custom_interaction_events';\nimport jQuery from 'jquery';\n\nexport default class {\n\n /**\n * Initialise the filter on the element with the given filterSet and callback.\n *\n * @param {HTMLElement} filterSet The filter element.\n * @param {Function} applyCallback Callback function when updateTableFromFilter\n */\n constructor(filterSet, applyCallback) {\n\n this.filterSet = filterSet;\n this.applyCallback = applyCallback;\n // Keep a reference to all of the active filters.\n this.activeFilters = {};\n }\n\n /**\n * Initialise event listeners to the filter.\n */\n init() {\n // Add listeners for the main actions.\n this.filterSet.querySelector(Selectors.filterset.region).addEventListener('click', e => {\n if (e.target.closest(Selectors.filterset.actions.addRow)) {\n e.preventDefault();\n\n this.addFilterRow();\n }\n\n if (e.target.closest(Selectors.filterset.actions.applyFilters)) {\n e.preventDefault();\n this.updateTableFromFilter();\n }\n\n if (e.target.closest(Selectors.filterset.actions.resetFilters)) {\n e.preventDefault();\n\n this.removeAllFilters();\n }\n });\n\n // Add the listener to remove a single filter.\n this.filterSet.querySelector(Selectors.filterset.regions.filterlist).addEventListener('click', e => {\n if (e.target.closest(Selectors.filter.actions.remove)) {\n e.preventDefault();\n\n this.removeOrReplaceFilterRow(e.target.closest(Selectors.filter.region), true);\n }\n });\n\n // Add listeners for the filter type selection.\n let filterRegion = jQuery(this.getFilterRegion());\n CustomEvents.define(filterRegion, [CustomEvents.events.accessibleChange]);\n filterRegion.on(CustomEvents.events.accessibleChange, e => {\n const typeField = e.target.closest(Selectors.filter.fields.type);\n if (typeField && typeField.value) {\n const filter = e.target.closest(Selectors.filter.region);\n\n this.addFilter(filter, typeField.value);\n }\n });\n\n this.filterSet.querySelector(Selectors.filterset.fields.join).addEventListener('change', e => {\n this.filterSet.dataset.filterverb = e.target.value;\n });\n }\n\n /**\n * Get the filter list region.\n *\n * @return {HTMLElement}\n */\n getFilterRegion() {\n return this.filterSet.querySelector(Selectors.filterset.regions.filterlist);\n }\n\n /**\n * Add a filter row.\n *\n * @param {Object} filterdata Optional, data for adding for row with an existing filter.\n * @return {Promise}\n */\n addFilterRow(filterdata = {}) {\n const pendingPromise = new Pending('core/datafilter:addFilterRow');\n const rownum = filterdata.rownum ?? 1 + this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rownum})\n .then(({html, js}) => {\n const newContentNodes = Templates.appendNodeContents(this.getFilterRegion(), html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(result => {\n pendingPromise.resolve();\n\n // If an existing filter is passed in, add it. Otherwise, leave the row empty.\n if (filterdata.filtertype) {\n result.forEach(filter => {\n this.addFilter(filter, filterdata.filtertype, filterdata.values,\n filterdata.jointype, filterdata.filteroptions);\n });\n }\n return result;\n })\n .catch(Notification.exception);\n }\n\n /**\n * Get the filter data source node fro the specified filter type.\n *\n * @param {String} filterType\n * @return {HTMLElement}\n */\n getFilterDataSource(filterType) {\n const filterDataNode = this.filterSet.querySelector(Selectors.filterset.regions.datasource);\n\n return filterDataNode.querySelector(Selectors.data.fields.byName(filterType));\n }\n\n /**\n * Add a filter to the list of active filters, performing any necessary setup.\n *\n * @param {HTMLElement} filterRow\n * @param {String} filterType\n * @param {Array} initialFilterValues The initially selected values for the filter\n * @param {String} filterJoin\n * @param {Object} filterOptions\n * @returns {Filter}\n */\n async addFilter(filterRow, filterType, initialFilterValues, filterJoin, filterOptions) {\n // Name the filter on the filter row.\n filterRow.dataset.filterType = filterType;\n\n const filterDataNode = this.getFilterDataSource(filterType);\n\n // Instantiate the Filter class.\n let Filter = GenericFilter;\n if (filterDataNode.dataset.filterTypeClass) {\n\n // Ensure the filter class passed through exists, otherwise the filtering will break.\n try {\n Filter = await import(filterDataNode.dataset.filterTypeClass);\n } catch (error) {\n Notification.exception(error);\n }\n\n }\n this.activeFilters[filterType] = new Filter(filterType, this.filterSet, initialFilterValues, filterOptions);\n\n // Disable the select.\n const typeField = filterRow.querySelector(Selectors.filter.fields.type);\n typeField.value = filterType;\n typeField.disabled = 'disabled';\n // Update the join list.\n this.updateJoinList(JSON.parse(filterDataNode.dataset.joinList), filterRow);\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n if (!isNaN(filterJoin)) {\n joinField.value = filterJoin;\n }\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n return this.activeFilters[filterType];\n }\n\n /**\n * Get the registered filter class for the named filter.\n *\n * @param {String} name\n * @return {Object} See the Filter class.\n */\n getFilterObject(name) {\n return this.activeFilters[name];\n }\n\n /**\n * Remove or replace the specified filter row and associated class, ensuring that if there is only one filter row,\n * that it is replaced instead of being removed.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n removeOrReplaceFilterRow(filterRow, refreshContent) {\n const filterCount = this.getFilterRegion().querySelectorAll(Selectors.filter.region).length;\n if (filterCount === 1) {\n this.replaceFilterRow(filterRow, refreshContent);\n } else {\n this.removeFilterRow(filterRow, refreshContent);\n }\n }\n\n /**\n * Remove the specified filter row and associated class.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n */\n async removeFilterRow(filterRow, refreshContent = true) {\n if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n const hasFilterValue = !!filterType.value;\n\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n // Remove the actual filter HTML.\n filterRow.remove();\n\n // Update the list of available filter types.\n this.updateFiltersOptions();\n\n if (hasFilterValue && refreshContent) {\n // Refresh the table if there was any content in this row.\n this.updateTableFromFilter();\n }\n\n // Update filter fieldset legends.\n const filterLegends = await this.getAvailableFilterLegends();\n\n this.getFilterRegion().querySelectorAll(Selectors.filter.region).forEach((filterRow, index) => {\n filterRow.querySelector('legend').innerText = filterLegends[index];\n });\n\n }\n\n /**\n * Replace the specified filter row with a new one.\n *\n * @param {HTMLElement} filterRow\n * @param {Bool} refreshContent Whether to refresh the table content when removing\n * @param {Number} rowNum The number used to label the filter fieldset legend (eg Row 1). Defaults to 1 (the first filter).\n * @return {Promise}\n */\n replaceFilterRow(filterRow, refreshContent = true, rowNum = 1) {\n if (filterRow.querySelector(Selectors.data.required)) {\n return;\n }\n // Remove the filter object.\n this.removeFilterObject(filterRow.dataset.filterType);\n\n return Templates.renderForPromise('core/datafilter/filter_row', {\"rownumber\": rowNum})\n .then(({html, js}) => {\n const newContentNodes = Templates.replaceNode(filterRow, html, js);\n\n return newContentNodes;\n })\n .then(filterRow => {\n // Note: This is a nasty hack.\n // We should try to find a better way of doing this.\n // We do not have the list of types in a readily consumable format, so we take the pre-rendered one and copy\n // it in place.\n const typeList = this.filterSet.querySelector(Selectors.data.typeList);\n\n filterRow.forEach(contentNode => {\n const contentTypeList = contentNode.querySelector(Selectors.filter.fields.type);\n\n if (contentTypeList) {\n contentTypeList.innerHTML = typeList.innerHTML;\n }\n });\n\n return filterRow;\n })\n .then(filterRow => {\n this.updateFiltersOptions();\n\n return filterRow;\n })\n .then(filterRow => {\n // Refresh the table.\n if (refreshContent) {\n return this.updateTableFromFilter();\n } else {\n return filterRow;\n }\n })\n .catch(Notification.exception);\n }\n\n /**\n * Remove the Filter Object from the register.\n *\n * @param {string} filterName The name of the filter to be removed\n */\n removeFilterObject(filterName) {\n if (filterName) {\n const filter = this.getFilterObject(filterName);\n if (filter) {\n filter.tearDown();\n\n // Remove from the list of active filters.\n delete this.activeFilters[filterName];\n }\n }\n }\n\n /**\n * Remove all filters.\n *\n * @returns {Promise}\n */\n removeAllFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => this.removeOrReplaceFilterRow(filterRow, false));\n\n // Refresh the table.\n return this.updateTableFromFilter();\n }\n\n /**\n * Remove any empty filters.\n */\n removeEmptyFilters() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const filterType = filterRow.querySelector(Selectors.filter.fields.type);\n if (!filterType.value) {\n this.removeOrReplaceFilterRow(filterRow, false);\n }\n });\n }\n\n /**\n * Update the list of filter types to filter out those already selected.\n */\n updateFiltersOptions() {\n const filters = this.getFilterRegion().querySelectorAll(Selectors.filter.region);\n filters.forEach(filterRow => {\n const options = filterRow.querySelectorAll(Selectors.filter.fields.type + ' option');\n options.forEach(option => {\n if (option.value === filterRow.dataset.filterType) {\n option.classList.remove('hidden');\n option.disabled = false;\n } else if (this.activeFilters[option.value]) {\n option.classList.add('hidden');\n option.disabled = true;\n } else {\n option.classList.remove('hidden');\n option.disabled = false;\n }\n });\n });\n\n // Configure the state of the \"Add row\" button.\n // This button is disabled when there is a filter row available for each condition.\n const addRowButton = this.filterSet.querySelector(Selectors.filterset.actions.addRow);\n const filterDataNode = this.filterSet.querySelectorAll(Selectors.data.fields.all);\n if (filterDataNode.length <= filters.length) {\n addRowButton.setAttribute('disabled', 'disabled');\n } else {\n addRowButton.removeAttribute('disabled');\n }\n\n if (filters.length === 1) {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.add('hidden');\n this.filterSet.querySelector(Selectors.filterset.fields.join).value = 2;\n this.filterSet.dataset.filterverb = 2;\n } else {\n this.filterSet.querySelector(Selectors.filterset.regions.filtermatch).classList.remove('hidden');\n }\n }\n\n /**\n * Update the Dynamic table based upon the current filter.\n *\n * @param {bool} validate Should we validate the filters? We might want to skip this if the filters won't have changed,\n * for example for pagination/sorting.\n */\n updateTableFromFilter(validate = true) {\n const pendingPromise = new Pending('core/datafilter:updateTableFromFilter');\n\n const filters = {};\n let valid = true;\n Object.values(this.activeFilters).forEach(filter => {\n if (validate) {\n valid = valid && filter.validate();\n }\n filters[filter.filterValue.name] = filter.filterValue;\n });\n if (validate) {\n valid = valid && document.querySelector(Selectors.filter.region).closest('form').reportValidity();\n }\n if (this.applyCallback && valid) {\n this.applyCallback(filters, pendingPromise);\n } else {\n pendingPromise.resolve();\n }\n }\n\n /**\n * Fetch the strings used to populate the fieldset legends for the maximum number of filters possible.\n *\n * @return {array}\n */\n async getAvailableFilterLegends() {\n const maxFilters = document.querySelector(Selectors.data.typeListSelect).length - 1;\n let requests = [];\n\n [...Array(maxFilters)].forEach((_, rowIndex) => {\n requests.push({\n \"key\": \"filterrowlegend\",\n \"component\": \"core\",\n // Add 1 since rows begin at 1 (index begins at zero).\n \"param\": rowIndex + 1\n });\n });\n\n const legendStrings = await getStrings(requests)\n .then(fetchedStrings => {\n return fetchedStrings;\n })\n .catch(Notification.exception);\n\n return legendStrings;\n }\n\n /**\n * Update the list of join types for a filter.\n *\n * This will update the list of join types based on the allowed types defined for a filter.\n * If only one type is allowed, the list will be hidden.\n *\n * @param {Array} filterJoinList Array of join types, a subset of the regularJoinList array in this function.\n * @param {Element} filterRow The row being updated.\n */\n updateJoinList(filterJoinList, filterRow) {\n const regularJoinList = [0, 1, 2];\n // If a join list was specified for this filter, find the default join list and disable the options that are not allowed\n // for this filter.\n if (filterJoinList.length !== 0) {\n const joinField = filterRow.querySelector(Selectors.filter.fields.join);\n // Check each option from the default list, and disable the option in this filter row if it is not allowed\n // for this filter.\n regularJoinList.forEach((join) => {\n if (!filterJoinList.includes(join)) {\n joinField.options[join].classList.add('hidden');\n joinField.options[join].disabled = true;\n }\n });\n // Now remove the disabled options, and hide the select list of there is only one option left.\n joinField.options.forEach((element, index) => {\n if (element.disabled) {\n joinField.options[index] = null;\n }\n });\n if (joinField.options.length === 1) {\n joinField.hidden = true;\n }\n }\n }\n}\n"],"names":["constructor","filterSet","applyCallback","activeFilters","init","querySelector","Selectors","filterset","region","addEventListener","e","target","closest","actions","addRow","preventDefault","addFilterRow","applyFilters","updateTableFromFilter","resetFilters","removeAllFilters","regions","filterlist","filter","remove","removeOrReplaceFilterRow","filterRegion","this","getFilterRegion","define","CustomEvents","events","accessibleChange","on","typeField","fields","type","value","addFilter","join","dataset","filterverb","filterdata","pendingPromise","Pending","rownum","querySelectorAll","length","Templates","renderForPromise","then","_ref","html","js","appendNodeContents","filterRow","typeList","data","forEach","contentNode","contentTypeList","innerHTML","updateFiltersOptions","result","resolve","filtertype","values","jointype","filteroptions","catch","Notification","exception","getFilterDataSource","filterType","datasource","byName","initialFilterValues","filterJoin","filterOptions","filterDataNode","Filter","GenericFilter","filterTypeClass","error","disabled","updateJoinList","JSON","parse","joinList","joinField","isNaN","getFilterObject","name","refreshContent","replaceFilterRow","removeFilterRow","required","hasFilterValue","removeFilterObject","filterLegends","getAvailableFilterLegends","index","innerText","rowNum","_ref2","replaceNode","filterName","tearDown","removeEmptyFilters","filters","option","classList","add","addRowButton","all","setAttribute","removeAttribute","filtermatch","validate","valid","Object","filterValue","document","reportValidity","maxFilters","typeListSelect","requests","Array","_","rowIndex","push","fetchedStrings","filterJoinList","regularJoinList","includes","options","element","hidden"],"mappings":"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"}
\ No newline at end of file
diff --git a/public/lib/amd/build/edit_switch.min.js b/public/lib/amd/build/edit_switch.min.js
index 908684047c153..903b0d5b6868b 100644
--- a/public/lib/amd/build/edit_switch.min.js
+++ b/public/lib/amd/build/edit_switch.min.js
@@ -1,11 +1,10 @@
-define("core/edit_switch",["exports","core/ajax","core/event_dispatcher","core/notification"],(function(_exports,_ajax,_event_dispatcher,_notification){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.eventTypes=void 0;
+define("core/edit_switch",["exports","core/ajax","core/event_dispatcher","core/notification","core/pending"],(function(_exports,_ajax,_event_dispatcher,_notification,_pending){var obj;
/**
* Controls the edit switch.
*
* @module core/edit_switch
* @copyright 2021 Bas Brands
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-const eventTypes={editModeSet:"core/edit_switch/editModeSet"};_exports.eventTypes=eventTypes;const notifyEditModeSet=(container,editMode)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.editModeSet,{editMode:editMode},container,{cancelable:!0});_exports.init=editingSwitchId=>{const editSwitch=document.getElementById(editingSwitchId);editSwitch.addEventListener("change",(()=>{var context,setmode;(context=editSwitch.dataset.context,setmode=editSwitch.checked,(0,_ajax.call)([{methodname:"core_change_editmode",args:{context:context,setmode:setmode}}])[0]).then((result=>{result.success?(editSwitch=>{editSwitch.checked?editSwitch.setAttribute("aria-checked",!0):editSwitch.setAttribute("aria-checked",!1),notifyEditModeSet(editSwitch,editSwitch.checked).defaultPrevented||(editSwitch.setAttribute("disabled",!0),window.location=editSwitch.dataset.pageurl)})(editSwitch):editSwitch.checked=!1})).catch(_notification.exception)}))}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=_exports.eventTypes=void 0,_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const eventTypes={editModeSet:"core/edit_switch/editModeSet"};_exports.eventTypes=eventTypes;const notifyEditModeSet=(container,editMode)=>(0,_event_dispatcher.dispatchEvent)(eventTypes.editModeSet,{editMode:editMode},container,{cancelable:!0});_exports.init=editingSwitchId=>{const editSwitch=document.getElementById(editingSwitchId);editSwitch.addEventListener("change",(()=>{const pendingPromise=new _pending.default("core/edit_switch:toggle");var context,setmode;(context=editSwitch.dataset.context,setmode=editSwitch.checked,(0,_ajax.call)([{methodname:"core_change_editmode",args:{context:context,setmode:setmode}}])[0]).then((result=>{if(result.success){const redirected=(editSwitch=>(editSwitch.checked?editSwitch.setAttribute("aria-checked",!0):editSwitch.setAttribute("aria-checked",!1),!notifyEditModeSet(editSwitch,editSwitch.checked).defaultPrevented&&(editSwitch.setAttribute("disabled",!0),window.location=editSwitch.dataset.pageurl,!0)))(editSwitch);redirected||pendingPromise.resolve()}else editSwitch.checked=!1,pendingPromise.resolve()})).catch((error=>{pendingPromise.resolve(),(0,_notification.exception)(error)}))}))}}));
//# sourceMappingURL=edit_switch.min.js.map
\ No newline at end of file
diff --git a/public/lib/amd/build/edit_switch.min.js.map b/public/lib/amd/build/edit_switch.min.js.map
index ac217b8891133..c5b0c449002a7 100644
--- a/public/lib/amd/build/edit_switch.min.js.map
+++ b/public/lib/amd/build/edit_switch.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"edit_switch.min.js","sources":["../src/edit_switch.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 * Controls the edit switch.\n *\n * @module core/edit_switch\n * @copyright 2021 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {exception as displayException} from 'core/notification';\n\n/**\n * Change the Edit mode.\n *\n * @param {number} context The contextid that editing is being set for\n * @param {bool} setmode Whether editing is set or not\n * @return {Promise} Resolved with an array file the stored file url.\n */\nconst setEditMode = (context, setmode) => fetchMany([{\n methodname: 'core_change_editmode',\n args: {\n context,\n setmode,\n },\n}])[0];\n\n/**\n * Toggle the edit switch\n *\n * @method\n * @protected\n * @param {HTMLElement} editSwitch\n */\nconst toggleEditSwitch = editSwitch => {\n if (editSwitch.checked) {\n editSwitch.setAttribute('aria-checked', true);\n } else {\n editSwitch.setAttribute('aria-checked', false);\n }\n\n const event = notifyEditModeSet(editSwitch, editSwitch.checked);\n if (!event.defaultPrevented) {\n editSwitch.setAttribute('disabled', true);\n window.location = editSwitch.dataset.pageurl;\n }\n};\n\n/**\n * Names of events for core/edit_switch.\n *\n * @static\n * @property {String} editModeSet See {@link event:core/edit_switch/editModeSet}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the edit mode toggled.\n *\n * @event core/edit_switch/editModeSet\n * @type {CustomEvent}\n * @property {HTMLElement} target The switch used to toggle the edit mode\n * @property {object} detail\n * @property {bool} detail.editMode\n */\n editModeSet: 'core/edit_switch/editModeSet',\n};\n\n/**\n * Dispatch the editModeSet event after changing the edit mode.\n *\n * This event is cancelable.\n *\n * The default action is to reload the page after toggling the edit mode.\n *\n * @method\n * @protected\n * @param {HTMLElement} container\n * @param {bool} editMode\n * @returns {CustomEvent}\n */\nconst notifyEditModeSet = (container, editMode) => dispatchEvent(\n eventTypes.editModeSet,\n {editMode},\n container,\n {cancelable: true}\n);\n\n/**\n * Add the eventlistener for the editswitch.\n *\n * @param {string} editingSwitchId The id of the editing switch to listen for\n */\nexport const init = editingSwitchId => {\n const editSwitch = document.getElementById(editingSwitchId);\n editSwitch.addEventListener('change', () => {\n setEditMode(editSwitch.dataset.context, editSwitch.checked)\n .then(result => {\n if (result.success) {\n toggleEditSwitch(editSwitch);\n } else {\n editSwitch.checked = false;\n }\n return;\n })\n .catch(displayException);\n });\n};\n"],"names":["eventTypes","editModeSet","notifyEditModeSet","container","editMode","cancelable","editingSwitchId","editSwitch","document","getElementById","addEventListener","context","setmode","dataset","checked","methodname","args","then","result","success","setAttribute","defaultPrevented","window","location","pageurl","toggleEditSwitch","catch","displayException"],"mappings":";;;;;;;;MAqEaA,WAAa,CAUtBC,YAAa,qEAgBXC,kBAAoB,CAACC,UAAWC,YAAa,mCAC/CJ,WAAWC,YACX,CAACG,SAAAA,UACDD,UACA,CAACE,YAAY,kBAQGC,wBACVC,WAAaC,SAASC,eAAeH,iBAC3CC,WAAWG,iBAAiB,UAAU,KA3EtB,IAACC,QAASC,SAATD,QA4EDJ,WAAWM,QAAQF,QA5ETC,QA4EkBL,WAAWO,SA5EjB,cAAU,CAAC,CACjDC,WAAY,uBACZC,KAAM,CACFL,QAAAA,QACAC,QAAAA,YAEJ,IAuEKK,MAAKC,SACEA,OAAOC,QA/DEZ,CAAAA,aACjBA,WAAWO,QACXP,WAAWa,aAAa,gBAAgB,GAExCb,WAAWa,aAAa,gBAAgB,GAG9BlB,kBAAkBK,WAAYA,WAAWO,SAC5CO,mBACPd,WAAWa,aAAa,YAAY,GACpCE,OAAOC,SAAWhB,WAAWM,QAAQW,UAsD7BC,CAAiBlB,YAEjBA,WAAWO,SAAU,KAI5BY,MAAMC"}
\ No newline at end of file
+{"version":3,"file":"edit_switch.min.js","sources":["../src/edit_switch.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 * Controls the edit switch.\n *\n * @module core/edit_switch\n * @copyright 2021 Bas Brands \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {dispatchEvent} from 'core/event_dispatcher';\nimport {exception as displayException} from 'core/notification';\nimport Pending from \"core/pending\";\n\n/**\n * Change the Edit mode.\n *\n * @param {number} context The contextid that editing is being set for\n * @param {bool} setmode Whether editing is set or not\n * @return {Promise} Resolved with an array file the stored file url.\n */\nconst setEditMode = (context, setmode) => fetchMany([{\n methodname: 'core_change_editmode',\n args: {\n context,\n setmode,\n },\n}])[0];\n\n/**\n * Toggle the edit switch\n *\n * @method\n * @protected\n * @param {HTMLElement} editSwitch\n */\nconst toggleEditSwitch = editSwitch => {\n if (editSwitch.checked) {\n editSwitch.setAttribute('aria-checked', true);\n } else {\n editSwitch.setAttribute('aria-checked', false);\n }\n\n const event = notifyEditModeSet(editSwitch, editSwitch.checked);\n if (!event.defaultPrevented) {\n editSwitch.setAttribute('disabled', true);\n window.location = editSwitch.dataset.pageurl;\n return true;\n }\n return false;\n};\n\n/**\n * Names of events for core/edit_switch.\n *\n * @static\n * @property {String} editModeSet See {@link event:core/edit_switch/editModeSet}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the edit mode toggled.\n *\n * @event core/edit_switch/editModeSet\n * @type {CustomEvent}\n * @property {HTMLElement} target The switch used to toggle the edit mode\n * @property {object} detail\n * @property {bool} detail.editMode\n */\n editModeSet: 'core/edit_switch/editModeSet',\n};\n\n/**\n * Dispatch the editModeSet event after changing the edit mode.\n *\n * This event is cancelable.\n *\n * The default action is to reload the page after toggling the edit mode.\n *\n * @method\n * @protected\n * @param {HTMLElement} container\n * @param {bool} editMode\n * @returns {CustomEvent}\n */\nconst notifyEditModeSet = (container, editMode) => dispatchEvent(\n eventTypes.editModeSet,\n {editMode},\n container,\n {cancelable: true}\n);\n\n/**\n * Add the eventlistener for the editswitch.\n *\n * @param {string} editingSwitchId The id of the editing switch to listen for\n */\nexport const init = editingSwitchId => {\n const editSwitch = document.getElementById(editingSwitchId);\n editSwitch.addEventListener('change', () => {\n const pendingPromise = new Pending(\"core/edit_switch:toggle\");\n setEditMode(editSwitch.dataset.context, editSwitch.checked)\n .then((result) => {\n if (result.success) {\n const redirected = toggleEditSwitch(editSwitch);\n if (!redirected) {\n pendingPromise.resolve();\n }\n } else {\n editSwitch.checked = false;\n pendingPromise.resolve();\n }\n return;\n })\n .catch((error) => {\n pendingPromise.resolve();\n displayException(error);\n });\n });\n};\n"],"names":["eventTypes","editModeSet","notifyEditModeSet","container","editMode","cancelable","editingSwitchId","editSwitch","document","getElementById","addEventListener","pendingPromise","Pending","context","setmode","dataset","checked","methodname","args","then","result","success","redirected","setAttribute","defaultPrevented","window","location","pageurl","toggleEditSwitch","resolve","catch","error"],"mappings":";;;;;;;sKAwEaA,WAAa,CAUtBC,YAAa,qEAgBXC,kBAAoB,CAACC,UAAWC,YAAa,mCAC/CJ,WAAWC,YACX,CAACG,SAAAA,UACDD,UACA,CAACE,YAAY,kBAQGC,wBACVC,WAAaC,SAASC,eAAeH,iBAC3CC,WAAWG,iBAAiB,UAAU,WAC5BC,eAAiB,IAAIC,iBAAQ,2BA9EvB,IAACC,QAASC,SAATD,QA+EDN,WAAWQ,QAAQF,QA/ETC,QA+EkBP,WAAWS,SA/EjB,cAAU,CAAC,CACjDC,WAAY,uBACZC,KAAM,CACFL,QAAAA,QACAC,QAAAA,YAEJ,IA0EKK,MAAMC,YACCA,OAAOC,QAAS,OACVC,WAnEGf,CAAAA,aACjBA,WAAWS,QACXT,WAAWgB,aAAa,gBAAgB,GAExChB,WAAWgB,aAAa,gBAAgB,IAG9BrB,kBAAkBK,WAAYA,WAAWS,SAC5CQ,mBACPjB,WAAWgB,aAAa,YAAY,GACpCE,OAAOC,SAAWnB,WAAWQ,QAAQY,SAC9B,IAwDoBC,CAAiBrB,YAC/Be,YACDX,eAAekB,eAGnBtB,WAAWS,SAAU,EACrBL,eAAekB,aAItBC,OAAOC,QACJpB,eAAekB,sCACEE"}
\ No newline at end of file
diff --git a/public/lib/amd/build/local/action_menu/subpanel.min.js b/public/lib/amd/build/local/action_menu/subpanel.min.js
index ba7954565d1f3..c321ae8b91d9d 100644
--- a/public/lib/amd/build/local/action_menu/subpanel.min.js
+++ b/public/lib/amd/build/local/action_menu/subpanel.min.js
@@ -5,6 +5,6 @@ define("core/local/action_menu/subpanel",["exports","core/utils","core/pagehelpe
* @module core/local/action_menu/subpanel
* @copyright 2023 Mikel Martín
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),_eventHandler=_interopRequireDefault(_eventHandler);const Selectors_mainMenu='[role="menu"]',Selectors_dropdownRight=".dropdown-menu-end",Selectors_subPanel=".dropdown-subpanel",Selectors_subPanelMenuItem=".dropdown-subpanel > .dropdown-item",Selectors_subPanelContent=".dropdown-subpanel > .dropdown-menu",Selectors_drawer='[data-region="fixed-drawer"]',Selectors_blockColumn=".blockcolumn",Selectors_columnLeft=".columnleft",Classes_dropRight="dropend",Classes_dropLeft="dropstart",Classes_dropDown="dropdown",Classes_forceLeft="downleft",Classes_contentDisplayed="content-displayed",BootstrapEvents_hideDropdown="hidden.bs.dropdown";let initialized=!1;const updateAllPanelsPosition=()=>{document.querySelectorAll(Selectors_subPanel).forEach((dropdown=>{new SubPanel(dropdown).updatePosition()}))};class SubPanel{constructor(element){this.element=element,this.menuItem=element.querySelector(Selectors_subPanelMenuItem),this.panelContent=element.querySelector(Selectors_subPanelContent),this.showPreviewOnFocus=!0}init(){if(this.element.dataset.subPanelInitialized)return;this.updatePosition(),this.element.addEventListener("focusin",this._mainElementFocusInHandler.bind(this)),this.menuItem.addEventListener("click",this._menuItemClickHandler.bind(this));const subpanelMenuItemSelector="#".concat(this.element.id).concat(Selectors_subPanelMenuItem);_eventHandler.default.on(document,"keydown",subpanelMenuItemSelector,this._menuItemKeyHandler.bind(this)),(0,_pagehelpers.isBehatSite)()||(this.menuItem.addEventListener("mouseover",this._menuItemHoverHandler.bind(this)),this.menuItem.addEventListener("mouseout",this._menuItemHoverOutHandler.bind(this))),this.panelContent.addEventListener("keydown",this._panelContentKeyHandler.bind(this)),this.element.dataset.subPanelInitialized=!0}_needSmallSpaceBehaviour(){return(0,_pagehelpers.isExtraSmall)()||null!==this.element.closest(Selectors_drawer)||null!==this.element.closest(Selectors_blockColumn)}_needDropdownRight(){return null===this.element.closest(Selectors_columnLeft)&&null!==this.element.closest(Selectors_dropdownRight)}_mainElementFocusInHandler(){!this._needSmallSpaceBehaviour()&&this.showPreviewOnFocus?this.setVisibility(!0):this.showPreviewOnFocus=!0}_menuItemClickHandler(event){event.stopPropagation(),event.preventDefault(),this._needSmallSpaceBehaviour()&&this.setVisibility(!this.getVisibility())}_menuItemHoverHandler(){this._needSmallSpaceBehaviour()||this.setVisibility(!0)}_menuItemHoverOutHandler(){this._needSmallSpaceBehaviour()||this._hideOtherSubPanels()}_menuItemKeyHandler(event){if("ArrowUp"===event.key||"ArrowDown"===event.key&&!this._needSmallSpaceBehaviour())return void this.setVisibility(!1);let focusPanel=!1;("ArrowRight"===event.key||"ArrowLeft"===event.key||"Tab"===event.key&&!event.shiftKey)&&(focusPanel=!0),"Enter"!==event.key&&" "!==event.key||(focusPanel=!0),"ArrowDown"===event.key&&this._needSmallSpaceBehaviour()&&this.getVisibility()&&(focusPanel=!0),focusPanel&&(event.stopPropagation(),event.preventDefault(),this.setVisibility(!0),this._focusPanelContent())}_panelContentKeyHandler(event){const canLoop=!this._needSmallSpaceBehaviour();let isBrowsingSubPanel=!1,newFocus=null;"ArrowRight"!==event.key&&"ArrowLeft"!==event.key||(newFocus=this.menuItem),("Escape"===event.key||"Tab"===event.key&&event.shiftKey)&&(newFocus=this.menuItem,this.setVisibility(!1),this.showPreviewOnFocus=!1),"ArrowUp"===event.key&&(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"ArrowDown"===event.key&&(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"Home"===event.key&&(newFocus=(0,_pagehelpers.firstFocusableElement)(this.panelContent),isBrowsingSubPanel=!0),"End"===event.key&&(newFocus=(0,_pagehelpers.lastFocusableElement)(this.panelContent),isBrowsingSubPanel=!0),null===newFocus&&isBrowsingSubPanel&&!canLoop&&(newFocus=this.menuItem),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/action_menu/subpanel:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panelContent);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisibility(visible){visible&&this._hideOtherSubPanels(),!visible&&this.getVisibility&&(0,_aria.hide)(this.panelContent),visible&&!this.getVisibility&&(0,_aria.unhide)(this.panelContent),this.menuItem.setAttribute("aria-expanded",visible?"true":"false"),this.panelContent.classList.toggle("show",visible),this.element.classList.toggle(Classes_contentDisplayed,visible)}_hideOtherSubPanels(){this.element.closest(Selectors_mainMenu).querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);if(dropdownSubPanel===this.element)return;new SubPanel(dropdownSubPanel).setVisibility(!1)}))}getVisibility(){return"true"===this.menuItem.getAttribute("aria-expanded")}updatePosition(){const dropdownRight=this._needDropdownRight();if(this._needSmallSpaceBehaviour())return this.element.classList.remove(Classes_dropRight),this.element.classList.remove(Classes_dropLeft),this.element.classList.add(Classes_dropDown),void this.element.classList.toggle(Classes_forceLeft,dropdownRight);this.element.classList.remove(Classes_dropDown),this.element.classList.remove(Classes_forceLeft),this.element.classList.toggle(Classes_dropRight,!dropdownRight),this.element.classList.toggle(Classes_dropLeft,dropdownRight)}}_exports.init=selector=>{initialized||(document.addEventListener(BootstrapEvents_hideDropdown,(()=>{document.querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);new SubPanel(dropdownSubPanel).setVisibility(!1)}))})),window.addEventListener("resize",(0,_utils.debounce)(updateAllPanelsPosition,400)),initialized=!0);const subMenu=document.querySelector(selector);if(!subMenu)throw new Error("Sub panel element not found: ".concat(selector));new SubPanel(subMenu).init()}}));
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),_eventHandler=_interopRequireDefault(_eventHandler);const Selectors_mainMenu='[role="menu"]',Selectors_dropdownRight=".dropdown-menu-end",Selectors_subPanel=".dropdown-subpanel",Selectors_subPanelMenuItem=".dropdown-subpanel > .dropdown-item",Selectors_subPanelContent=".dropdown-subpanel > .dropdown-menu",Selectors_drawer='[data-region="fixed-drawer"]',Selectors_blockColumn=".blockcolumn",Selectors_columnLeft=".columnleft",Classes_dropRight="dropend",Classes_dropLeft="dropstart",Classes_dropDown="dropdown",Classes_forceLeft="downleft",Classes_contentDisplayed="content-displayed",BootstrapEvents_hideDropdown="hidden.bs.dropdown";let initialized=!1;const updateAllPanelsPosition=()=>{document.querySelectorAll(Selectors_subPanel).forEach((dropdown=>{new SubPanel(dropdown).updatePosition()}))};class SubPanel{constructor(element){this.element=element,this.menuItem=element.querySelector(Selectors_subPanelMenuItem),this.panelContent=element.querySelector(Selectors_subPanelContent),this.showPreviewOnFocus=!0}init(){if(this.element.dataset.subPanelInitialized)return;this.updatePosition(),this.element.addEventListener("focusin",this._mainElementFocusInHandler.bind(this)),this.menuItem.addEventListener("click",this._menuItemClickHandler.bind(this));const subpanelMenuItemSelector="#".concat(this.element.id).concat(Selectors_subPanelMenuItem);_eventHandler.default.on(document,"keydown",subpanelMenuItemSelector,this._menuItemKeyHandler.bind(this)),(0,_pagehelpers.isBehatSite)()||(this.menuItem.addEventListener("mouseover",this._menuItemHoverHandler.bind(this)),this.menuItem.addEventListener("mouseout",this._menuItemHoverOutHandler.bind(this))),this.panelContent.addEventListener("keydown",this._panelContentKeyHandler.bind(this)),this.element.dataset.subPanelInitialized=!0}_hideCurrentSubPanel(event){const related=event.relatedTarget;this.menuItem.contains(related)||this.panelContent.contains(related)||this.setVisibility(!1)}_needSmallSpaceBehaviour(){return(0,_pagehelpers.isExtraSmall)()||null!==this.element.closest(Selectors_drawer)||null!==this.element.closest(Selectors_blockColumn)}_needDropdownRight(){return null===this.element.closest(Selectors_columnLeft)&&null!==this.element.closest(Selectors_dropdownRight)}_mainElementFocusInHandler(){!this._needSmallSpaceBehaviour()&&this.showPreviewOnFocus?this.setVisibility(!0):this.showPreviewOnFocus=!0}_menuItemClickHandler(event){event.stopPropagation(),event.preventDefault(),this._needSmallSpaceBehaviour()&&this.setVisibility(!this.getVisibility())}_menuItemHoverHandler(){this._needSmallSpaceBehaviour()||this.setVisibility(!0)}_menuItemHoverOutHandler(event){this._needSmallSpaceBehaviour()||(this._hideOtherSubPanels(),this._hideCurrentSubPanel(event))}_menuItemKeyHandler(event){if("ArrowUp"===event.key||"ArrowDown"===event.key&&!this._needSmallSpaceBehaviour())return void this.setVisibility(!1);let focusPanel=!1;("ArrowRight"===event.key||"ArrowLeft"===event.key||"Tab"===event.key&&!event.shiftKey)&&(focusPanel=!0),"Enter"!==event.key&&" "!==event.key||(focusPanel=!0),"ArrowDown"===event.key&&this._needSmallSpaceBehaviour()&&this.getVisibility()&&(focusPanel=!0),focusPanel&&(event.stopPropagation(),event.preventDefault(),this.setVisibility(!0),this._focusPanelContent())}_panelContentKeyHandler(event){const canLoop=!this._needSmallSpaceBehaviour();let isBrowsingSubPanel=!1,newFocus=null;"ArrowRight"!==event.key&&"ArrowLeft"!==event.key||(newFocus=this.menuItem),("Escape"===event.key||"Tab"===event.key&&event.shiftKey)&&(newFocus=this.menuItem,this.setVisibility(!1),this.showPreviewOnFocus=!1),"ArrowUp"===event.key&&(newFocus=(0,_pagehelpers.previousFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"ArrowDown"===event.key&&(newFocus=(0,_pagehelpers.nextFocusableElement)(this.panelContent,canLoop),isBrowsingSubPanel=!0),"Home"===event.key&&(newFocus=(0,_pagehelpers.firstFocusableElement)(this.panelContent),isBrowsingSubPanel=!0),"End"===event.key&&(newFocus=(0,_pagehelpers.lastFocusableElement)(this.panelContent),isBrowsingSubPanel=!0),null===newFocus&&isBrowsingSubPanel&&!canLoop&&(newFocus=this.menuItem),null!==newFocus&&(event.stopPropagation(),event.preventDefault(),newFocus.focus())}_focusPanelContent(){const pendingPromise=new _pending.default("core/action_menu/subpanel:focuscontent");setTimeout((()=>{const firstFocusable=(0,_pagehelpers.firstFocusableElement)(this.panelContent);firstFocusable&&firstFocusable.focus(),pendingPromise.resolve()}),100)}setVisibility(visible){visible&&this._hideOtherSubPanels(),!visible&&this.getVisibility&&(0,_aria.hide)(this.panelContent),visible&&!this.getVisibility&&(0,_aria.unhide)(this.panelContent),this.menuItem.setAttribute("aria-expanded",visible?"true":"false"),this.panelContent.classList.toggle("show",visible),this.element.classList.toggle(Classes_contentDisplayed,visible)}_hideOtherSubPanels(){this.element.closest(Selectors_mainMenu).querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);if(dropdownSubPanel===this.element)return;new SubPanel(dropdownSubPanel).setVisibility(!1)}))}getVisibility(){return"true"===this.menuItem.getAttribute("aria-expanded")}updatePosition(){const dropdownRight=this._needDropdownRight();if(this._needSmallSpaceBehaviour())return this.element.classList.remove(Classes_dropRight),this.element.classList.remove(Classes_dropLeft),this.element.classList.add(Classes_dropDown),void this.element.classList.toggle(Classes_forceLeft,dropdownRight);this.element.classList.remove(Classes_dropDown),this.element.classList.remove(Classes_forceLeft),this.element.classList.toggle(Classes_dropRight,!dropdownRight),this.element.classList.toggle(Classes_dropLeft,dropdownRight)}}_exports.init=selector=>{initialized||(document.addEventListener(BootstrapEvents_hideDropdown,(()=>{document.querySelectorAll("".concat(Selectors_subPanelContent,".show")).forEach((visibleSubPanel=>{const dropdownSubPanel=visibleSubPanel.closest(Selectors_subPanel);new SubPanel(dropdownSubPanel).setVisibility(!1)}))})),window.addEventListener("resize",(0,_utils.debounce)(updateAllPanelsPosition,400)),initialized=!0);const subMenu=document.querySelector(selector);if(!subMenu)throw new Error("Sub panel element not found: ".concat(selector));new SubPanel(subMenu).init()}}));
//# sourceMappingURL=subpanel.min.js.map
\ No newline at end of file
diff --git a/public/lib/amd/build/local/action_menu/subpanel.min.js.map b/public/lib/amd/build/local/action_menu/subpanel.min.js.map
index ff14def10ef67..1b03aad78534e 100644
--- a/public/lib/amd/build/local/action_menu/subpanel.min.js.map
+++ b/public/lib/amd/build/local/action_menu/subpanel.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"subpanel.min.js","sources":["../../../src/local/action_menu/subpanel.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 * Action menu subpanel JS controls.\n *\n * @module core/local/action_menu/subpanel\n * @copyright 2023 Mikel Martín \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {debounce} from 'core/utils';\nimport {\n isBehatSite,\n isExtraSmall,\n firstFocusableElement,\n lastFocusableElement,\n previousFocusableElement,\n nextFocusableElement,\n} from 'core/pagehelpers';\nimport Pending from 'core/pending';\nimport {\n hide,\n unhide,\n} from 'core/aria';\nimport EventHandler from 'theme_boost/bootstrap/dom/event-handler';\n\nconst Selectors = {\n mainMenu: '[role=\"menu\"]',\n dropdownRight: '.dropdown-menu-end',\n subPanel: '.dropdown-subpanel',\n subPanelMenuItem: '.dropdown-subpanel > .dropdown-item',\n subPanelContent: '.dropdown-subpanel > .dropdown-menu',\n // Drawer selector.\n drawer: '[data-region=\"fixed-drawer\"]',\n // Lateral blocks columns selectors.\n blockColumn: '.blockcolumn',\n columnLeft: '.columnleft',\n};\n\nconst Classes = {\n dropRight: 'dropend',\n dropLeft: 'dropstart',\n dropDown: 'dropdown',\n forceLeft: 'downleft',\n contentDisplayed: 'content-displayed',\n};\n\nconst BootstrapEvents = {\n hideDropdown: 'hidden.bs.dropdown',\n};\n\nlet initialized = false;\n\n/**\n * Initialize all delegated events into the page.\n */\nconst initPageEvents = () => {\n if (initialized) {\n return;\n }\n // Hide all subpanels when hiding a dropdown.\n document.addEventListener(BootstrapEvents.hideDropdown, () => {\n document.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n const subPanel = new SubPanel(dropdownSubPanel);\n subPanel.setVisibility(false);\n });\n });\n\n window.addEventListener('resize', debounce(updateAllPanelsPosition, 400));\n\n initialized = true;\n};\n\n/**\n * Update all the panels position.\n */\nconst updateAllPanelsPosition = () => {\n document.querySelectorAll(Selectors.subPanel).forEach(dropdown => {\n const subpanel = new SubPanel(dropdown);\n subpanel.updatePosition();\n });\n};\n\n/**\n * Subpanel class.\n * @private\n */\nclass SubPanel {\n /**\n * Constructor.\n * @param {HTMLElement} element The element to initialize.\n */\n constructor(element) {\n this.element = element;\n this.menuItem = element.querySelector(Selectors.subPanelMenuItem);\n this.panelContent = element.querySelector(Selectors.subPanelContent);\n /**\n * Enable preview when the menu item has focus.\n *\n * This is disabled when the user press ESC or shift+TAB to force closing\n *\n * @type {Boolean}\n * @private\n */\n this.showPreviewOnFocus = true;\n }\n\n /**\n * Initialize the subpanel element.\n *\n * This method adds the event listeners to the subpanel and the position classes.\n */\n init() {\n if (this.element.dataset.subPanelInitialized) {\n return;\n }\n\n this.updatePosition();\n\n // Full element events.\n this.element.addEventListener('focusin', this._mainElementFocusInHandler.bind(this));\n // Menu Item events.\n this.menuItem.addEventListener('click', this._menuItemClickHandler.bind(this));\n // Use the Bootstrap key handler for the menu item key handler.\n // This will avoid Boostrap Dropdown handler to prevent the propagation to the subpanel.\n const subpanelMenuItemSelector = `#${this.element.id}${Selectors.subPanelMenuItem}`;\n EventHandler.on(document, 'keydown', subpanelMenuItemSelector, this._menuItemKeyHandler.bind(this));\n if (!isBehatSite()) {\n // Behat in Chrome usually move the mouse over the page when trying clicking a subpanel element.\n // If the menu has more than one subpanel this could cause closing the subpanel by mistake.\n this.menuItem.addEventListener('mouseover', this._menuItemHoverHandler.bind(this));\n this.menuItem.addEventListener('mouseout', this._menuItemHoverOutHandler.bind(this));\n }\n // Subpanel content events.\n this.panelContent.addEventListener('keydown', this._panelContentKeyHandler.bind(this));\n\n this.element.dataset.subPanelInitialized = true;\n }\n\n /**\n * Checks if the subpanel has enough space.\n *\n * In general there are two scenarios were the subpanel must be interacted differently:\n * - Extra small screens: The subpanel is displayed below the menu item.\n * - Drawer: The subpanel is displayed one of the drawers.\n * - Block columns: for classic based themes.\n *\n * @returns {Boolean} true if the subpanel should be displayed in small screens.\n */\n _needSmallSpaceBehaviour() {\n return isExtraSmall() ||\n this.element.closest(Selectors.drawer) !== null ||\n this.element.closest(Selectors.blockColumn) !== null;\n }\n\n /**\n * Check if the subpanel should be displayed on the right.\n *\n * This is defined by the drop right boostrap class. However, if the menu is\n * displayed in a block column on the right, the subpanel should be forced\n * to the right.\n *\n * @returns {Boolean} true if the subpanel should be displayed on the right.\n */\n _needDropdownRight() {\n if (this.element.closest(Selectors.columnLeft) !== null) {\n return false;\n }\n return this.element.closest(Selectors.dropdownRight) !== null;\n }\n\n /**\n * Main element focus in handler.\n */\n _mainElementFocusInHandler() {\n if (this._needSmallSpaceBehaviour() || !this.showPreviewOnFocus) {\n // Preview is disabled when the user press ESC or shift+TAB to force closing\n // but if the continue navigating with keyboard the preview is enabled again.\n this.showPreviewOnFocus = true;\n return;\n }\n this.setVisibility(true);\n }\n\n /**\n * Menu item click handler.\n * @param {Event} event\n */\n _menuItemClickHandler(event) {\n // Avoid dropdowns being closed after clicking a subemnu.\n // This won't be needed with BS5 (data-bs-auto-close handles it).\n event.stopPropagation();\n event.preventDefault();\n if (this._needSmallSpaceBehaviour()) {\n this.setVisibility(!this.getVisibility());\n }\n }\n\n /**\n * Menu item hover handler.\n * @private\n */\n _menuItemHoverHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this.setVisibility(true);\n }\n\n /**\n * Menu item hover out handler.\n * @private\n */\n _menuItemHoverOutHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this._hideOtherSubPanels();\n }\n\n /**\n * Menu item key handler.\n * @param {Event} event\n * @private\n */\n _menuItemKeyHandler(event) {\n // In small sizes te down key will focus on the panel.\n if (event.key === 'ArrowUp' || (event.key === 'ArrowDown' && !this._needSmallSpaceBehaviour())) {\n this.setVisibility(false);\n return;\n }\n\n // Keys to move focus to the panel.\n let focusPanel = false;\n\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft' || (event.key === 'Tab' && !event.shiftKey)) {\n focusPanel = true;\n }\n if ((event.key === 'Enter' || event.key === ' ')) {\n focusPanel = true;\n }\n // In extra small screen the panel is shown below the item.\n if (event.key === 'ArrowDown' && this._needSmallSpaceBehaviour() && this.getVisibility()) {\n focusPanel = true;\n }\n if (focusPanel) {\n event.stopPropagation();\n event.preventDefault();\n this.setVisibility(true);\n this._focusPanelContent();\n }\n\n }\n\n /**\n * Sub panel content key handler.\n * @param {Event} event\n * @private\n */\n _panelContentKeyHandler(event) {\n // In extra small devices the panel is displayed under the menu item\n // so the arrow up/down switch between subpanel and the menu item.\n const canLoop = !this._needSmallSpaceBehaviour();\n let isBrowsingSubPanel = false;\n let newFocus = null;\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n newFocus = this.menuItem;\n }\n // Acording to WCAG Esc and Tab are similar to arrow navigation but they\n // force the subpanel to be closed.\n if (event.key === 'Escape' || (event.key === 'Tab' && event.shiftKey)) {\n newFocus = this.menuItem;\n this.setVisibility(false);\n this.showPreviewOnFocus = false;\n }\n if (event.key === 'ArrowUp') {\n newFocus = previousFocusableElement(this.panelContent, canLoop);\n isBrowsingSubPanel = true;\n }\n if (event.key === 'ArrowDown') {\n newFocus = nextFocusableElement(this.panelContent, canLoop);\n isBrowsingSubPanel = true;\n }\n if (event.key === 'Home') {\n newFocus = firstFocusableElement(this.panelContent);\n isBrowsingSubPanel = true;\n }\n if (event.key === 'End') {\n newFocus = lastFocusableElement(this.panelContent);\n isBrowsingSubPanel = true;\n }\n // If the user cannot loop and arrive to the start/end of the subpanel\n // we focus on the menu item.\n if (newFocus === null && isBrowsingSubPanel && !canLoop) {\n newFocus = this.menuItem;\n }\n if (newFocus !== null) {\n event.stopPropagation();\n event.preventDefault();\n newFocus.focus();\n }\n }\n\n /**\n * Focus on the first focusable element of the subpanel.\n * @private\n */\n _focusPanelContent() {\n const pendingPromise = new Pending('core/action_menu/subpanel:focuscontent');\n // Some Bootstrap events are triggered after the click event.\n // To prevent this from affecting the focus we wait a bit.\n setTimeout(() => {\n const firstFocusable = firstFocusableElement(this.panelContent);\n if (firstFocusable) {\n firstFocusable.focus();\n }\n pendingPromise.resolve();\n }, 100);\n }\n\n /**\n * Set the visibility of a subpanel.\n * @param {Boolean} visible true if the subpanel should be visible.\n */\n setVisibility(visible) {\n if (visible) {\n this._hideOtherSubPanels();\n }\n // Aria hidden/unhidden can alter the focus, we only want to do it when needed.\n if (!visible && this.getVisibility) {\n hide(this.panelContent);\n }\n if (visible && !this.getVisibility) {\n unhide(this.panelContent);\n }\n this.menuItem.setAttribute('aria-expanded', visible ? 'true' : 'false');\n this.panelContent.classList.toggle('show', visible);\n this.element.classList.toggle(Classes.contentDisplayed, visible);\n }\n\n /**\n * Hide all other subpanels in the parent menu.\n * @private\n */\n _hideOtherSubPanels() {\n const dropdown = this.element.closest(Selectors.mainMenu);\n dropdown.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n if (dropdownSubPanel === this.element) {\n return;\n }\n const subPanel = new SubPanel(dropdownSubPanel);\n subPanel.setVisibility(false);\n });\n }\n\n /**\n * Get the visibility of a subpanel.\n * @returns {Boolean} true if the subpanel is visible.\n */\n getVisibility() {\n return this.menuItem.getAttribute('aria-expanded') === 'true';\n }\n\n /**\n * Update the panels position depending on the screen size and panel position.\n */\n updatePosition() {\n const dropdownRight = this._needDropdownRight();\n if (this._needSmallSpaceBehaviour()) {\n this.element.classList.remove(Classes.dropRight);\n this.element.classList.remove(Classes.dropLeft);\n this.element.classList.add(Classes.dropDown);\n this.element.classList.toggle(Classes.forceLeft, dropdownRight);\n return;\n }\n this.element.classList.remove(Classes.dropDown);\n this.element.classList.remove(Classes.forceLeft);\n this.element.classList.toggle(Classes.dropRight, !dropdownRight);\n this.element.classList.toggle(Classes.dropLeft, dropdownRight);\n }\n}\n\n/**\n * Initialise module for given report\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n initPageEvents();\n const subMenu = document.querySelector(selector);\n if (!subMenu) {\n throw new Error(`Sub panel element not found: ${selector}`);\n }\n const subPanel = new SubPanel(subMenu);\n subPanel.init();\n};\n"],"names":["Selectors","Classes","BootstrapEvents","initialized","updateAllPanelsPosition","document","querySelectorAll","forEach","dropdown","SubPanel","updatePosition","constructor","element","menuItem","querySelector","panelContent","showPreviewOnFocus","init","this","dataset","subPanelInitialized","addEventListener","_mainElementFocusInHandler","bind","_menuItemClickHandler","subpanelMenuItemSelector","id","on","_menuItemKeyHandler","_menuItemHoverHandler","_menuItemHoverOutHandler","_panelContentKeyHandler","_needSmallSpaceBehaviour","closest","_needDropdownRight","setVisibility","event","stopPropagation","preventDefault","getVisibility","_hideOtherSubPanels","key","focusPanel","shiftKey","_focusPanelContent","canLoop","isBrowsingSubPanel","newFocus","focus","pendingPromise","Pending","setTimeout","firstFocusable","resolve","visible","setAttribute","classList","toggle","visibleSubPanel","dropdownSubPanel","getAttribute","dropdownRight","remove","add","selector","window","subMenu","Error"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"subpanel.min.js","sources":["../../../src/local/action_menu/subpanel.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 * Action menu subpanel JS controls.\n *\n * @module core/local/action_menu/subpanel\n * @copyright 2023 Mikel Martín \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {debounce} from 'core/utils';\nimport {\n isBehatSite,\n isExtraSmall,\n firstFocusableElement,\n lastFocusableElement,\n previousFocusableElement,\n nextFocusableElement,\n} from 'core/pagehelpers';\nimport Pending from 'core/pending';\nimport {\n hide,\n unhide,\n} from 'core/aria';\nimport EventHandler from 'theme_boost/bootstrap/dom/event-handler';\n\nconst Selectors = {\n mainMenu: '[role=\"menu\"]',\n dropdownRight: '.dropdown-menu-end',\n subPanel: '.dropdown-subpanel',\n subPanelMenuItem: '.dropdown-subpanel > .dropdown-item',\n subPanelContent: '.dropdown-subpanel > .dropdown-menu',\n // Drawer selector.\n drawer: '[data-region=\"fixed-drawer\"]',\n // Lateral blocks columns selectors.\n blockColumn: '.blockcolumn',\n columnLeft: '.columnleft',\n};\n\nconst Classes = {\n dropRight: 'dropend',\n dropLeft: 'dropstart',\n dropDown: 'dropdown',\n forceLeft: 'downleft',\n contentDisplayed: 'content-displayed',\n};\n\nconst BootstrapEvents = {\n hideDropdown: 'hidden.bs.dropdown',\n};\n\nlet initialized = false;\n\n/**\n * Initialize all delegated events into the page.\n */\nconst initPageEvents = () => {\n if (initialized) {\n return;\n }\n // Hide all subpanels when hiding a dropdown.\n document.addEventListener(BootstrapEvents.hideDropdown, () => {\n document.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n const subPanel = new SubPanel(dropdownSubPanel);\n subPanel.setVisibility(false);\n });\n });\n\n window.addEventListener('resize', debounce(updateAllPanelsPosition, 400));\n\n initialized = true;\n};\n\n/**\n * Update all the panels position.\n */\nconst updateAllPanelsPosition = () => {\n document.querySelectorAll(Selectors.subPanel).forEach(dropdown => {\n const subpanel = new SubPanel(dropdown);\n subpanel.updatePosition();\n });\n};\n\n/**\n * Subpanel class.\n * @private\n */\nclass SubPanel {\n /**\n * Constructor.\n * @param {HTMLElement} element The element to initialize.\n */\n constructor(element) {\n this.element = element;\n this.menuItem = element.querySelector(Selectors.subPanelMenuItem);\n this.panelContent = element.querySelector(Selectors.subPanelContent);\n /**\n * Enable preview when the menu item has focus.\n *\n * This is disabled when the user press ESC or shift+TAB to force closing\n *\n * @type {Boolean}\n * @private\n */\n this.showPreviewOnFocus = true;\n }\n\n /**\n * Initialize the subpanel element.\n *\n * This method adds the event listeners to the subpanel and the position classes.\n */\n init() {\n if (this.element.dataset.subPanelInitialized) {\n return;\n }\n\n this.updatePosition();\n\n // Full element events.\n this.element.addEventListener('focusin', this._mainElementFocusInHandler.bind(this));\n // Menu Item events.\n this.menuItem.addEventListener('click', this._menuItemClickHandler.bind(this));\n // Use the Bootstrap key handler for the menu item key handler.\n // This will avoid Boostrap Dropdown handler to prevent the propagation to the subpanel.\n const subpanelMenuItemSelector = `#${this.element.id}${Selectors.subPanelMenuItem}`;\n EventHandler.on(document, 'keydown', subpanelMenuItemSelector, this._menuItemKeyHandler.bind(this));\n if (!isBehatSite()) {\n // Behat in Chrome usually move the mouse over the page when trying clicking a subpanel element.\n // If the menu has more than one subpanel this could cause closing the subpanel by mistake.\n this.menuItem.addEventListener('mouseover', this._menuItemHoverHandler.bind(this));\n this.menuItem.addEventListener('mouseout', this._menuItemHoverOutHandler.bind(this));\n }\n // Subpanel content events.\n this.panelContent.addEventListener('keydown', this._panelContentKeyHandler.bind(this));\n\n this.element.dataset.subPanelInitialized = true;\n }\n\n /**\n * Hides the subpanel when mouse leaves the menu item.\n * @param {Event} event\n */\n _hideCurrentSubPanel(event) {\n // Only hide if not hovering over the menu item or subpanel content.\n const related = event.relatedTarget;\n if (!this.menuItem.contains(related) && !this.panelContent.contains(related)) {\n this.setVisibility(false);\n }\n }\n\n /**\n * Checks if the subpanel has enough space.\n *\n * In general there are two scenarios were the subpanel must be interacted differently:\n * - Extra small screens: The subpanel is displayed below the menu item.\n * - Drawer: The subpanel is displayed one of the drawers.\n * - Block columns: for classic based themes.\n *\n * @returns {Boolean} true if the subpanel should be displayed in small screens.\n */\n _needSmallSpaceBehaviour() {\n return isExtraSmall() ||\n this.element.closest(Selectors.drawer) !== null ||\n this.element.closest(Selectors.blockColumn) !== null;\n }\n\n /**\n * Check if the subpanel should be displayed on the right.\n *\n * This is defined by the drop right boostrap class. However, if the menu is\n * displayed in a block column on the right, the subpanel should be forced\n * to the right.\n *\n * @returns {Boolean} true if the subpanel should be displayed on the right.\n */\n _needDropdownRight() {\n if (this.element.closest(Selectors.columnLeft) !== null) {\n return false;\n }\n return this.element.closest(Selectors.dropdownRight) !== null;\n }\n\n /**\n * Main element focus in handler.\n */\n _mainElementFocusInHandler() {\n if (this._needSmallSpaceBehaviour() || !this.showPreviewOnFocus) {\n // Preview is disabled when the user press ESC or shift+TAB to force closing\n // but if the continue navigating with keyboard the preview is enabled again.\n this.showPreviewOnFocus = true;\n return;\n }\n this.setVisibility(true);\n }\n\n /**\n * Menu item click handler.\n * @param {Event} event\n */\n _menuItemClickHandler(event) {\n // Avoid dropdowns being closed after clicking a subemnu.\n // This won't be needed with BS5 (data-bs-auto-close handles it).\n event.stopPropagation();\n event.preventDefault();\n if (this._needSmallSpaceBehaviour()) {\n this.setVisibility(!this.getVisibility());\n }\n }\n\n /**\n * Menu item hover handler.\n * @private\n */\n _menuItemHoverHandler() {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this.setVisibility(true);\n }\n\n /**\n * Menu item hover out handler.\n * @param {Event} event\n * @private\n */\n _menuItemHoverOutHandler(event) {\n if (this._needSmallSpaceBehaviour()) {\n return;\n }\n this._hideOtherSubPanels();\n // Hide subpanel when the menu item itself is not hovered.\n this._hideCurrentSubPanel(event);\n }\n\n /**\n * Menu item key handler.\n * @param {Event} event\n * @private\n */\n _menuItemKeyHandler(event) {\n // In small sizes te down key will focus on the panel.\n if (event.key === 'ArrowUp' || (event.key === 'ArrowDown' && !this._needSmallSpaceBehaviour())) {\n this.setVisibility(false);\n return;\n }\n\n // Keys to move focus to the panel.\n let focusPanel = false;\n\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft' || (event.key === 'Tab' && !event.shiftKey)) {\n focusPanel = true;\n }\n if ((event.key === 'Enter' || event.key === ' ')) {\n focusPanel = true;\n }\n // In extra small screen the panel is shown below the item.\n if (event.key === 'ArrowDown' && this._needSmallSpaceBehaviour() && this.getVisibility()) {\n focusPanel = true;\n }\n if (focusPanel) {\n event.stopPropagation();\n event.preventDefault();\n this.setVisibility(true);\n this._focusPanelContent();\n }\n\n }\n\n /**\n * Sub panel content key handler.\n * @param {Event} event\n * @private\n */\n _panelContentKeyHandler(event) {\n // In extra small devices the panel is displayed under the menu item\n // so the arrow up/down switch between subpanel and the menu item.\n const canLoop = !this._needSmallSpaceBehaviour();\n let isBrowsingSubPanel = false;\n let newFocus = null;\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n newFocus = this.menuItem;\n }\n // Acording to WCAG Esc and Tab are similar to arrow navigation but they\n // force the subpanel to be closed.\n if (event.key === 'Escape' || (event.key === 'Tab' && event.shiftKey)) {\n newFocus = this.menuItem;\n this.setVisibility(false);\n this.showPreviewOnFocus = false;\n }\n if (event.key === 'ArrowUp') {\n newFocus = previousFocusableElement(this.panelContent, canLoop);\n isBrowsingSubPanel = true;\n }\n if (event.key === 'ArrowDown') {\n newFocus = nextFocusableElement(this.panelContent, canLoop);\n isBrowsingSubPanel = true;\n }\n if (event.key === 'Home') {\n newFocus = firstFocusableElement(this.panelContent);\n isBrowsingSubPanel = true;\n }\n if (event.key === 'End') {\n newFocus = lastFocusableElement(this.panelContent);\n isBrowsingSubPanel = true;\n }\n // If the user cannot loop and arrive to the start/end of the subpanel\n // we focus on the menu item.\n if (newFocus === null && isBrowsingSubPanel && !canLoop) {\n newFocus = this.menuItem;\n }\n if (newFocus !== null) {\n event.stopPropagation();\n event.preventDefault();\n newFocus.focus();\n }\n }\n\n /**\n * Focus on the first focusable element of the subpanel.\n * @private\n */\n _focusPanelContent() {\n const pendingPromise = new Pending('core/action_menu/subpanel:focuscontent');\n // Some Bootstrap events are triggered after the click event.\n // To prevent this from affecting the focus we wait a bit.\n setTimeout(() => {\n const firstFocusable = firstFocusableElement(this.panelContent);\n if (firstFocusable) {\n firstFocusable.focus();\n }\n pendingPromise.resolve();\n }, 100);\n }\n\n /**\n * Set the visibility of a subpanel.\n * @param {Boolean} visible true if the subpanel should be visible.\n */\n setVisibility(visible) {\n if (visible) {\n this._hideOtherSubPanels();\n }\n // Aria hidden/unhidden can alter the focus, we only want to do it when needed.\n if (!visible && this.getVisibility) {\n hide(this.panelContent);\n }\n if (visible && !this.getVisibility) {\n unhide(this.panelContent);\n }\n this.menuItem.setAttribute('aria-expanded', visible ? 'true' : 'false');\n this.panelContent.classList.toggle('show', visible);\n this.element.classList.toggle(Classes.contentDisplayed, visible);\n }\n\n /**\n * Hide all other subpanels in the parent menu.\n * @private\n */\n _hideOtherSubPanels() {\n const dropdown = this.element.closest(Selectors.mainMenu);\n dropdown.querySelectorAll(`${Selectors.subPanelContent}.show`).forEach(visibleSubPanel => {\n const dropdownSubPanel = visibleSubPanel.closest(Selectors.subPanel);\n if (dropdownSubPanel === this.element) {\n return;\n }\n const subPanel = new SubPanel(dropdownSubPanel);\n subPanel.setVisibility(false);\n });\n }\n\n /**\n * Get the visibility of a subpanel.\n * @returns {Boolean} true if the subpanel is visible.\n */\n getVisibility() {\n return this.menuItem.getAttribute('aria-expanded') === 'true';\n }\n\n /**\n * Update the panels position depending on the screen size and panel position.\n */\n updatePosition() {\n const dropdownRight = this._needDropdownRight();\n if (this._needSmallSpaceBehaviour()) {\n this.element.classList.remove(Classes.dropRight);\n this.element.classList.remove(Classes.dropLeft);\n this.element.classList.add(Classes.dropDown);\n this.element.classList.toggle(Classes.forceLeft, dropdownRight);\n return;\n }\n this.element.classList.remove(Classes.dropDown);\n this.element.classList.remove(Classes.forceLeft);\n this.element.classList.toggle(Classes.dropRight, !dropdownRight);\n this.element.classList.toggle(Classes.dropLeft, dropdownRight);\n }\n}\n\n/**\n * Initialise module for given report\n *\n * @method\n * @param {string} selector The query selector to init.\n */\nexport const init = (selector) => {\n initPageEvents();\n const subMenu = document.querySelector(selector);\n if (!subMenu) {\n throw new Error(`Sub panel element not found: ${selector}`);\n }\n const subPanel = new SubPanel(subMenu);\n subPanel.init();\n};\n"],"names":["Selectors","Classes","BootstrapEvents","initialized","updateAllPanelsPosition","document","querySelectorAll","forEach","dropdown","SubPanel","updatePosition","constructor","element","menuItem","querySelector","panelContent","showPreviewOnFocus","init","this","dataset","subPanelInitialized","addEventListener","_mainElementFocusInHandler","bind","_menuItemClickHandler","subpanelMenuItemSelector","id","on","_menuItemKeyHandler","_menuItemHoverHandler","_menuItemHoverOutHandler","_panelContentKeyHandler","_hideCurrentSubPanel","event","related","relatedTarget","contains","setVisibility","_needSmallSpaceBehaviour","closest","_needDropdownRight","stopPropagation","preventDefault","getVisibility","_hideOtherSubPanels","key","focusPanel","shiftKey","_focusPanelContent","canLoop","isBrowsingSubPanel","newFocus","focus","pendingPromise","Pending","setTimeout","firstFocusable","resolve","visible","setAttribute","classList","toggle","visibleSubPanel","dropdownSubPanel","getAttribute","dropdownRight","remove","add","selector","window","subMenu","Error"],"mappings":"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"}
\ No newline at end of file
diff --git a/public/lib/amd/build/local/reactive/srlogger.min.js b/public/lib/amd/build/local/reactive/srlogger.min.js
index b52a128fdb877..64070df5ff10f 100644
--- a/public/lib/amd/build/local/reactive/srlogger.min.js
+++ b/public/lib/amd/build/local/reactive/srlogger.min.js
@@ -1,3 +1,3 @@
-define("core/local/reactive/srlogger",["exports","core/local/reactive/logger"],(function(_exports,_logger){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_logger=(obj=_logger)&&obj.__esModule?obj:{default:obj};class SRLogger extends _logger.default{add(entry){if(entry.feedbackMessage){let loggerFeedback=document.getElementById(SRLogger.liveRegionId);loggerFeedback||(loggerFeedback=document.createElement("div"),loggerFeedback.id=SRLogger.liveRegionId,loggerFeedback.classList.add("visually-hidden"),loggerFeedback.setAttribute("aria-live","polite"),document.body.append(loggerFeedback)),loggerFeedback.innerHTML=entry.feedbackMessage,setTimeout((()=>{loggerFeedback.innerHTML=""}),4e3)}}}return _exports.default=SRLogger,function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(SRLogger,"liveRegionId","sr-logger-feedback-container"),_exports.default}));
+define("core/local/reactive/srlogger",["exports","core/toast","core/local/reactive/logger"],(function(_exports,_toast,_logger){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_logger=(obj=_logger)&&obj.__esModule?obj:{default:obj};class SRLogger extends _logger.default{add(entry){entry.feedbackMessage&&(0,_toast.add)(entry.feedbackMessage,{visuallyHidden:!0})}}return _exports.default=SRLogger,function(obj,key,value){key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}(SRLogger,"liveRegionId","sr-logger-feedback-container"),_exports.default}));
//# sourceMappingURL=srlogger.min.js.map
\ No newline at end of file
diff --git a/public/lib/amd/build/local/reactive/srlogger.min.js.map b/public/lib/amd/build/local/reactive/srlogger.min.js.map
index 0e34175a92fd5..c49293bc13943 100644
--- a/public/lib/amd/build/local/reactive/srlogger.min.js.map
+++ b/public/lib/amd/build/local/reactive/srlogger.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"srlogger.min.js","sources":["../../../src/local/reactive/srlogger.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 * Screen reader-only (visually-hidden) reactive mutations logger class.\n *\n * This logger can be used by the StateManager to log mutation feedbacks and actions.\n * The feedback messages logged by this logger will be rendered in a visually-hidden, ARIA live region.\n *\n * @module core/local/reactive/srlogger\n * @class SRLogger\n * @copyright 2023 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Logger from 'core/local/reactive/logger';\n\n/**\n * Logger entry structure.\n *\n * @typedef {object} LoggerEntry\n * @property {string} feedbackMessage Feedback message.\n */\n\n/**\n * Screen reader-only (visually-hidden) reactive mutations logger class.\n *\n * @class SRLogger\n */\nexport default class SRLogger extends Logger {\n /**\n * The element ID of the ARIA live region where the logger feedback will be rendered.\n *\n * @type {string}\n */\n static liveRegionId = 'sr-logger-feedback-container';\n\n /**\n * Add a log entry.\n * @param {LoggerEntry} entry Log entry.\n */\n add(entry) {\n if (entry.feedbackMessage) {\n // Fetch or create an ARIA live region that will serve as the container for the logger feedback.\n let loggerFeedback = document.getElementById(SRLogger.liveRegionId);\n if (!loggerFeedback) {\n loggerFeedback = document.createElement('div');\n loggerFeedback.id = SRLogger.liveRegionId;\n loggerFeedback.classList.add('visually-hidden');\n loggerFeedback.setAttribute('aria-live', 'polite');\n document.body.append(loggerFeedback);\n }\n // Set the ARIA live region's contents with the feedback.\n loggerFeedback.innerHTML = entry.feedbackMessage;\n\n // Clear the feedback message after 4 seconds to avoid the contents from being read out in case the user navigates\n // to this region. This is similar to the default timeout of toast messages before disappearing from view.\n setTimeout(() => {\n loggerFeedback.innerHTML = '';\n }, 4000);\n }\n }\n}\n"],"names":["SRLogger","Logger","add","entry","feedbackMessage","loggerFeedback","document","getElementById","liveRegionId","createElement","id","classList","setAttribute","body","append","innerHTML","setTimeout"],"mappings":"iQAyCqBA,iBAAiBC,gBAYlCC,IAAIC,UACIA,MAAMC,gBAAiB,KAEnBC,eAAiBC,SAASC,eAAeP,SAASQ,cACjDH,iBACDA,eAAiBC,SAASG,cAAc,OACxCJ,eAAeK,GAAKV,SAASQ,aAC7BH,eAAeM,UAAUT,IAAI,mBAC7BG,eAAeO,aAAa,YAAa,UACzCN,SAASO,KAAKC,OAAOT,iBAGzBA,eAAeU,UAAYZ,MAAMC,gBAIjCY,YAAW,KACPX,eAAeU,UAAY,KAC5B,kLA9BMf,wBAMK"}
\ No newline at end of file
+{"version":3,"file":"srlogger.min.js","sources":["../../../src/local/reactive/srlogger.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 * Screen reader-only (visually-hidden) reactive mutations logger class.\n *\n * This logger can be used by the StateManager to log mutation feedbacks and actions.\n * The feedback messages logged by this logger will be rendered in a visually-hidden, ARIA live region.\n *\n * @module core/local/reactive/srlogger\n * @class SRLogger\n * @copyright 2023 Jun Pataleta \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {add as addToast} from 'core/toast';\nimport Logger from 'core/local/reactive/logger';\n\n/**\n * Logger entry structure.\n *\n * @typedef {object} LoggerEntry\n * @property {string} feedbackMessage Feedback message.\n */\n\n/**\n * Screen reader-only (visually-hidden) reactive mutations logger class.\n *\n * @class SRLogger\n */\nexport default class SRLogger extends Logger {\n /**\n * The element ID of the ARIA live region where the logger feedback will be rendered.\n *\n * @type {string}\n */\n static liveRegionId = 'sr-logger-feedback-container';\n\n /**\n * Add a log entry.\n * @param {LoggerEntry} entry Log entry.\n */\n add(entry) {\n if (entry.feedbackMessage) {\n addToast(entry.feedbackMessage, {visuallyHidden: true});\n }\n }\n}\n"],"names":["SRLogger","Logger","add","entry","feedbackMessage","visuallyHidden"],"mappings":"qRA0CqBA,iBAAiBC,gBAYlCC,IAAIC,OACIA,MAAMC,gCACGD,MAAMC,gBAAiB,CAACC,gBAAgB,gLAdxCL,wBAMK"}
\ No newline at end of file
diff --git a/public/lib/amd/build/local/templates/renderer.min.js b/public/lib/amd/build/local/templates/renderer.min.js
index d8b0868e2566b..bf2bc29a7a796 100644
--- a/public/lib/amd/build/local/templates/renderer.min.js
+++ b/public/lib/amd/build/local/templates/renderer.min.js
@@ -1,4 +1,4 @@
-define("core/local/templates/renderer",["exports","core/log","core/truncate","core/user_date","core/pending","core/str","core/icon_system","core/config","core/mustache","./loader","core/utils"],(function(_exports,Log,Truncate,UserDate,_pending,_str,_icon_system,_config,_mustache,_loader,_utils){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}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 _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Log=_interopRequireWildcard(Log),Truncate=_interopRequireWildcard(Truncate),UserDate=_interopRequireWildcard(UserDate),_pending=_interopRequireDefault(_pending),_icon_system=_interopRequireDefault(_icon_system),_config=_interopRequireDefault(_config),_mustache=_interopRequireDefault(_mustache),_loader=_interopRequireDefault(_loader);
+define("core/local/templates/renderer",["exports","core/log","core/truncate","core/user_date","core/pending","core/str","core/icon_system","core/config","core/mustache","./loader","core/utils"],(function(_exports,Log,Truncate,UserDate,_pending,_str,_icon_system,_config,_mustache,_loader,_utils){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}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 _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Log=_interopRequireWildcard(Log),Truncate=_interopRequireWildcard(Truncate),UserDate=_interopRequireWildcard(UserDate),_pending=_interopRequireDefault(_pending),_icon_system=_interopRequireDefault(_icon_system),_config=_interopRequireDefault(_config),_mustache=_interopRequireDefault(_mustache),_loader=_interopRequireDefault(_loader);const originalMustacheEscape=_mustache.default.escape;_mustache.default.escape=function(string){return string=(string=originalMustacheEscape(string)).replace(/&#([0-9]+|x[0-9a-fA-F]+);/g,"$1;")};
/**
* Template Renderer Class.
*
diff --git a/public/lib/amd/build/local/templates/renderer.min.js.map b/public/lib/amd/build/local/templates/renderer.min.js.map
index 40991f31796ba..464ffb4b1fafe 100644
--- a/public/lib/amd/build/local/templates/renderer.min.js.map
+++ b/public/lib/amd/build/local/templates/renderer.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"renderer.min.js","sources":["../../../src/local/templates/renderer.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\nimport * as Log from 'core/log';\nimport * as Truncate from 'core/truncate';\nimport * as UserDate from 'core/user_date';\nimport Pending from 'core/pending';\nimport {getStrings} from 'core/str';\nimport IconSystem from 'core/icon_system';\nimport config from 'core/config';\nimport mustache from 'core/mustache';\nimport Loader from './loader';\nimport {getNormalisedComponent} from 'core/utils';\n\n/** @var {string} The placeholder character used for standard strings (unclean) */\nconst placeholderString = 's';\n\n/** @var {string} The placeholder character used for cleaned strings */\nconst placeholderCleanedString = 'c';\n\n/**\n * Template Renderer Class.\n *\n * Note: This class is not intended to be instantiated directly. Instead, use the core/templates module.\n *\n * @module core/local/templates/renderer\n * @copyright 2023 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 4.3\n */\nexport default class Renderer {\n /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */\n requiredStrings = null;\n\n /** @var {object[]} requiredDates - Collection of dates found during the rendering of one template */\n requiredDates = [];\n\n /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */\n requiredJS = null;\n\n /** @var {String} themeName for the current render */\n currentThemeName = '';\n\n /** @var {Number} uniqInstances Count of times this constructor has been called. */\n static uniqInstances = 0;\n\n /** @var {Object[]} loadTemplateBuffer - List of templates to be loaded */\n static loadTemplateBuffer = [];\n\n /** @var {Bool} isLoadingTemplates - Whether templates are currently being loaded */\n static isLoadingTemplates = false;\n\n /** @var {Object} iconSystem - Object extending core/iconsystem */\n iconSystem = null;\n\n /** @var {Array} disallowedNestedHelpers - List of helpers that can't be called within other helpers */\n static disallowedNestedHelpers = [\n 'js',\n ];\n\n /** @var {String[]} templateCache - Cache of already loaded template strings */\n static templateCache = {};\n\n /**\n * Cache of already loaded template promises.\n *\n * @type {Promise[]}\n * @static\n * @private\n */\n static templatePromises = {};\n\n /**\n * The loader used to fetch templates.\n * @type {Loader}\n * @static\n * @private\n */\n static loader = Loader;\n\n /**\n * Constructor\n *\n * Each call to templates.render gets it's own instance of this class.\n */\n constructor() {\n this.requiredStrings = [];\n this.requiredJS = [];\n this.requiredDates = [];\n this.currentThemeName = '';\n }\n\n /**\n * Set the template loader to use for all Template renderers.\n *\n * @param {Loader} loader\n */\n static setLoader(loader) {\n this.loader = loader;\n }\n\n /**\n * Get the Loader used to fetch templates.\n *\n * @returns {Loader}\n */\n static getLoader() {\n return this.loader;\n }\n\n /**\n * Render a single image icon.\n *\n * @method renderIcon\n * @private\n * @param {string} key The icon key.\n * @param {string} component The component name.\n * @param {string} title The icon title\n * @returns {Promise}\n */\n async renderIcon(key, component, title) {\n // Preload the module to do the icon rendering based on the theme iconsystem.\n component = getNormalisedComponent(component);\n\n await this.setupIconSystem();\n const template = await Renderer.getLoader().getTemplate(\n this.iconSystem.getTemplateName(),\n this.currentThemeName,\n );\n\n return this.iconSystem.renderIcon(\n key,\n component,\n title,\n template\n );\n }\n\n /**\n * Helper to set up the icon system.\n */\n async setupIconSystem() {\n if (!this.iconSystem) {\n this.iconSystem = await IconSystem.instance();\n }\n\n return this.iconSystem;\n }\n\n /**\n * Render image icons.\n *\n * @method pixHelper\n * @private\n * @param {object} context The mustache context\n * @param {string} sectionText The text to parse arguments from.\n * @param {function} helper Used to render the alt attribute of the text.\n * @returns {string}\n */\n pixHelper(context, sectionText, helper) {\n const parts = sectionText.split(',');\n let key = '';\n let component = '';\n let text = '';\n\n if (parts.length > 0) {\n key = helper(parts.shift().trim(), context);\n }\n if (parts.length > 0) {\n component = helper(parts.shift().trim(), context);\n }\n if (parts.length > 0) {\n text = helper(parts.join(',').trim(), context);\n }\n\n // Note: We cannot use Promises in Mustache helpers.\n // We must fetch straight from the Loader cache.\n // The Loader cache is statically defined on the Loader class and should be used by all children.\n const Loader = Renderer.getLoader();\n const templateName = this.iconSystem.getTemplateName();\n const searchKey = Loader.getSearchKey(this.currentThemeName, templateName);\n const template = Loader.getTemplateFromCache(searchKey);\n\n component = getNormalisedComponent(component);\n\n // The key might have been escaped by the JS Mustache engine which\n // converts forward slashes to HTML entities. Let us undo that here.\n key = key.replace(///gi, '/');\n\n return this.iconSystem.renderIcon(\n key,\n component,\n text,\n template\n );\n }\n\n /**\n * Render blocks of javascript and save them in an array.\n *\n * @method jsHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to save as a js block.\n * @param {function} helper Used to render the block.\n * @returns {string}\n */\n jsHelper(context, sectionText, helper) {\n this.requiredJS.push(helper(sectionText, context));\n return '';\n }\n\n /**\n * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}\n * into a get_string call.\n *\n * @method stringHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @returns {string}\n */\n stringHelper(context, sectionText, helper) {\n // A string instruction is in the format:\n // key, component, params.\n\n let parts = sectionText.split(',');\n\n const key = parts.length > 0 ? parts.shift().trim() : '';\n const component = parts.length > 0 ? getNormalisedComponent(parts.shift().trim()) : '';\n let param = parts.length > 0 ? parts.join(',').trim() : '';\n\n if (param !== '') {\n // Allow variable expansion in the param part only.\n param = helper(param, context);\n }\n\n if (param.match(/^{\\s*\"/gm)) {\n // If it can't be parsed then the string is not a JSON format.\n try {\n const parsedParam = JSON.parse(param);\n // Handle non-exception-throwing cases, e.g. null, integer, boolean.\n if (parsedParam && typeof parsedParam === \"object\") {\n param = parsedParam;\n }\n } catch (err) {\n // This was probably not JSON.\n // Keep the error message visible but do not promote it because it may not be an error.\n window.console.warn(err.message);\n }\n }\n\n const index = this.requiredStrings.length;\n this.requiredStrings.push({\n key,\n component,\n param,\n });\n\n // The placeholder must not use {{}} as those can be misinterpreted by the engine.\n return `[[_s${index}]]`;\n }\n\n /**\n * String helper to render {{#cleanstr}}abd component { a : 'fish'}{{/cleanstr}}\n * into a get_string following by an HTML escape.\n *\n * @method cleanStringHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @returns {string}\n */\n cleanStringHelper(context, sectionText, helper) {\n // We're going to use [[_cx]] format for clean strings, where x is a number.\n // Hence, replacing 's' with 'c' in the placeholder that stringHelper returns.\n return this\n .stringHelper(context, sectionText, helper)\n .replace(placeholderString, placeholderCleanedString);\n }\n\n /**\n * Quote helper used to wrap content in quotes, and escape all special JSON characters present in the content.\n *\n * @method quoteHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @returns {string}\n */\n quoteHelper(context, sectionText, helper) {\n let content = helper(sectionText.trim(), context);\n\n // Escape the {{ and JSON encode.\n // This involves wrapping {{, and }} in change delimeter tags.\n content = JSON.stringify(content);\n content = content.replace(/([{}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>');\n return content;\n }\n\n /**\n * Shorten text helper to truncate text and append a trailing ellipsis.\n *\n * @method shortenTextHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @returns {string}\n */\n shortenTextHelper(context, sectionText, helper) {\n // Non-greedy split on comma to grab section text into the length and\n // text parts.\n const parts = sectionText.match(/(.*?),(.*)/);\n\n // The length is the part matched in the first set of parethesis.\n const length = parts[1].trim();\n // The length is the part matched in the second set of parethesis.\n const text = parts[2].trim();\n const content = helper(text, context);\n return Truncate.truncate(content, {\n length,\n words: true,\n ellipsis: '...'\n });\n }\n\n /**\n * User date helper to render user dates from timestamps.\n *\n * @method userDateHelper\n * @private\n * @param {object} context The current mustache context.\n * @param {string} sectionText The text to parse the arguments from.\n * @param {function} helper Used to render subsections of the text.\n * @returns {string}\n */\n userDateHelper(context, sectionText, helper) {\n // Non-greedy split on comma to grab the timestamp and format.\n const parts = sectionText.match(/(.*?),(.*)/);\n\n const timestamp = helper(parts[1].trim(), context);\n const format = helper(parts[2].trim(), context);\n const index = this.requiredDates.length;\n\n this.requiredDates.push({\n timestamp: timestamp,\n format: format\n });\n\n return `[[_t_${index}]]`;\n }\n\n /**\n * Return a helper function to be added to the context for rendering the a\n * template.\n *\n * This will parse the provided text before giving it to the helper function\n * in order to remove any disallowed nested helpers to prevent one helper\n * from calling another.\n *\n * In particular to prevent the JS helper from being called from within another\n * helper because it can lead to security issues when the JS portion is user\n * provided.\n *\n * @param {function} helperFunction The helper function to add\n * @param {object} context The template context for the helper function\n * @returns {Function} To be set in the context\n */\n addHelperFunction(helperFunction, context) {\n return function() {\n return function(sectionText, helper) {\n // Override the disallowed helpers in the template context with\n // a function that returns an empty string for use when executing\n // other helpers. This is to prevent these helpers from being\n // executed as part of the rendering of another helper in order to\n // prevent any potential security issues.\n const originalHelpers = Renderer.disallowedNestedHelpers.reduce((carry, name) => {\n if (context.hasOwnProperty(name)) {\n carry[name] = context[name];\n }\n\n return carry;\n }, {});\n\n Renderer.disallowedNestedHelpers.forEach((helperName) => {\n context[helperName] = () => '';\n });\n\n // Execute the helper with the modified context that doesn't include\n // the disallowed nested helpers. This prevents the disallowed\n // helpers from being called from within other helpers.\n const result = helperFunction.apply(this, [context, sectionText, helper]);\n\n // Restore the original helper implementation in the context so that\n // any further rendering has access to them again.\n for (const name in originalHelpers) {\n context[name] = originalHelpers[name];\n }\n\n return result;\n }.bind(this);\n }.bind(this);\n }\n\n /**\n * Add some common helper functions to all context objects passed to templates.\n * These helpers match exactly the helpers available in php.\n *\n * @method addHelpers\n * @private\n * @param {Object} context Simple types used as the context for the template.\n * @param {String} themeName We set this multiple times, because there are async calls.\n */\n addHelpers(context, themeName) {\n this.currentThemeName = themeName;\n this.requiredStrings = [];\n this.requiredJS = [];\n context.uniqid = (Renderer.uniqInstances++);\n\n // Please note that these helpers _must_ not return a Promise.\n context.str = this.addHelperFunction(this.stringHelper, context);\n context.cleanstr = this.addHelperFunction(this.cleanStringHelper, context);\n context.pix = this.addHelperFunction(this.pixHelper, context);\n context.js = this.addHelperFunction(this.jsHelper, context);\n context.quote = this.addHelperFunction(this.quoteHelper, context);\n context.shortentext = this.addHelperFunction(this.shortenTextHelper, context);\n context.userdate = this.addHelperFunction(this.userDateHelper, context);\n context.globals = {config: config};\n context.currentTheme = themeName;\n }\n\n /**\n * Get all the JS blocks from the last rendered template.\n *\n * @method getJS\n * @private\n * @returns {string}\n */\n getJS() {\n return this.requiredJS.join(\";\\n\");\n }\n\n /**\n * Treat strings in content.\n *\n * The purpose of this method is to replace the placeholders found in a string\n * with the their respective translated strings.\n *\n * Previously we were relying on String.replace() but the complexity increased with\n * the numbers of strings to replace. Now we manually walk the string and stop at each\n * placeholder we find, only then we replace it. Most of the time we will\n * replace all the placeholders in a single run, at times we will need a few\n * more runs when placeholders are replaced with strings that contain placeholders\n * themselves.\n *\n * @param {String} content The content in which string placeholders are to be found.\n * @param {Map} stringMap The strings to replace with.\n * @returns {String} The treated content.\n */\n treatStringsInContent(content, stringMap) {\n // Placeholders are in the for [[_sX]] or [[_cX]] where X is the string index.\n const stringPattern = /(?\\[\\[_(?[cs])(?\\d+)\\]\\])/g;\n\n // A helper to fetch the string for a given placeholder.\n const getUpdatedString = ({placeholder, stringType, stringIndex}) => {\n if (stringMap.has(placeholder)) {\n return stringMap.get(placeholder);\n }\n\n if (stringType === placeholderCleanedString) {\n // Attempt to find the unclean string and clean it. Store it for later use.\n const uncleanString = stringMap.get(`[[_s${stringIndex}]]`);\n if (uncleanString) {\n stringMap.set(placeholder, mustache.escape(uncleanString));\n return stringMap.get(placeholder);\n }\n }\n\n Log.debug(`Could not find string for pattern ${placeholder}`);\n return ''; // Fallback if no match is found.\n };\n\n let updatedContent = content; // Start with the original content.\n let placeholderFound = true; // Flag to track if we are still finding placeholders.\n\n // Continue looping until no more placeholders are found in the updated content.\n while (placeholderFound) {\n let match;\n let result = [];\n let lastIndex = 0;\n placeholderFound = false; // Assume no placeholders are found.\n\n // Find all placeholders in the content and replace them with their respective strings.\n while ((match = stringPattern.exec(updatedContent)) !== null) {\n placeholderFound = true; // A placeholder was found, so continue looping.\n\n // Add the content before the matched placeholder.\n result.push(updatedContent.slice(lastIndex, match.index));\n\n // Add the updated string for the placeholder.\n result.push(getUpdatedString(match.groups));\n\n // Update lastIndex to move past the current match.\n lastIndex = match.index + match[0].length;\n }\n\n // Add the remaining part of the content after the last match.\n result.push(updatedContent.slice(lastIndex));\n\n // Join the parts of the result array into the updated content.\n updatedContent = result.join('');\n }\n\n return updatedContent; // Return the fully updated content after all loops.\n }\n\n /**\n * Treat strings in content.\n *\n * The purpose of this method is to replace the date placeholders found in the\n * content with the their respective translated dates.\n *\n * @param {String} content The content in which string placeholders are to be found.\n * @param {Array} dates The dates to replace with.\n * @returns {String} The treated content.\n */\n treatDatesInContent(content, dates) {\n dates.forEach((date, index) => {\n content = content.replace(\n new RegExp(`\\\\[\\\\[_t_${index}\\\\]\\\\]`, 'g'),\n date,\n );\n });\n\n return content;\n }\n\n /**\n * Render a template and then call the callback with the result.\n *\n * @method doRender\n * @private\n * @param {string|Promise} templateSourcePromise The mustache template to render.\n * @param {Object} context Simple types used as the context for the template.\n * @param {String} themeName Name of the current theme.\n * @returns {Promise |