Merge branch 'MDL-68498-master' of git://github.com/mihailges/moodle
authorAdrian Greeve <abgreeve@gmail.com>
Thu, 14 May 2020 06:13:55 +0000 (14:13 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Thu, 14 May 2020 06:13:55 +0000 (14:13 +0800)
69 files changed:
admin/cli/cfg.php
admin/settings/courses.php
backup/moodle2/backup_course_task.class.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_settingslib.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_course_task.class.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_settingslib.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/restore_controller_dbops.class.php
backup/util/ui/tests/behat/import_contentbank_content.feature [new file with mode: 0644]
contentbank/amd/build/search.min.js [new file with mode: 0644]
contentbank/amd/build/search.min.js.map [new file with mode: 0644]
contentbank/amd/build/selectors.min.js [new file with mode: 0644]
contentbank/amd/build/selectors.min.js.map [new file with mode: 0644]
contentbank/amd/src/search.js [new file with mode: 0644]
contentbank/amd/src/selectors.js [new file with mode: 0644]
contentbank/classes/contentbank.php
contentbank/classes/output/bankcontent.php
contentbank/index.php
contentbank/templates/bankcontent.mustache
contentbank/templates/bankcontent/search.mustache [new file with mode: 0644]
contentbank/templates/bankcontent/toolbar.mustache [moved from contentbank/templates/toolbar.mustache with 91% similarity]
contentbank/tests/behat/search_content.feature [new file with mode: 0644]
contentbank/tests/content_test.php
contentbank/tests/contentbank_test.php
contentbank/tests/contenttype_test.php
contentbank/tests/external/delete_content_test.php
contentbank/tests/external/rename_content_test.php
contentbank/tests/privacy_test.php
course/amd/build/activitychooser.min.js
course/amd/build/activitychooser.min.js.map
course/amd/build/local/activitychooser/dialogue.min.js
course/amd/build/local/activitychooser/dialogue.min.js.map
course/amd/src/activitychooser.js
course/amd/src/local/activitychooser/dialogue.js
h5p/classes/editor.php
h5p/classes/editor_framework.php
h5p/classes/helper.php
h5p/classes/local/library/autoloader.php
h5p/classes/local/library/handler.php
h5p/h5plib/v124/joubel/editor/readme_moodle.txt
h5p/h5plib/v124/lang/en/h5plib_v124.php
h5p/tests/editor_test.php
h5p/tests/local/library/handler_test.php [new file with mode: 0644]
lang/en/backup.php
lang/en/contentbank.php
lib/amd/build/modal.min.js
lib/amd/build/modal.min.js.map
lib/amd/src/modal.js
lib/behat/classes/behat_core_generator.php
lib/cronlib.php
lib/table/amd/build/dynamic.min.js
lib/table/amd/build/dynamic.min.js.map
lib/table/amd/src/dynamic.js
lib/tablelib.php
pix/i/contentbank.png [new file with mode: 0644]
pix/i/contentbank.svg [new file with mode: 0644]
report/configlog/classes/output/report_table.php
report/configlog/lang/en/report_configlog.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/message.scss
theme/boost/scss/moodle/tables.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
version.php

index 7f56b92..35045ca 100644 (file)
@@ -124,7 +124,12 @@ if ($options['unset'] || $options['set'] !== null) {
         cli_error('The configuration variable is hard-set in the config.php, unable to change.', 4);
     }
 
-    set_config($options['name'], $options['set'], $options['component']);
+    $new = $options['set'];
+    $old = get_config($options['component'], $options['name']);
+    if ($new !== $old) {
+        set_config($options['name'], $options['set'], $options['component']);
+        add_to_config_log($options['name'], $old, $new, $options['component']);
+    }
     exit(0);
 }
 
index 841b72b..8936f7c 100644 (file)
@@ -235,6 +235,11 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
             new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'),
             array('value' => 1, 'locked' => 0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_competencies', new lang_string('generalcompetencies','backup'), new lang_string('configgeneralcompetencies','backup'), array('value'=>1, 'locked'=>0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_general_contentbankcontent',
+        new lang_string('generalcontentbankcontent', 'backup'),
+        new lang_string('configgeneralcontentbankcontent', 'backup'),
+        ['value' => 1, 'locked' => 0])
+    );
 
     $ADMIN->add('backups', $temp);
 
@@ -256,6 +261,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
             new lang_string('generalgroups', 'backup'), new lang_string('configgeneralgroups', 'backup'),
             array('value' => 1, 'locked' => 0)));
     $temp->add(new admin_setting_configcheckbox_with_lock('backup/backup_import_competencies', new lang_string('generalcompetencies','backup'), new lang_string('configgeneralcompetencies','backup'), array('value'=>1, 'locked'=>0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock(
+        'backup/backup_import_contentbankcontent',
+        new lang_string('generalcontentbankcontent', 'backup'),
+        new lang_string('configgeneralcontentbankcontent', 'backup'),
+        ['value' => 1, 'locked' => 0])
+    );
 
     $ADMIN->add('backups', $temp);
 
@@ -375,6 +386,12 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_groups', new lang_string('generalgroups', 'backup'),
             new lang_string('configgeneralgroups', 'backup'), 1));
     $temp->add(new admin_setting_configcheckbox('backup/backup_auto_competencies', new lang_string('generalcompetencies','backup'), new lang_string('configgeneralcompetencies','backup'), 1));
+    $temp->add(new admin_setting_configcheckbox(
+        'backup/backup_auto_contentbankcontent',
+        new lang_string('generalcontentbankcontent', 'backup'),
+        new lang_string('configgeneralcontentbankcontent', 'backup'),
+        1)
+    );
 
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_messages', new lang_string('messages', 'message'), new lang_string('backupmessageshelp','message'), 0));
     //$temp->add(new admin_setting_configcheckbox('backup/backup_auto_blogs', new lang_string('blogs', 'blog'), new lang_string('backupblogshelp','blog'), 0));
@@ -435,6 +452,9 @@ if ($hassiteconfig or has_any_capability($capabilities, $systemcontext)) {
     $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_competencies',
         new lang_string('generalcompetencies', 'backup'),
         new lang_string('configrestorecompetencies', 'backup'), array('value' => 1, 'locked' => 0)));
+    $temp->add(new admin_setting_configcheckbox_with_lock('restore/restore_general_contentbankcontent',
+        new lang_string('generalcontentbankcontent', 'backup'),
+        new lang_string('configrestorecontentbankcontent', 'backup'), array('value' => 1, 'locked' => 0)));
 
     // Restore defaults when merging into another course.
     $temp->add(new admin_setting_heading('mergerestoredefaults', new lang_string('mergerestoredefaults', 'backup'), ''));
index d9060e3..d2ac8c5 100644 (file)
@@ -139,6 +139,11 @@ class backup_course_task extends backup_task {
         // Migrate the already exported inforef entries to final ones
         $this->add_step(new move_inforef_annotations_to_final('migrate_inforef'));
 
+        // Generate the content bank file (conditionally).
+        if ($this->get_setting_value('contentbankcontent')) {
+            $this->add_step(new backup_contentbankcontent_structure_step('course_contentbank', 'contentbank.xml'));
+        }
+
         // At the end, mark it as built
         $this->built = true;
     }
index ee434c1..4771c39 100644 (file)
@@ -179,5 +179,10 @@ class backup_root_task extends backup_task {
         $customfields = new backup_customfield_setting('customfield', base_setting::IS_BOOLEAN, true);
         $customfields->set_ui(new backup_setting_ui_checkbox($customfields, get_string('rootsettingcustomfield', 'backup')));
         $this->add_setting($customfields);
+
+        // Define content bank content inclusion setting.
+        $contentbank = new backup_contentbankcontent_setting('contentbankcontent', base_setting::IS_BOOLEAN, true);
+        $contentbank->set_ui(new backup_setting_ui_checkbox($contentbank, get_string('rootsettingcontentbankcontent', 'backup')));
+        $this->add_setting($contentbank);
     }
 }
index 9125673..59ea252 100644 (file)
@@ -199,3 +199,9 @@ class backup_activity_included_setting extends activity_backup_setting {}
  * user information or no, depends of @backup_users_setting
  */
 class backup_activity_userinfo_setting extends activity_backup_setting {}
+
+/**
+ * Root setting to control if backup will include content bank content or no
+ */
+class backup_contentbankcontent_setting extends backup_generic_setting {
+}
index ac8c917..cf0115a 100644 (file)
@@ -2740,3 +2740,34 @@ class backup_completion_defaults_structure_step extends backup_structure_step {
 
     }
 }
+
+/**
+ * Structure step in charge of constructing the contentbank.xml file for all the contents found in a given context
+ */
+class backup_contentbankcontent_structure_step extends backup_structure_step {
+
+    /**
+     * Define structure for content bank step
+     */
+    protected function define_structure() {
+
+        // Define each element separated.
+        $contents = new backup_nested_element('contents');
+        $content = new backup_nested_element('content', ['id'], [
+            'name', 'contenttype', 'instanceid', 'configdata', 'usercreated', 'usermodified', 'timecreated', 'timemodified']);
+
+        // Build the tree.
+        $contents->add_child($content);
+
+        // Define sources.
+        $content->set_source_table('contentbank_content', ['contextid' => backup::VAR_CONTEXTID]);
+
+        // Define annotations.
+        $content->annotate_ids('user', 'usercreated');
+        $content->annotate_ids('user', 'usermodified');
+        $content->annotate_files('contentbank', 'public', 'id');
+
+        // Return the root element (contents).
+        return $contents;
+    }
+}
index 6bcf826..be4f3f9 100644 (file)
@@ -126,6 +126,11 @@ class restore_course_task extends restore_task {
         // Activity completion defaults.
         $this->add_step(new restore_completion_defaults_structure_step('course_completion_defaults', 'completiondefaults.xml'));
 
+        // Content bank content (conditionally).
+        if ($this->get_setting_value('contentbankcontent')) {
+            $this->add_step(new restore_contentbankcontent_structure_step('course_contentbank', 'contentbank.xml'));
+        }
+
         // At the end, mark it as built
         $this->built = true;
     }
index 3069a0b..632985f 100644 (file)
@@ -290,5 +290,17 @@ class restore_root_task extends restore_task {
         $customfields = new restore_customfield_setting('customfields', base_setting::IS_BOOLEAN, $defaultvalue);
         $customfields->set_ui(new backup_setting_ui_checkbox($customfields, get_string('rootsettingcustomfield', 'backup')));
         $this->add_setting($customfields);
+
+        // Define Content bank content.
+        $defaultvalue = false;
+        $changeable = false;
+        if (isset($rootsettings['contentbankcontent']) && $rootsettings['contentbankcontent']) { // Only enabled when available.
+            $defaultvalue = true;
+            $changeable = true;
+        }
+        $contents = new restore_contentbankcontent_setting('contentbankcontent', base_setting::IS_BOOLEAN, $defaultvalue);
+        $contents->set_ui(new backup_setting_ui_checkbox($contents, get_string('rootsettingcontentbankcontent', 'backup')));
+        $contents->get_ui()->set_changeable($changeable);
+        $this->add_setting($contents);
     }
 }
index daa3fec..61124de 100644 (file)
@@ -236,3 +236,9 @@ class restore_activity_included_setting extends restore_activity_generic_setting
  * user information or no, depends of @restore_users_setting
  */
 class restore_activity_userinfo_setting extends restore_activity_generic_setting {}
+
+/**
+ * root setting to control if restore will create content bank content or no
+ */
+class restore_contentbankcontent_setting extends restore_generic_setting {
+}
index 10199e0..223b849 100644 (file)
@@ -3984,6 +3984,60 @@ class restore_activity_grade_history_structure_step extends restore_structure_st
     }
 }
 
+/**
+ * This structure steps restores the content bank content
+ */
+class restore_contentbankcontent_structure_step extends restore_structure_step {
+
+    /**
+     * Define structure for content bank step
+     */
+    protected function define_structure() {
+
+        $paths = [];
+        $paths[] = new restore_path_element('contentbankcontent', '/contents/content');
+
+        return $paths;
+    }
+
+    /**
+     * Define data processed for content bank
+     *
+     * @param mixed  $data
+     */
+    public function process_contentbankcontent($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $oldid = $data->id;
+
+        $params = [
+            'name'           => $data->name,
+            'contextid'      => $this->task->get_contextid(),
+            'contenttype'    => $data->contenttype,
+            'instanceid'     => $data->instanceid,
+            'timecreated'    => $data->timecreated,
+        ];
+        $exists = $DB->record_exists('contentbank_content', $params);
+        if (!$exists) {
+            $params['configdata'] = $data->configdata;
+            $params['usercreated'] = $this->get_mappingid('user', $data->usercreated);
+            $params['usermodified'] = $this->get_mappingid('user', $data->usermodified);
+            $params['timemodified'] = time();
+            $newitemid = $DB->insert_record('contentbank_content', $params);
+            $this->set_mapping('contentbank_content', $oldid, $newitemid, true);
+        }
+    }
+
+    /**
+     * Define data processed after execute for content bank
+     */
+    protected function after_execute() {
+        // Add related files.
+        $this->add_related_files('contentbank', 'public', 'contentbank_content');
+    }
+}
+
 /**
  * This structure steps restores one instance + positions of one block
  * Note: Positions corresponding to one existing context are restored
index 6649934..b4fed99 100644 (file)
@@ -1063,4 +1063,32 @@ class core_backup_moodle2_testcase extends advanced_testcase {
             }
         }
     }
+
+    /**
+     * Test the content bank content through a backup and restore.
+     */
+    public function test_contentbank_content_backup() {
+        global $DB, $USER, $CFG;
+        $this->resetAfterTest();
+
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+        $cbgenerator = $this->getDataGenerator()->get_plugin_generator('core_contentbank');
+
+        // Create course and add content bank content.
+        $course = $generator->create_course();
+        $context = context_course::instance($course->id);
+        $filepath = $CFG->dirroot . '/h5p/tests/fixtures/filltheblanks.h5p';
+        $contents = $cbgenerator->generate_contentbank_data('contenttype_h5p', 2, $USER->id, $context, true, $filepath);
+        $this->assertEquals(2, $DB->count_records('contentbank_content'));
+
+        // Do backup and restore.
+        $newcourseid = $this->backup_and_restore($course);
+
+        // Confirm that values were transferred correctly into content bank on new course.
+        $newcontext = context_course::instance($newcourseid);
+
+        $this->assertEquals(4, $DB->count_records('contentbank_content'));
+        $this->assertEquals(2, $DB->count_records('contentbank_content', ['contextid' => $newcontext->id]));
+    }
 }
index ce9c1ba..9e09f55 100644 (file)
@@ -564,7 +564,8 @@ abstract class backup_controller_dbops extends backup_dbops {
                         'backup_general_histories'          => 'grade_histories',
                         'backup_general_questionbank'       => 'questionbank',
                         'backup_general_groups'             => 'groups',
-                        'backup_general_competencies'       => 'competencies'
+                        'backup_general_competencies'       => 'competencies',
+                        'backup_general_contentbankcontent' => 'contentbankcontent',
                 );
                 self::apply_admin_config_defaults($controller, $settings, true);
                 break;
@@ -577,7 +578,8 @@ abstract class backup_controller_dbops extends backup_dbops {
                         'backup_import_calendarevents'     => 'calendarevents',
                         'backup_import_questionbank'       => 'questionbank',
                         'backup_import_groups'             => 'groups',
-                        'backup_import_competencies'       => 'competencies'
+                        'backup_import_competencies'       => 'competencies',
+                        'backup_import_contentbankcontent' => 'contentbankcontent',
                 );
                 self::apply_admin_config_defaults($controller, $settings, true);
                 if ((!$controller->get_interactive()) &&
@@ -608,7 +610,8 @@ abstract class backup_controller_dbops extends backup_dbops {
                         'backup_auto_histories'          => 'grade_histories',
                         'backup_auto_questionbank'       => 'questionbank',
                         'backup_auto_groups'             => 'groups',
-                        'backup_auto_competencies'       => 'competencies'
+                        'backup_auto_competencies'       => 'competencies',
+                        'backup_auto_contentbankcontent' => 'contentbankcontent'
                 );
                 self::apply_admin_config_defaults($controller, $settings, false);
                 break;
index 6649777..148655b 100644 (file)
@@ -157,7 +157,8 @@ abstract class restore_controller_dbops extends restore_dbops {
             'restore_general_histories'          => 'grade_histories',
             'restore_general_questionbank'       => 'questionbank',
             'restore_general_groups'             => 'groups',
-            'restore_general_competencies'       => 'competencies'
+            'restore_general_competencies'       => 'competencies',
+            'restore_general_contentbankcontent' => 'contentbankcontent'
         );
         self::apply_admin_config_defaults($controller, $settings, true);
 
diff --git a/backup/util/ui/tests/behat/import_contentbank_content.feature b/backup/util/ui/tests/behat/import_contentbank_content.feature
new file mode 100644 (file)
index 0000000..68f2fb3
--- /dev/null
@@ -0,0 +1,45 @@
+@core @core_backup @core_contentbank
+Feature: Import course content bank content
+  In order to import content from a course contentbank
+  As a teacher
+  I need to confirm that errors will not happen
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+      | Course 2 | C2        | 0        |
+    And the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | teacher1 | C2 | editingteacher |
+    And the following "contentbank content" exist:
+      | course| contenttype     | user     | contentname       |
+      | C1    | contenttype_h5p | teacher1 | ipsums.h5p        |
+    And I log in as "teacher1"
+
+  Scenario: Import content bank content to another course
+    Given I am on "Course 2" course homepage
+    And I click on "Content bank" "link"
+    And I should not see "ipsums.h5p"
+    When I import "Course 1" course into "Course 2" course using this options:
+    And I click on "Content bank" "link"
+    Then I should see "ipsums.h5p"
+    And I am on "Course 1" course homepage
+    And I click on "Content bank" "link"
+    And I should see "ipsums.h5p"
+
+  Scenario: User could configure not to import content bank
+    Given I am on "Course 2" course homepage
+    And I click on "Content bank" "link"
+    And I should not see "ipsums.h5p"
+    When I import "Course 1" course into "Course 2" course using this options:
+      | Initial | Include content bank content | 0 |
+    And I click on "Content bank" "link"
+    Then I should not see "ipsums.h5p"
+    And I am on "Course 1" course homepage
+    And I click on "Content bank" "link"
+    And I should see "ipsums.h5p"
diff --git a/contentbank/amd/build/search.min.js b/contentbank/amd/build/search.min.js
new file mode 100644 (file)
index 0000000..9090c9b
Binary files /dev/null and b/contentbank/amd/build/search.min.js differ
diff --git a/contentbank/amd/build/search.min.js.map b/contentbank/amd/build/search.min.js.map
new file mode 100644 (file)
index 0000000..a3ddca1
Binary files /dev/null and b/contentbank/amd/build/search.min.js.map differ
diff --git a/contentbank/amd/build/selectors.min.js b/contentbank/amd/build/selectors.min.js
new file mode 100644 (file)
index 0000000..c7322b9
Binary files /dev/null and b/contentbank/amd/build/selectors.min.js differ
diff --git a/contentbank/amd/build/selectors.min.js.map b/contentbank/amd/build/selectors.min.js.map
new file mode 100644 (file)
index 0000000..99b3b56
Binary files /dev/null and b/contentbank/amd/build/selectors.min.js.map differ
diff --git a/contentbank/amd/src/search.js b/contentbank/amd/src/search.js
new file mode 100644 (file)
index 0000000..bd5cb55
--- /dev/null
@@ -0,0 +1,160 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Search methods for finding contents in the content bank.
+ *
+ * @module     core_contentbank/search
+ * @package    core_contentbank
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import $ from 'jquery';
+import selectors from 'core_contentbank/selectors';
+import {get_string as getString} from 'core/str';
+import Pending from 'core/pending';
+import {debounce} from 'core/utils';
+
+/**
+ * Set up the search.
+ *
+ * @method init
+ */
+export const init = () => {
+    const pendingPromise = new Pending();
+
+    const root = $(selectors.elements.main);
+    registerListenerEvents(root);
+
+    pendingPromise.resolve();
+};
+
+/**
+ * Register contentbank search related event listeners.
+ *
+ * @method registerListenerEvents
+ * @param {Object} root The root element for the contentbank.
+ */
+const registerListenerEvents = (root) => {
+
+    const searchInput = root.find(selectors.elements.searchinput)[0];
+
+    root.on('click', selectors.actions.search, function(e) {
+        e.preventDefault();
+        toggleSearchResultsView(root, searchInput.value);
+    });
+
+    root.on('click', selectors.actions.clearSearch, function(e) {
+        e.preventDefault();
+        searchInput.value = "";
+        searchInput.focus();
+        toggleSearchResultsView(root, searchInput.value);
+    });
+
+    // The search input is also triggered.
+    searchInput.addEventListener('input', debounce(() => {
+        // Display the search results.
+        toggleSearchResultsView(root, searchInput.value);
+    }, 300));
+
+};
+
+/**
+ * Toggle (display/hide) the search results depending on the value of the search query.
+ *
+ * @method toggleSearchResultsView
+ * @param {HTMLElement} body The root element for the contentbank.
+ * @param {String} searchQuery The search query.
+ */
+const toggleSearchResultsView = async(body, searchQuery) => {
+    const clearSearchButton = body.find(selectors.elements.clearsearch)[0];
+    const searchIcon = body.find(selectors.elements.searchicon)[0];
+
+    const navbarBreadcrumb = body.find(selectors.elements.cbnavbarbreadcrumb)[0];
+    const navbarTotal = body.find(selectors.elements.cbnavbartotalsearch)[0];
+    // Update the results.
+    const filteredContents = filterContents(body, searchQuery);
+    if (searchQuery.length > 0) {
+        // As the search query is present, search results should be displayed.
+
+        // Display the "clear" search button in the activity chooser search bar.
+        searchIcon.classList.add('d-none');
+        clearSearchButton.classList.remove('d-none');
+
+        // Change the cb-navbar to display total items found.
+        navbarBreadcrumb.classList.add('d-none');
+        navbarTotal.innerHTML = await getString('itemsfound', 'core_contentbank', filteredContents.length);
+        navbarTotal.classList.remove('d-none');
+    } else {
+        // As search query is not present, the search results should be removed.
+
+        // Hide the "clear" search button in the activity chooser search bar.
+        clearSearchButton.classList.add('d-none');
+        searchIcon.classList.remove('d-none');
+
+        // Display again the breadcrumb in the navbar.
+        navbarBreadcrumb.classList.remove('d-none');
+        navbarTotal.classList.add('d-none');
+    }
+};
+
+/**
+ * Return the list of contents which have a name that matches the given search term.
+ *
+ * @method filterContents
+ * @param {HTMLElement} body The root element for the contentbank.
+ * @param {String} searchTerm The search term to match.
+ * @return {Array}
+ */
+const filterContents = (body, searchTerm) => {
+    const contents = Array.from(body.find(selectors.elements.cbfile));
+    const searchResults = [];
+    contents.forEach((content) => {
+        const contentName = content.getAttribute('data-file');
+        if (searchTerm === '' || contentName.toLowerCase().includes(searchTerm.toLowerCase())) {
+            // The content matches the search criteria so it should be displayed and hightlighted.
+            searchResults.push(content);
+            const contentNameElement = content.querySelector(selectors.regions.cbcontentname);
+            contentNameElement.innerHTML = highlight(contentName, searchTerm);
+            content.classList.remove('d-none');
+        } else {
+            content.classList.add('d-none');
+        }
+    });
+
+    return searchResults;
+};
+
+/**
+ * Highlight a given string in a text.
+ *
+ * @method highlight
+ * @param  {String} text The whole text.
+ * @param  {String} highlightText The piece of text to highlight.
+ * @return {String}
+ */
+const highlight = (text, highlightText) => {
+    let result = text;
+    if (highlightText !== '') {
+        const pos = text.toLowerCase().indexOf(highlightText.toLowerCase());
+        if (pos > -1) {
+            result = text.substr(0, pos) + '<span class="matchtext">' + text.substr(pos, highlightText.length) + '</span>' +
+                text.substr(pos + highlightText.length);
+        }
+    }
+
+    return result;
+};
diff --git a/contentbank/amd/src/selectors.js b/contentbank/amd/src/selectors.js
new file mode 100644 (file)
index 0000000..080f85f
--- /dev/null
@@ -0,0 +1,54 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Define all of the selectors we will be using on the contentbank interface.
+ *
+ * @module     core_contentbank/selectors
+ * @package    core_contentbank
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * A small helper function to build queryable data selectors.
+ *
+ * @method getDataSelector
+ * @param {String} name
+ * @param {String} value
+ * @return {string}
+ */
+const getDataSelector = (name, value) => {
+    return `[data-${name}="${value}"]`;
+};
+
+export default {
+    regions: {
+        cbcontentname: getDataSelector('region', 'cb-content-name'),
+    },
+    actions: {
+        search: getDataSelector('action', 'searchcontent'),
+        clearSearch: getDataSelector('action', 'clearsearchcontent'),
+    },
+    elements: {
+        cbfile: '.cb-file',
+        cbnavbarbreadcrumb: '.cb-navbar-breadbrumb',
+        cbnavbartotalsearch: '.cb-navbar-totalsearch',
+        clearsearch: '.input-group-append .clear-icon',
+        main: '#region-main',
+        searchicon: '.input-group-append .search-icon',
+        searchinput: '#searchinput',
+    },
+};
index 35499ce..197a36c 100644 (file)
@@ -42,7 +42,7 @@ class contentbank {
      *
      * @return string[] Array of contentbank contenttypes.
      */
-    private function get_enabled_content_types(): array {
+    public function get_enabled_content_types(): array {
         $enabledtypes = \core\plugininfo\contenttype::get_enabled_plugins();
         $types = [];
         foreach ($enabledtypes as $name) {
@@ -159,20 +159,28 @@ class contentbank {
      * Find the contents with %$search% in the contextid defined.
      * If contextid and search are empty, all contents are returned.
      * In all the cases, only the contents for the enabled contentbank-type plugins are returned.
+     * No content-type permissions are validated here. It is the caller responsability to check that the user can access to them.
+     * The only validation done here is, for each content, a call to the method $content->is_view_allowed().
      *
      * @param  string|null $search Optional string to search (for now it will search only into the name).
      * @param  int $contextid Optional contextid to search.
+     * @param  array $contenttypenames Optional array with the list of content-type names to search.
      * @return array The contents for the enabled contentbank-type plugins having $search as name and placed in $contextid.
      */
-    public function search_contents(?string $search = null, ?int $contextid = 0): array {
+    public function search_contents(?string $search = null, ?int $contextid = 0, ?array $contenttypenames = null): array {
         global $DB;
 
         $contents = [];
 
         // Get only contents for enabled content-type plugins.
-        $contenttypes = array_map(function($contenttypename) {
-            return "contenttype_$contenttypename";
-        }, $this->get_enabled_content_types());
+        $contenttypes = [];
+        $enabledcontenttypes = $this->get_enabled_content_types();
+        foreach ($enabledcontenttypes as $contenttypename) {
+            if (empty($contenttypenames) || in_array($contenttypename, $contenttypenames)) {
+                $contenttypes[] = "contenttype_$contenttypename";
+            }
+        }
+
         if (empty($contenttypes)) {
             // Early return if there are no content-type plugins enabled.
             return $contents;
@@ -193,7 +201,7 @@ class contentbank {
             $params['name'] = '%' . $DB->sql_like_escape($search) . '%';
         }
 
-        $records = $DB->get_records_select('contentbank_content', $sql, $params);
+        $records = $DB->get_records_select('contentbank_content', $sql, $params, 'name ASC');
         foreach ($records as $record) {
             $contentclass = "\\$record->contenttype\\content";
             $content = new $contentclass($record);
index ac1a855..471266e 100644 (file)
@@ -72,22 +72,22 @@ class bankcontent implements renderable, templatable {
      * @return stdClass
      */
     public function export_for_template(renderer_base $output): stdClass {
+        global $PAGE;
+
+        $PAGE->requires->js_call_amd('core_contentbank/search', 'init');
+
         $data = new stdClass();
         $contentdata = array();
         foreach ($this->contents as $content) {
             $record = $content->get_content();
-            $managerclass = $content->get_content_type().'\\contenttype';
-            if (class_exists($managerclass)) {
-                $manager = new $managerclass($this->context);
-                if ($manager->can_access()) {
-                    $name = $content->get_name();
-                    $contentdata[] = array(
-                        'name' => $name,
-                        'link' => $manager->get_view_url($record),
-                        'icon' => $manager->get_icon($name)
-                    );
-                }
-            }
+            $contenttypeclass = $content->get_content_type().'\\contenttype';
+            $contenttype = new $contenttypeclass($this->context);
+            $name = $content->get_name();
+            $contentdata[] = array(
+                'name' => $name,
+                'link' => $contenttype->get_view_url($record),
+                'icon' => $contenttype->get_icon($name)
+            );
         }
         $data->contents = $contentdata;
         $data->tools = $this->toolbar;
index bbf9786..f4f101b 100644 (file)
@@ -46,9 +46,19 @@ $PAGE->set_title($title);
 $PAGE->set_heading($title);
 $PAGE->set_pagetype('contenbank');
 
-// Get all contents managed by active plugins to render.
+// Get all contents managed by active plugins where the user has permission to render them.
 $cb = new \core_contentbank\contentbank();
-$foldercontents = $cb->search_contents($search, $contextid);
+$contenttypes = [];
+$enabledcontenttypes = $cb->get_enabled_content_types();
+foreach ($enabledcontenttypes as $contenttypename) {
+    $contenttypeclass = "\\contenttype_$contenttypename\\contenttype";
+    $contenttype = new $contenttypeclass($context);
+    if ($contenttype->can_access()) {
+        $contenttypes[] = $contenttypename;
+    }
+}
+
+$foldercontents = $cb->search_contents($search, $contextid, $contenttypes);
 
 // Get the toolbar ready.
 $toolbar = array ();
index 2212b81..396c297 100644 (file)
     }
 
 }}
-{{>core_contentbank/toolbar}}
+<div class="d-flex justify-content-between flex-column flex-sm-row">
+    <div class="cb-search-container mb-2">
+        {{>core_contentbank/bankcontent/search}}
+    </div>
+    <div class="cb-toolbar-container mb-2">
+        {{>core_contentbank/bankcontent/toolbar}}
+    </div>
+</div>
 <div class="content-bank-container pb-3 border">
     <div class="content-bank">
         <div class="cb-navbar bg-light p-2 border-bottom">
-            {{#pix}} i/folder {{/pix}}
+            <div class="cb-navbar-breadbrumb">
+                {{#pix}} i/folder {{/pix}}
+            </div>
+            <div class="cb-navbar-totalsearch d-none">
+            </div>
         </div>
         <div class="cb-content-wrapper d-flex flex-wrap p-2">
         {{#contents}}
-            <div class="cb-file position-relative mb-2">
+            <div class="cb-file position-relative mb-2" data-file="{{{name}}}">
                 <div class="p-2">
                     <div class="cb-thumbnail mb-1 text-center">
                         {{{ icon }}}
@@ -60,7 +71,7 @@
                     {{#link}}
                         <a href="{{{ link }}}" class="stretched-link" title="{{{name}}}">
                     {{/link}}
-                            <span class="cb-name word-break-all clamp-2 text-center" >
+                            <span class="cb-name word-break-all clamp-2 text-center" data-region="cb-content-name">
                                 {{{ name }}}
                             </span>
                     {{#link}}
diff --git a/contentbank/templates/bankcontent/search.mustache b/contentbank/templates/bankcontent/search.mustache
new file mode 100644 (file)
index 0000000..8d02863
--- /dev/null
@@ -0,0 +1,51 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_contentbank/bankcontent/search
+
+    Example context (json):
+    {}
+
+}}
+<div class="searchbar input-group" role="search">
+    <label for="searchinput">
+        <span class="sr-only">{{#str}} searchcontentbankbyname, contentbank {{/str}}</span>
+    </label>
+    <input type="text"
+           id="searchinput"
+           class="form-control searchinput border-right-0"
+           placeholder="{{#str}} search, core {{/str}}"
+           name="search"
+           autocomplete="off"
+    >
+    <div class="input-group-append">
+        <div class="input-group-text bg-transparent">
+            <div class="search-icon">
+                <button class="btn p-0 align-baseline icon-no-margin" data-action="searchcontent"
+                    aria-label="{{#str}} search, core {{/str}}">
+                    <span class="d-flex" aria-hidden="true">{{#pix}} a/search, core {{/pix}}</span>
+                </button>
+            </div>
+            <div class="clear-icon d-none">
+                <button class="btn p-0 align-baseline icon-no-margin" data-action="clearsearchcontent"
+                    aria-label="{{#str}} clearsearch, core {{/str}}">
+                    <span class="d-flex" aria-hidden="true">{{#pix}} e/cancel_solid_circle, core {{/pix}}</span>
+                </button>
+            </div>
+        </div>
+    </div>
+</div>
\ No newline at end of file
similarity index 91%
rename from contentbank/templates/toolbar.mustache
rename to contentbank/templates/bankcontent/toolbar.mustache
index 9ab8024..88f4a4c 100644 (file)
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core_contentbank/toolbar
+    @template core_contentbank/bankcontent/toolbar
 
     Example context (json):
     {
@@ -38,7 +38,7 @@
         {{#tools}}
             {{#link}}<a href="{{{ link }}}" title="{{{ name }}}">{{/link}}
                 <div class="cb-tool icon-no-margin btn btn-secondary btn-lg">
-                    {{#pix}} {{{ icon }}} {{/pix}}
+                    {{#pix}} {{{ icon }}} {{/pix}} <span class="sr-only">{{{ name }}}</span>
                 </div>
             {{#link}}</a>{{/link}}
         {{/tools}}
diff --git a/contentbank/tests/behat/search_content.feature b/contentbank/tests/behat/search_content.feature
new file mode 100644 (file)
index 0000000..940415f
--- /dev/null
@@ -0,0 +1,51 @@
+@core @core_contentbank @contentbank_h5p @_file_upload @javascript
+Feature: Search content in the content bank
+  In order to find easily content in the content bank
+  As an admin
+  I need to be able to search content in the content bank
+
+  Background:
+    Given the following "contentbank content" exist:
+        | contextid | contenttype       | user  | contentname          |
+        | 1         | contenttype_h5p   | admin | santjordi.h5p        |
+        | 1         | contenttype_h5p   | admin | santjordi_rose.h5p   |
+        | 1         | contenttype_h5p   | admin | SantJordi_book       |
+        | 1         | contenttype_h5p   | admin | Dragon_santjordi.h5p |
+        | 1         | contenttype_h5p   | admin | princess.h5p         |
+        | 1         | contenttype_h5p   | admin | mathsbook.h5p        |
+        | 1         | contenttype_h5p   | admin | historybook.h5p      |
+        | 1         | contenttype_h5p   | admin | santvicenc.h5p       |
+
+  Scenario: Admins can search content in the content bank
+    Given I log in as "admin"
+    And I am on site homepage
+    And I turn editing mode on
+    And I add the "Navigation" block if not present
+    And I expand "Site pages" node
+    And I click on "Content bank" "link"
+    And I should see "santjordi.h5p"
+    And "Clear search input" "button" should not exist
+    And I should not see "items found"
+    When I set the field "Search" to "book"
+    Then "Clear search input" "button" should exist
+    And I should see "3 items found"
+    And I should see "SantJordi_book"
+    And I should see "mathsbook.h5p"
+    And I should see "historybook.h5p"
+    And I set the field "Search" to "sant"
+    And "Clear search input" "button" should exist
+    And I should see "5 items found"
+    And I set the field "Search" to "santjordi"
+    And I should see "4 items found"
+    And I should see "santjordi.h5p"
+    And I should see "santjordi_rose.h5p"
+    And I should see "SantJordi_book"
+    And I should see "Dragon_santjordi.h5p"
+    And I click on "Clear search input" "button"
+    And "Clear search input" "button" should not exist
+    And I should not see "items found"
+    And I set the field "Search" to ".h5p"
+    And "Clear search input" "button" should exist
+    And I should see "7 items found"
+    And I set the field "Search" to "friend"
+    And I should see "0 items found"
index 2fcd66c..75d44c9 100644 (file)
 
 namespace core_contentbank;
 
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
-require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
-
 use stdClass;
 use context_system;
 use contenttype_testable\contenttype as contenttype;
+
 /**
  * Test for content bank contenttype class.
  *
@@ -46,6 +41,16 @@ use contenttype_testable\contenttype as contenttype;
  */
 class core_contenttype_content_testcase extends \advanced_testcase {
 
+    /**
+     * Setup to ensure that fixtures are loaded.
+     */
+    public static function setupBeforeClass(): void {
+        global $CFG;
+
+        require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+        require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+    }
+
     /**
      * Tests for behaviour of get_name().
      *
index e420456..1999559 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-defined('MOODLE_INTERNAL') || die();
+namespace core_contentbank;
 
-global $CFG;
-require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+use advanced_testcase;
+use context_course;
+use context_system;
 
 /**
  * Test for extensions manager.
@@ -38,6 +39,16 @@ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.p
  * @coversDefaultClass \core_contentbank\contentbank
  */
 class core_contentbank_testcase extends advanced_testcase {
+
+    /**
+     * Setup to ensure that fixtures are loaded.
+     */
+    public static function setupBeforeClass(): void {
+        global $CFG;
+
+        require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+    }
+
     /**
      * Data provider for test_get_extension_supporter.
      *
@@ -62,7 +73,7 @@ class core_contentbank_testcase extends advanced_testcase {
     public function test_get_extension(string $filename, string $expected) {
         $this->resetAfterTest();
 
-        $cb = new \core_contentbank\contentbank();
+        $cb = new contentbank();
 
         $extension = $cb->get_extension($filename);
         $this->assertEquals($expected, $extension);
@@ -93,7 +104,7 @@ class core_contentbank_testcase extends advanced_testcase {
     public function test_get_extension_supporter_for_admins(array $supporters, string $extension, string $expected) {
         $this->resetAfterTest();
 
-        $cb = new \core_contentbank\contentbank();
+        $cb = new contentbank();
         $expectedsupporters = [$extension => $expected];
 
         $systemcontext = context_system::instance();
@@ -117,7 +128,7 @@ class core_contentbank_testcase extends advanced_testcase {
     public function test_get_extension_supporter_for_users(array $supporters, string $extension, string $expected) {
         $this->resetAfterTest();
 
-        $cb = new \core_contentbank\contentbank();
+        $cb = new contentbank();
         $systemcontext = context_system::instance();
 
         // Set a user with no permissions.
@@ -142,7 +153,7 @@ class core_contentbank_testcase extends advanced_testcase {
     public function test_get_extension_supporter_for_teachers(array $supporters, string $extension, string $expected) {
         $this->resetAfterTest();
 
-        $cb = new \core_contentbank\contentbank();
+        $cb = new contentbank();
         $expectedsupporters = [$extension => $expected];
 
         $course = $this->getDataGenerator()->create_course();
@@ -168,7 +179,7 @@ class core_contentbank_testcase extends advanced_testcase {
     public function test_get_extension_supporter(array $supporters, string $extension, string $expected) {
         $this->resetAfterTest();
 
-        $cb = new \core_contentbank\contentbank();
+        $cb = new contentbank();
         $systemcontext = context_system::instance();
         $this->setAdminUser();
 
@@ -185,7 +196,8 @@ class core_contentbank_testcase extends advanced_testcase {
      * @param  int $expectedresult Expected result.
      * @param  array $contexts List of contexts where to create content.
      */
-    public function test_search_contents(?string $search, string $where, int $expectedresult, array $contexts = []): void {
+    public function test_search_contents(?string $search, string $where, int $expectedresult, array $contexts = [],
+            array $contenttypes = null): void {
         global $DB;
 
         $this->resetAfterTest();
@@ -218,8 +230,8 @@ class core_contentbank_testcase extends advanced_testcase {
         }
 
         // Search for some content.
-        $cb = new \core_contentbank\contentbank();
-        $contents = $cb->search_contents($search, $contextid);
+        $cb = new contentbank();
+        $contents = $cb->search_contents($search, $contextid, $contenttypes);
 
         $this->assertCount($expectedresult, $contents);
         if (!empty($contents) && !empty($search)) {
@@ -321,6 +333,13 @@ class core_contentbank_testcase extends advanced_testcase {
                 0,
                 []
             ],
+            'Search with unexisting content-type' => [
+                null,
+                'course',
+                0,
+                ['system', 'category', 'course'],
+                ['contenttype_unexisting'],
+            ],
         ];
     }
 
@@ -350,7 +369,7 @@ class core_contentbank_testcase extends advanced_testcase {
         $fs = get_file_storage();
         $dummyh5pfile = $fs->create_file_from_string($dummyh5p, 'Dummy H5Pcontent');
 
-        $cb = new \core_contentbank\contentbank();
+        $cb = new contentbank();
         $content = $cb->create_content_from_file($systemcontext, $USER->id, $dummyh5pfile);
 
         $this->assertEquals('contenttype_h5p', $content->get_content_type());
index e74bd1b..d5cf377 100644 (file)
 
 namespace core_contentbank;
 
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
-require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
-
 use stdClass;
 use context_system;
 use contenttype_testable\contenttype as contenttype;
@@ -64,6 +58,16 @@ class core_contenttype_contenttype_testcase extends \advanced_testcase {
     /** @var contenttype The contenttype instance. */
     protected $contenttype;
 
+    /**
+     * Setup to ensure that fixtures are loaded.
+     */
+    public static function setupBeforeClass(): void {
+        global $CFG;
+
+        require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.php');
+        require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
+    }
+
     /**
      * Tests get_contenttype_name result.
      *
index fd35f45..262e7e6 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-namespace core_contentbank;
+namespace core_contentbank\external;
 
 defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
 
-use core_contentbank\external\delete_content;
 use dml_missing_record_exception;
 use external_api;
 use externallib_advanced_testcase;
index 4b1bf6b..6a9ea67 100644 (file)
@@ -24,7 +24,7 @@
  * @since      Moodle 3.9
  */
 
-namespace core_contentbank;
+namespace core_contentbank\external;
 
 defined('MOODLE_INTERNAL') || die();
 
@@ -33,9 +33,6 @@ require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_contenttype.p
 require_once($CFG->dirroot . '/contentbank/tests/fixtures/testable_content.php');
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 
-use core_contentbank\external\delete_content;
-use core_contentbank\external\external;
-use core_contentbank\external\rename_content;
 use external_api;
 
 /**
index 09c3f0f..e737117 100644 (file)
@@ -25,8 +25,6 @@
 
 namespace core_contentbank;
 
-defined('MOODLE_INTERNAL') || die();
-
 use stdClass;
 use context_system;
 use context_coursecat;
index 3bf1b45..b3f553f 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js and b/course/amd/build/activitychooser.min.js differ
index 90b5d87..78345bc 100644 (file)
Binary files a/course/amd/build/activitychooser.min.js.map and b/course/amd/build/activitychooser.min.js.map differ
index ae2b3bb..abfc5d3 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js and b/course/amd/build/local/activitychooser/dialogue.min.js differ
index acc2129..355a979 100644 (file)
Binary files a/course/amd/build/local/activitychooser/dialogue.min.js.map and b/course/amd/build/local/activitychooser/dialogue.min.js.map differ
index d3b83d0..a66560e 100644 (file)
@@ -78,7 +78,6 @@ const registerListenerEvents = (courseId) => {
     events.forEach((event) => {
         document.addEventListener(event, async(e) => {
             if (e.target.closest(selectors.elements.sectionmodchooser)) {
-                const data = await fetchModuleData();
                 // We need to know who called this.
                 // Standard courses use the ID in the main section info.
                 const sectionDiv = e.target.closest(selectors.elements.section);
@@ -86,11 +85,31 @@ const registerListenerEvents = (courseId) => {
                 const button = e.target.closest(selectors.elements.sectionmodchooser);
                 // If we don't have a section ID use the fallback ID.
                 const caller = sectionDiv || button;
-                const favouriteFunction = partiallyAppliedFavouriteManager(data, caller.dataset.sectionid);
+
+                // We want to show the modal instantly but loading whilst waiting for our data.
+                let bodyPromiseResolver;
+                const bodyPromise = new Promise(resolve => {
+                    bodyPromiseResolver = resolve;
+                });
+
+                const sectionModal = buildModal(bodyPromise);
+
+                // Now we have a modal we should start fetching data.
+                const data = await fetchModuleData();
+
+                // Apply the section id to all the module instance links.
                 const builtModuleData = sectionIdMapper(data, caller.dataset.sectionid);
-                const sectionModal = await modalBuilder(builtModuleData);
 
-                ChooserDialogue.displayChooser(caller, sectionModal, builtModuleData, favouriteFunction);
+                ChooserDialogue.displayChooser(
+                    sectionModal,
+                    builtModuleData,
+                    partiallyAppliedFavouriteManager(data, caller.dataset.sectionid),
+                );
+
+                bodyPromiseResolver(await Templates.render(
+                    'core_course/activitychooser',
+                    templateDataBuilder(builtModuleData)
+                ));
             }
         });
     });
@@ -102,7 +121,7 @@ const registerListenerEvents = (courseId) => {
  *
  * @method sectionIdMapper
  * @param {Object} webServiceData Our original data from the Web service call
- * @param {Array} id The ID of the section we need to append to the links
+ * @param {Number} id The ID of the section we need to append to the links
  * @return {Array} [modules] with URL's built
  */
 const sectionIdMapper = (webServiceData, id) => {
@@ -114,15 +133,6 @@ const sectionIdMapper = (webServiceData, id) => {
     return newData.content_items;
 };
 
-/**
- * Build a modal on demand to save page load times
- *
- * @method modalBuilder
- * @param {Array} data our array of modules with section ID's applied in the URL field
- * @return {Object} Our modal that we are going to show the user
- */
-const modalBuilder = data => buildModal(templateDataBuilder(data));
-
 /**
  * Given an array of modules we want to figure out where & how to place them into our template object
  *
@@ -158,18 +168,22 @@ const templateDataBuilder = (data) => {
  * Given an object we want to build a modal ready to show
  *
  * @method buildModal
- * @param {Object} data The template data which contains arrays of modules
- * @return {Object} The modal for the calling section with everything already set up
+ * @param {Promise} bodyPromise
+ * @return {Object} The modal ready to display immediately and render body in later.
  */
-const buildModal = data => {
+const buildModal = bodyPromise => {
     return ModalFactory.create({
         type: ModalFactory.types.DEFAULT,
         title: getString('addresourceoractivity'),
-        body: Templates.render('core_course/activitychooser', data),
+        body: bodyPromise,
         large: true,
         templateContext: {
             classes: 'modchooser'
         }
+    })
+    .then(modal => {
+        modal.show();
+        return modal;
     });
 };
 
@@ -240,6 +254,7 @@ const partiallyAppliedFavouriteManager = (moduleData, sectionId) => {
             if (favourite) {
                 result.favourite = true;
 
+                // eslint-disable-next-line camelcase
                 newFaves.content_items = moduleData.content_items.filter(mod => mod.favourite === true);
 
                 const builtFaves = sectionIdMapper(newFaves, sectionId);
index 6a476cf..42a19b3 100644 (file)
@@ -42,6 +42,7 @@ import {debounce} from 'core/utils';
 const showModuleHelp = (carousel, moduleData) => {
     const help = carousel.find(selectors.regions.help)[0];
     help.innerHTML = '';
+    help.classList.add('m-auto');
 
     // Add a spinner.
     const spinnerPromise = addIconToContainer(help);
@@ -483,6 +484,37 @@ const searchModules = (modules, searchTerm) => {
     return searchResults;
 };
 
+/**
+ * Set up our tabindex information across the chooser.
+ *
+ * @method setupKeyboardAccessibility
+ * @param {Promise} modal Our created modal for the section
+ * @param {Map} mappedModules A map of all of the built module information
+ */
+const setupKeyboardAccessibility = (modal, mappedModules) => {
+    modal.getModal()[0].tabIndex = -1;
+
+    modal.getBodyPromise().then(body => {
+        $(selectors.elements.tab).on('shown.bs.tab', (e) => {
+            const activeSectionId = e.target.getAttribute("href");
+            const activeSectionChooserOptions = body[0]
+                .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
+            const firstChooserOption = activeSectionChooserOptions
+                .querySelector(selectors.regions.chooserOption.container);
+            const prevActiveSectionId = e.relatedTarget.getAttribute("href");
+            const prevActiveSectionChooserOptions = body[0]
+                .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));
+
+            // Disable the focus of every chooser option in the previous active section.
+            disableFocusAllChooserOptions(prevActiveSectionChooserOptions);
+            // Enable the focus of the first chooser option in the current active section.
+            toggleFocusableChooserOption(firstChooserOption, true);
+            initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions);
+        });
+        return;
+    }).catch(Notification.exception);
+};
+
 /**
  * Disable the focus of all chooser options in a specific container (section).
  *
@@ -500,13 +532,11 @@ const disableFocusAllChooserOptions = (sectionChooserOptions) => {
  * Display the module chooser.
  *
  * @method displayChooser
- * @param {HTMLElement} origin The calling button
- * @param {Object} modal Our created modal for the section
+ * @param {Promise} modalPromise Our created modal for the section
  * @param {Array} sectionModules An array of all of the built module information
  * @param {Function} partialFavourite Partially applied function we need to manage favourite status
  */
-export const displayChooser = (origin, modal, sectionModules, partialFavourite) => {
-
+export const displayChooser = (modalPromise, sectionModules, partialFavourite) => {
     // Make a map so we can quickly fetch a specific module's object for either rendering or searching.
     const mappedModules = new Map();
     sectionModules.forEach((module) => {
@@ -514,39 +544,18 @@ export const displayChooser = (origin, modal, sectionModules, partialFavourite)
     });
 
     // Register event listeners.
-    registerListenerEvents(modal, mappedModules, partialFavourite);
+    modalPromise.then(modal => {
+        registerListenerEvents(modal, mappedModules, partialFavourite);
 
-    // We want to focus on the action select when the dialog is closed.
-    modal.getRoot().on(ModalEvents.hidden, () => {
-        modal.destroy();
-    });
+        // We want to focus on the first chooser option element as soon as the modal is opened.
+        setupKeyboardAccessibility(modal, mappedModules);
 
-    // We want to focus on the first chooser option element as soon as the modal is opened.
-    modal.getRoot().on(ModalEvents.shown, () => {
-        modal.getModal()[0].tabIndex = -1;
-
-        modal.getBodyPromise()
-        .then(body => {
-            $(selectors.elements.tab).on('shown.bs.tab', (e) => {
-                const activeSectionId = e.target.getAttribute("href");
-                const activeSectionChooserOptions = body[0]
-                    .querySelector(selectors.regions.getSectionChooserOptions(activeSectionId));
-                const firstChooserOption = activeSectionChooserOptions
-                    .querySelector(selectors.regions.chooserOption.container);
-                const prevActiveSectionId = e.relatedTarget.getAttribute("href");
-                const prevActiveSectionChooserOptions = body[0]
-                    .querySelector(selectors.regions.getSectionChooserOptions(prevActiveSectionId));
-
-                // Disable the focus of every chooser option in the previous active section.
-                disableFocusAllChooserOptions(prevActiveSectionChooserOptions);
-                // Enable the focus of the first chooser option in the current active section.
-                toggleFocusableChooserOption(firstChooserOption, true);
-                initChooserOptionsKeyboardNavigation(body[0], mappedModules, activeSectionChooserOptions);
-            });
-            return;
-        })
-        .catch(Notification.exception);
-    });
+        // We want to focus on the action select when the dialog is closed.
+        modal.getRoot().on(ModalEvents.hidden, () => {
+            modal.destroy();
+        });
 
-    modal.show();
+        return modal;
+    })
+    .catch();
 };
index 9ad479f..eed3774 100644 (file)
@@ -378,15 +378,10 @@ class editor {
         $PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-editor.js' . $cachebuster), true);
         $PAGE->requires->js(autoloader::get_h5p_editor_library_url('scripts/h5peditor-init.js' . $cachebuster), true);
 
-        // Add translations.
+        // Load editor translations.
         $language = framework::get_language();
-        $languagescript = "language/{$language}.js";
-
-        if (!file_exists("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript))) {
-            $languagescript = 'language/en.js';
-        }
-        $PAGE->requires->js(autoloader::get_h5p_editor_library_url($languagescript . $cachebuster),
-            true);
+        $editorstrings = $this->get_editor_translations($language);
+        $PAGE->requires->data_for_js('H5PEditor.language.core', $editorstrings, false);
 
         // Add JavaScript settings.
         $root = $CFG->wwwroot;
@@ -423,6 +418,47 @@ class editor {
         $PAGE->requires->data_for_js('H5PIntegration', $settings, true);
     }
 
+    /**
+     * Get editor translations for the defined language.
+     * Check if the editor strings have been translated in Moodle.
+     * If the strings exist, they will override the existing ones in the JS file.
+     *
+     * @param string $language The language for the translations to be returned.
+     * @return array The editor string translations.
+     */
+    private function get_editor_translations(string $language): array {
+        global $CFG;
+
+        // Add translations.
+        $languagescript = "language/{$language}.js";
+
+        if (!file_exists("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript))) {
+            $languagescript = 'language/en.js';
+        }
+
+        // Check if the editor strings have been translated in Moodle.
+        // If the strings exist, they will override the existing ones in the JS file.
+
+        // Get existing strings from current JS language file.
+        $langcontent = file_get_contents("{$CFG->dirroot}" . autoloader::get_h5p_editor_library_base($languagescript));
+
+        // Get only the content between { } (for instance, ; at the end of the file has to be removed).
+        $langcontent = substr($langcontent, 0, strpos($langcontent, '}', -0) + 1);
+        $langcontent = substr($langcontent, strpos($langcontent, '{'));
+
+        // Parse the JS language content and get a PHP array.
+        $editorstrings = helper::parse_js_array($langcontent);
+        foreach ($editorstrings as $key => $value) {
+            $stringkey = 'editor:'.strtolower(trim($key));
+            $value = autoloader::get_h5p_string($stringkey, $language);
+            if (!empty($value)) {
+                $editorstrings[$key] = $value;
+            }
+        }
+
+        return $editorstrings;
+    }
+
     /**
      * Preprocess the data sent through the form to the H5P JS Editor Library.
      *
index bac7950..c4b575c 100644 (file)
@@ -62,7 +62,14 @@ class editor_framework implements H5peditorStorage {
         $librarykey = helper::get_cache_librarykey(core::record_to_string($library));
         $cachekey = "{$librarykey}/{$lang}";
         $translation = $langcache->get($cachekey);
-        if ($translation) {
+
+        if ($translation !== false) {
+            // When there is no translation we store it in the cache as `null`.
+            // This API requires it be returned as `false`.
+            if ($translation === null) {
+                return false;
+            }
+
             return $translation;
         }
 
@@ -88,14 +95,19 @@ class editor_framework implements H5peditorStorage {
 
         $result = $DB->get_record_sql($sql, $params);
 
-        if (!empty($result)) {
-            // If the JS language file exists, its content should be returned.
-            $fs = get_file_storage();
-            $file = $fs->get_file_by_hash($result->pathnamehash);
-            $translation = $file->get_content();
+        if (empty($result)) {
+            // Save the fact that there is no translation into the cache.
+            // The cache API cannot handle setting a literal `false` value so conver to `null` instead.
+            $langcache->set($cachekey, null);
+
+            return false;
         }
 
-        // Save translation into the cache (even if there is no translation for this language).
+        // Save translation into the cache, and return its content.
+        $fs = get_file_storage();
+        $file = $fs->get_file_by_hash($result->pathnamehash);
+        $translation = $file->get_content();
+
         $langcache->set($cachekey, $translation);
 
         return $translation;
index 41b1c9d..93780fa 100644 (file)
@@ -82,7 +82,6 @@ class helper {
         return false;
     }
 
-
     /**
      * Get the error messages stored in our H5P framework.
      *
@@ -410,4 +409,24 @@ class helper {
         // Remove whitespaces and replace '.' to '_'.
         return str_replace('.', '_', str_replace(' ', '', $library));
     }
+
+    /**
+     * Parse a JS array to a PHP array.
+     *
+     * @param  string $jscontent The JS array to parse to PHP array.
+     * @return array The JS array converted to PHP array.
+     */
+    public static function parse_js_array(string $jscontent): array {
+        $jsarray = preg_split('/,\n\s+/', substr($jscontent, 0, -1));
+        $jsarray = preg_replace('~{?\\n~', '', $jsarray);
+
+        $strings = [];
+        foreach ($jsarray as $key => $value) {
+            $splitted = explode(":", $value, 2);
+            $value = preg_replace("/^['|\"](.*)['|\"]$/", "$1", trim($splitted[1], ' ,'));
+            $strings[ trim($splitted[0]) ] = str_replace("\'", "'", $value);
+        }
+
+        return $strings;
+    }
 }
index 27b5f4e..0f7324b 100644 (file)
@@ -149,6 +149,17 @@ class autoloader {
         return component_class_callback(self::get_handler_classname(), 'get_h5p_editor_library_base', [$filepath]);
     }
 
+    /**
+     * Returns a localized string, if it exists in the h5plib plugin and the value it's different from the English version.
+     *
+     * @param string $identifier The key identifier for the localized string
+     * @param string $language Language to get the localized string.
+     * @return string|null The localized string or null if it doesn't exist in this H5P library plugin.
+     */
+    public static function get_h5p_string(string $identifier, string $language): ?string {
+        return component_class_callback(self::get_handler_classname(), 'get_h5p_string', [$identifier, $language]);
+    }
+
     /**
      * Register the H5P autoloader.
      */
index ca59674..c9c5389 100644 (file)
@@ -123,6 +123,31 @@ abstract class handler {
         return new \moodle_url(static::get_h5p_editor_library_base($filepath), $params);
     }
 
+    /**
+     * Returns a localized string, if it exists in the h5plib plugin and the value it's different from the English version.
+     *
+     * @param string $identifier The key identifier for the localized string
+     * @param string $language Language to get the localized string.
+     * @return string|null The localized string or null if it doesn't exist in this H5P library plugin.
+     */
+    public static function get_h5p_string(string $identifier, string $language): ?string {
+        $value = null;
+        $h5pversion = static::get_h5p_version();
+        $component = 'h5plib_v' . $h5pversion;
+        if (get_string_manager()->string_exists($identifier, $component)) {
+            $defaultmoodlelang = 'en';
+            // In Moodle, all the English strings always will exist because they have to be declared in order to let users
+            // to translate them. That's why, this method will only replace existing key if the value is different from
+            // the English version and the current language is not English.
+            $string = new \lang_string($identifier, $component);
+            if ($language === $defaultmoodlelang || $string->out($language) !== $string->out($defaultmoodlelang)) {
+                $value = $string->out($language);
+            }
+        }
+
+        return $value;
+    }
+
     /**
      * Return the list of classes with their location within the joubel directory.
      *
index 34f3c9f..e030a16 100644 (file)
@@ -8,6 +8,13 @@ Import procedure:
 - Copy all the files from the folder repository in this directory.
 - In the method ns.LibrarySelector.prototype.appendTo (scripts/h5peditor-library-selector.js),
   comment the line "this.$selector.appendTo($element);" to avoid the display of the Hub Selector.
+- Review strings in joubel/editor/language/en.js and compare them with
+existing ones in lang/en/h5plib_vXXX.php: add the new ones and remove the
+unexisting ones. Remember to use the AMOS script commands, such CPY, to copy
+all the existing strings from the previous version. As you'll see, all the
+strings in en.js have been converted following these rules:
+  * Prefix  "editor:" has been added.
+  * Keys have been lowercased.
 
 Removed:
  * composer.json
index 222f128..10e1f71 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['editor:atoz'] = 'A to Z';
+$string['editor:add'] = 'Add';
+$string['editor:addauthor'] = 'Save author';
+$string['editor:addentity'] = 'Add :entity';
+$string['editor:addfile'] = 'Add file';
+$string['editor:addnewchange'] = 'Add new change';
+$string['editor:addtitle'] = 'Add title';
+$string['editor:addvideodescription'] = 'H5P supports all external video sources formatted as mp4, webm or ogv, like Vimeo Pro, and has support for YouTube links.';
+$string['editor:avtablistlabel'] = 'Insert using';
+$string['editor:belowmin'] = 'The :property value is below the minimum of :min.';
+$string['editor:cancel'] = 'Cancel';
+$string['editor:cancellabel'] = 'Cancel';
+$string['editor:changefile'] = 'Change file';
+$string['editor:changelanguage'] = 'Change language to :language?';
+$string['editor:changelibrary'] = 'Change content type?';
+$string['editor:changelogdescription'] = 'Some licenses require that changes made to the original work, or derivatives are logged and displayed. You may log your changes here for licensing reasons or just to allow yourself and others to keep track of the changes made to this content.';
+$string['editor:close'] = 'Close';
+$string['editor:commonfields'] = 'Text overrides and translations';
+$string['editor:commonfieldsdescription'] = 'Here you can edit settings or translate texts used in this content.';
+$string['editor:confirmchangelibrary'] = 'By doing this you will lose all work done with the current content type. Are you sure you wish to change content type?';
+$string['editor:confirmdeletechangelog'] = 'Are you sure you would like to delete this change log entry?';
+$string['editor:confirmimageremoval'] = 'This will remove your image. Are you sure you wish to proceed?';
+$string['editor:confirmpastebuttontext'] = 'Replace content';
+$string['editor:confirmpastecontent'] = 'By doing this you will replace the current content with the content from your clipboard. The current content will be lost. Are you sure you wish to continue?';
+$string['editor:confirmremoval'] = 'Are you sure you wish to remove this :type?';
+$string['editor:confirmremoveauthor'] = 'Are you sure you would like to remove this author?';
+$string['editor:contenttypebackbuttonlabel'] = 'Back';
+$string['editor:contenttypecacheoutdated'] = 'Content type list outdated';
+$string['editor:contenttypecacheoutdateddesc'] = 'Your site is having difficulties connecting to H5P.org to check for content type updates. You may not be able to update or install new content types.';
+$string['editor:contenttypedemobuttonlabel'] = 'Content Demo';
+$string['editor:contenttypedetailbuttonlabel'] = 'Details';
+$string['editor:contenttypegetbuttonlabel'] = 'Get';
+$string['editor:contenttypeiconalttext'] = 'Icon';
+$string['editor:contenttypeinstallbuttonlabel'] = 'Install';
+$string['editor:contenttypeinstallerror'] = ':contentType could not be installed. Contact your administrator.';
+$string['editor:contenttypeinstallsuccess'] = ':contentType successfully installed!';
+$string['editor:contenttypeinstallingbuttonlabel'] = 'Installing';
+$string['editor:contenttypelicensepaneltitle'] = 'License';
+$string['editor:contenttypenotinstalled'] = 'Content type not installed';
+$string['editor:contenttypenotinstalleddesc'] = 'You do not have permission to install content types.';
+$string['editor:contenttypeowner'] = 'By :owner';
+$string['editor:contenttyperestricted'] = 'Restricted content type';
+$string['editor:contenttyperestricteddesc'] = 'The use of this content type has been restricted by an administrator.';
+$string['editor:contenttypesearchfieldplaceholder'] = 'Search for Content Types';
+$string['editor:contenttypesectionall'] = 'All Content Types';
+$string['editor:contenttypeunsupportedapiversioncontent'] = 'Contact your system administrator to provide you with the necessary updates';
+$string['editor:contenttypeunsupportedapiversiontitle'] = 'This content type requires a newer core version';
+$string['editor:contenttypeupdateavailable'] = 'Update available';
+$string['editor:contenttypeupdatebuttonlabel'] = 'Update';
+$string['editor:contenttypeupdatesuccess'] = ':contentType successfully updated!';
+$string['editor:contenttypeupdatingbuttonlabel'] = 'Updating';
+$string['editor:contenttypeusebuttonlabel'] = 'Use';
+$string['editor:contributetranslations'] = 'If you want to complete the translation for :language you can learn about <a href=":url" target="_new">contributing translations to H5P</a>';
+$string['editor:copiedbutton'] = 'Copied';
+$string['editor:copiedtoclipboard'] = 'Content is copied to the clipboard';
+$string['editor:copybutton'] = 'Copy';
+$string['editor:copytoclipboard'] = 'Copy H5P content to the clipboard';
+$string['editor:createcontenttablabel'] = 'Create Content';
+$string['editor:currentmenuselected'] = 'current selection';
+$string['editor:editcopyright'] = 'Edit copyright';
+$string['editor:editimage'] = 'Edit image';
+$string['editor:editmode'] = 'Editing mode';
+$string['editor:enteraudiotitle'] = 'Paste link or other audio source URL';
+$string['editor:enteraudiourl'] = 'Enter audio source URL';
+$string['editor:enterfullscreenbuttonlabel'] = 'Enter fullscreen';
+$string['editor:entervideotitle'] = 'Paste YouTube link or other video source URL';
+$string['editor:entervideourl'] = 'Enter video source URL or YouTube link';
+$string['editor:errorcalculatingmaxscore'] = 'Could not calculate the max score for this content. The max score is assumed to be 0. Contact your administrator if this isn’t correct.';
+$string['editor:errorcommunicatinghubcontent'] = 'An error occured. Please try again.';
+$string['editor:errorcommunicatinghubtitle'] = 'Not able to communicate with hub.';
+$string['editor:errorheader'] = 'An error occured';
+$string['editor:errornotsupported'] = 'Parameters contain %used which is not supported.';
+$string['editor:errorparamsbroken'] = 'Parameters are broken.';
+$string['editor:errortoohighversion'] = 'Parameters contain %used while only %supported or earlier are supported.';
+$string['editor:example'] = 'Example';
+$string['editor:exceedsmax'] = 'The :property value exceeds the maximum of :max.';
+$string['editor:exitfullscreenbuttonlabel'] = 'Exit fullscreen';
+$string['editor:expandcollapse'] = 'Expand/Collapse';
+$string['editor:filetolarge'] = 'The file you are trying to upload might be too large.';
+$string['editor:fillinthefieldsbelow'] = 'Fill in the fields below';
+$string['editor:gethelp'] = 'Get help';
+$string['editor:h5pfileuploadservererrorcontent'] = 'An unexpected error occured. Check your server error log for\' +  \' more details.';
+$string['editor:h5pfileuploadservererrortitle'] = 'The H5P file could not be uploaded';
+$string['editor:h5pfilevalidationfailedcontent'] = 'Make sure the uploaded H5P contains valid H5P content. H5P\' +  \' files containing only libraries should be uploaded through the H5P Libraries page.';
+$string['editor:h5pfilevalidationfailedtitle'] = 'Could not validate H5P file.';
+$string['editor:h5pfilewrongextensioncontent'] = 'Only files with the .h5p extension are allowed.';
+$string['editor:h5pfilewrongextensiontitle'] = 'The selected file could not be uploaded';
+$string['editor:height'] = 'height';
+$string['editor:hide'] = 'Hide';
+$string['editor:hideimportantinstructions'] = 'Hide important instructions';
+$string['editor:hubpanellabel'] = 'Select content type';
+$string['editor:illegaldecimalnumber'] = ':property can only contain numbers with max :decimals decimals.';
+$string['editor:imagelightboxprogress'] = ':num of :total';
+$string['editor:imagelightboxtitle'] = 'Images';
+$string['editor:importantinstructions'] = 'Important instructions';
+$string['editor:insert'] = 'Insert';
+$string['editor:invalidformat'] = 'Field value contains an invalid format or characters that are forbidden.';
+$string['editor:language'] = 'Language';
+$string['editor:librarymissing'] = 'Missing required library %lib.';
+$string['editor:licensecandistribute'] = 'Can distribute';
+$string['editor:licensecanholdliable'] = 'Can hold liable';
+$string['editor:licensecanmodify'] = 'Can modify';
+$string['editor:licensecansublicense'] = 'Can sublicense';
+$string['editor:licensecanusecommercially'] = 'Can use commercially';
+$string['editor:licensecannotholdliable'] = 'Cannot hold liable';
+$string['editor:licensedescription'] = 'Some of the features of this license are indicated below. Click the info icon above to read the original license text.';
+$string['editor:licensefetchdetailsfailed'] = 'Failed fetching license details';
+$string['editor:licensemodalsubtitle'] = 'Select a license to view information about proper usage';
+$string['editor:licensemodaltitle'] = 'License Details';
+$string['editor:licensemustincludecopyright'] = 'Must include copyright';
+$string['editor:licensemustincludelicense'] = 'Must include license';
+$string['editor:licenseunspecified'] = 'Unspecified';
+$string['editor:listbelowmin'] = 'The list needs at least :min items for the content to function properly.';
+$string['editor:listexceedsmax'] = 'The list exceeds the maximum of :max items.';
+$string['editor:listlabel'] = 'List';
+$string['editor:loading'] = 'Loading, please wait...';
+$string['editor:loadingimageeditor'] = 'Loading image editor, please wait...';
+$string['editor:logthischange'] = 'Log this change';
+$string['editor:loggedchanges'] = 'Logged changes';
+$string['editor:maxscoresemanticsmissing'] = 'Could not find the expected semantics in the content.';
+$string['editor:metadata'] = 'Metadata';
+$string['editor:metadatasharingandlicensinginfo'] = 'Metadata (sharing and licensing info)';
+$string['editor:missingproperty'] = 'Field :index is missing its :property property.';
+$string['editor:missingtranslation'] = '[Missing translation :key]';
+$string['editor:newchangehasbeenlogged'] = 'New change has been logged';
+$string['editor:newestfirst'] = 'Newest First';
+$string['editor:nextimage'] = 'Next image';
+$string['editor:nochangeshavebeenlogged'] = 'No changes have been logged';
+$string['editor:nocontenttypesavailable'] = 'No content types are available';
+$string['editor:nocontenttypesavailabledesc'] = 'Your site is having difficulties connecting to H5P.org and listing the available content types.';
+$string['editor:nofollow'] = 'Cannot follow field ":path".';
+$string['editor:nolanguagessupported'] = 'No languages supported';
+$string['editor:noresultsfound'] = 'No results found';
+$string['editor:noresultsfounddesc'] = 'There is no content type that matches your search criteria.';
+$string['editor:nosemantics'] = 'Error, could not load the content type form.';
+$string['editor:notalltextschanged'] = 'Not all texts were changed, there is only partial coverage for :language.';
+$string['editor:notimagefield'] = '":path" is not an image.';
+$string['editor:notimageordimensionsfield'] = '":path" is not an image or dimensions field.';
+$string['editor:numresults'] = ':num results';
+$string['editor:numberfield'] = 'number field';
+$string['editor:ok'] = 'OK';
+$string['editor:onlynumbers'] = 'The :property value can only contain numbers.';
+$string['editor:or'] = 'or';
+$string['editor:orderitemdown'] = 'Order item down';
+$string['editor:orderitemup'] = 'Order item up';
+$string['editor:outofstep'] = 'The :property value can only be changed in steps of :step.';
+$string['editor:pasteandreplacebutton'] = 'Paste & Replace';
+$string['editor:pasteandreplacefromclipboard'] = 'Replace existing content with H5P Content from the clipboard';
+$string['editor:pastebutton'] = 'Paste';
+$string['editor:pastecontent'] = 'Replace Content';
+$string['editor:pastecontentnotsupported'] = 'The content in the H5P clipboard is not supported in this context';
+$string['editor:pastecontentrestricted'] = 'The content in the clipboard has been restricted on this site';
+$string['editor:pasteerror'] = 'Cannot paste from clipboard';
+$string['editor:pastefromclipboard'] = 'Paste H5P content from the clipboard';
+$string['editor:pastenocontent'] = 'No H5P content on the clipboard';
+$string['editor:pastetoonew'] = 'The content in the H5P clipboard is of a higher version (:clip) than what is supported in this context (:local), if possible try to have this content upgraded first, and then try pasting the content here again.';
+$string['editor:pastetooold'] = 'The content in the H5P clipboard is of a lower version (:clip) than what is supported in this context (:local), if possible try to have the content you want to paste upgraded, copy it again and try pasting it here.';
+$string['editor:popularfirst'] = 'Popular First';
+$string['editor:previousimage'] = 'Previous image';
+$string['editor:proceedbuttonlabel'] = 'Proceed to save';
+$string['editor:readless'] = 'Read less';
+$string['editor:readmore'] = 'Read more';
+$string['editor:recentlyusedfirst'] = 'Recently Used First';
+$string['editor:reloadbuttonlabel'] = 'Reload';
+$string['editor:removefile'] = 'Remove file';
+$string['editor:removeimage'] = 'Remove image';
+$string['editor:removeitem'] = 'Remove item';
+$string['editor:requiredproperty'] = 'The :property is required and must have a value.';
+$string['editor:resettooriginallabel'] = 'Reset to original';
+$string['editor:savelabel'] = 'Save';
+$string['editor:savemetadata'] = 'Save metadata';
+$string['editor:screenshots'] = 'Screenshots';
+$string['editor:scriptmissing'] = 'Could not load upgrades script for %lib.';
+$string['editor:searchresults'] = 'Search Results';
+$string['editor:selectfiletoupload'] = 'Select file to upload';
+$string['editor:selectlibrary'] = 'Select the library you wish to use for your content.';
+$string['editor:semanticserror'] = 'Semantics error: :error';
+$string['editor:show'] = 'Show';
+$string['editor:showimportantinstructions'] = 'Show instructions';
+$string['editor:tabtitlebasicfileupload'] = 'File Upload';
+$string['editor:tabtitleinputlinkurl'] = 'Link/URL';
+$string['editor:textfield'] = 'text field';
+$string['editor:thecontenttype'] = 'the content type';
+$string['editor:thiswillpotentially'] = 'This will potentially reset all the text and translations. You can\'t undo this. The content itself will not be changed. Do you want to proceed?';
+$string['editor:title'] = 'Title';
+$string['editor:toolong'] = 'Field value is too long, should contain :max letters or less.';
+$string['editor:tryagain'] = 'Try again';
+$string['editor:tutorial'] = 'Tutorial';
+$string['editor:unabletointerpreterror'] = 'Unable to interpret response.';
+$string['editor:unabletointerpretsolution'] = 'Please check your error log.';
+$string['editor:unknownfieldpath'] = 'Unable to find ":path".';
+$string['editor:unknownfileuploaderror'] = 'Unknown file upload error';
+$string['editor:unknownlibrary'] = 'Unfortunately, the selected content type \'%lib\' isn\'t installed on this system.';
+$string['editor:untitled'] = 'Untitled :libraryTitle';
+$string['editor:uploadaudiotitle'] = 'Upload audio file';
+$string['editor:uploaderror'] = 'File Upload Error';
+$string['editor:uploadfilebuttonchangelabel'] = 'Change file';
+$string['editor:uploadfilebuttonlabel'] = 'Upload a file';
+$string['editor:uploadinstructionscontent'] = 'You may start with examples from <a href="https://h5p.org/content-types-and-applications" target="blank">H5P.org</a>.';
+$string['editor:uploadinstructionstitle'] = 'Upload an H5P file.';
+$string['editor:uploadplaceholder'] = 'No file chosen';
+$string['editor:uploadsuccess'] = ':title was successfully uploaded!';
+$string['editor:uploadtablabel'] = 'Upload';
+$string['editor:uploadvideotitle'] = 'Upload video file';
+$string['editor:uploading'] = 'Uploading, please wait...';
+$string['editor:uploadingthrobber'] = 'Now uploading...';
+$string['editor:usedforsearchingreportsandcopyrightinformation'] = 'Used for searching, reports and copyright information';
+$string['editor:videoquality'] = 'Video quality label';
+$string['editor:videoqualitydefaultlabel'] = 'Quality :index';
+$string['editor:videoqualitydescription'] = 'This label helps the user identify the current quality of the video. E.g. 1080p, 720p, HD or Mobile';
+$string['editor:warningchangebrowsingtoseeresults'] = 'Click <em>All</em> to get the list of all the content types you can install.';
+$string['editor:warningnocontenttypesinstalled'] = 'You don\'t have any content types installed.';
+$string['editor:warningupdateavailablebody'] = 'Update to the latest version for an improved experience.';
+$string['editor:warningupdateavailabletitle'] = 'A new version of :contentType is available.';
+$string['editor:width'] = 'width';
 $string['pluginname'] = 'H5P framework v1.24';
 $string['pluginname_help'] = 'H5P framework version 1.24.';
 $string['privacy:metadata'] = 'The H5P framework v1.24 does not store any personal data.';
index fb007bf..dff6022 100644 (file)
@@ -214,15 +214,6 @@ class editor_testcase extends advanced_testcase {
             $item = $h5pcorepath . $item . $cachebuster;
         });
 
-        // Add translation script.
-        $language = framework::get_language();
-        $languagescript = "language/{$language}.js";
-
-        if (!file_exists($CFG->dirroot . autoloader::get_h5p_editor_library_base($languagescript))) {
-            $languagescript = 'language/en.js';
-        }
-        $expectedjs[] = autoloader::get_h5p_editor_library_url($languagescript . $cachebuster)->out();
-
         $expectedjs[] = (new \moodle_url('/h5p/js/h5p_overrides.js' . $cachebuster))->out();
         $expectedjs[] = autoloader::get_h5p_editor_library_url('scripts/h5peditor-editor.js' . $cachebuster)->out();
         $expectedjs[] = autoloader::get_h5p_editor_library_url('scripts/h5peditor-init.js' . $cachebuster)->out();
diff --git a/h5p/tests/local/library/handler_test.php b/h5p/tests/local/library/handler_test.php
new file mode 100644 (file)
index 0000000..5d92f50
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Testing the H5P library handler.
+ *
+ * @package    core_h5p
+ * @category   test
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_h5p\local\library;
+
+use advanced_testcase;
+
+/**
+ * Test class covering the H5P library handler.
+ *
+ * @package    core_h5p
+ * @copyright  2020 Sara Arjona <sara@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class handler_testcase extends advanced_testcase {
+
+    /**
+     * Test the behaviour of get_h5p_string().
+     *
+     * @dataProvider get_h5p_string_provider
+     * @param  string $identifier      The key identifier for the localized string.
+     * @param  string $expectedresult  Expected result.
+     * @param  string $lang            Language to get the localized string.
+     */
+    public function test_get_h5p_string(string $identifier, ?string $expectedresult, ?string $lang = 'en'): void {
+        $result = autoloader::get_h5p_string($identifier, $lang);
+        $this->assertEquals($expectedresult, $result);
+    }
+
+    /**
+     * Data provider for test_get_h5p_string().
+     *
+     * @return array
+     */
+    public function get_h5p_string_provider(): array {
+        return [
+            'Existing string in h5plib plugin' => [
+                'editor:add',
+                'Add',
+            ],
+            'Unexisting translation for an existing string in h5plib plugin (es)' => [
+                'editor:add',
+                null,
+                'es',
+            ],
+            'Unexisting string in h5plib plugin' => [
+                'unexistingstring',
+                null,
+            ],
+            'Unexisting translation for an unexisting string in h5plib plugin (es)' => [
+                'unexistingstring',
+                null,
+                'es',
+            ],
+        ];
+    }
+}
index 0c08c50..c59daba 100644 (file)
@@ -127,6 +127,7 @@ $string['configgeneralblocks'] = 'Sets the default for including blocks in a bac
 $string['configgeneralcalendarevents'] = 'Sets the default for including calendar events in a backup.';
 $string['configgeneralcomments'] = 'Sets the default for including comments in a backup.';
 $string['configgeneralcompetencies'] = 'Sets the default for including competencies in a backup.';
+$string['configgeneralcontentbankcontent'] = 'Sets the default for including content bank content in a backup.';
 $string['configgeneralfiles'] = 'Sets the default for including files in a backup. Please note: Disabling this setting will result in a backup which only includes references to files. This is not a problem if the backup is restored on the same site and the files have not been deleted according to the setting \'Clean up trash pool files\' (filescleanupperiod).';
 $string['configgeneralfilters'] = 'Sets the default for including filters in a backup.';
 $string['configgeneralhistories'] = 'Sets the default for including user history within a backup.';
@@ -143,6 +144,7 @@ $string['configrestoreblocks'] = 'Sets the default for restoring blocks.';
 $string['configrestorecalendarevents'] = 'Sets the default for restoring calendar events.';
 $string['configrestorecomments'] = 'Sets the default for restoring comments.';
 $string['configrestorecompetencies'] = 'Sets the default for restoring competencies.';
+$string['configrestorecontentbankcontent'] = 'Sets the default for restoring content bank content.';
 $string['configrestoreenrolments'] = 'Sets the default for restoring enrolment methods.';
 $string['configrestorefilters'] = 'Sets the default for restoring filters.';
 $string['configrestorehistories'] = 'Sets the default for restoring user history if it was included in the backup.';
@@ -202,6 +204,7 @@ $string['generalblocks'] = 'Include blocks';
 $string['generalcalendarevents'] = 'Include calendar events';
 $string['generalcomments'] = 'Include comments';
 $string['generalcompetencies'] = 'Include competencies';
+$string['generalcontentbankcontent'] = 'Include content bank content';
 $string['generalenrolments'] = 'Include enrolment methods';
 $string['generalfiles'] = 'Include files';
 $string['generalfilters'] = 'Include filters';
@@ -333,6 +336,7 @@ $string['rootsettingfilters'] = 'Include filters';
 $string['rootsettingfiles'] = 'Include files';
 $string['rootsettingcomments'] = 'Include comments';
 $string['rootsettingcalendarevents'] = 'Include calendar events';
+$string['rootsettingcontentbankcontent'] = 'Include content bank content';
 $string['rootsettinguserscompletion'] = 'Include user completion details';
 $string['rootsettingquestionbank'] = 'Include question bank';
 $string['rootsettinglogs'] = 'Include course logs';
index 06958d6..e111a7c 100644 (file)
@@ -37,6 +37,7 @@ $string['deletecontent'] = 'Delete content';
 $string['deletecontentconfirm'] = 'Are you sure you want to delete the content <em>\'{$a->name}\'</em> and all associated files? This action cannot be undone.';
 $string['file'] = 'Upload content';
 $string['file_help'] = 'Files may be stored in the content bank for use in courses. Only files used by content types enabled on the site may be uploaded.';
+$string['itemsfound'] = '{$a} items found';
 $string['name'] = 'Content';
 $string['nopermissiontodelete'] = 'You do not have permission to delete content.';
 $string['nopermissiontomanage'] = 'You do not have permission to manage content.';
@@ -50,6 +51,7 @@ $string['privacy:metadata:contentbankcontent'] = 'Stores the content of the cont
 $string['privacy:metadata:userid'] = 'The ID of the user creating or modifying content bank content.';
 $string['rename'] = 'Rename';
 $string['renamecontent'] = 'Rename content';
+$string['searchcontentbankbyname'] = 'Search for content by name';
 $string['timecreated'] = 'Time created';
 $string['unsupported'] = 'This content type is not supported.';
 $string['upload'] = 'Upload';
index bf0f119..1b3b1b4 100644 (file)
Binary files a/lib/amd/build/modal.min.js and b/lib/amd/build/modal.min.js differ
index a2acd22..6ebad58 100644 (file)
Binary files a/lib/amd/build/modal.min.js.map and b/lib/amd/build/modal.min.js.map differ
index 154f974..d04924f 100644 (file)
@@ -322,6 +322,9 @@ define([
             var contentPromise = null;
             body.css('overflow', 'hidden');
 
+            // Ensure that the `value` is a jQuery Promise.
+            value = $.when(value);
+
             if (value.state() == 'pending') {
                 // We're still waiting for the body promise to resolve so
                 // let's show a loading icon.
index a1ad313..cbb6905 100644 (file)
@@ -219,7 +219,7 @@ class behat_core_generator extends behat_generator_base {
             ],
             'contentbank content' => [
                 'datagenerator' => 'contentbank_content',
-                'required' => array('course', 'contenttype', 'user', 'contentname'),
+                'required' => array('contenttype', 'user', 'contentname'),
                 'switchids' => array('course' => 'courseid', 'user' => 'userid')
             ],
         ];
@@ -829,7 +829,14 @@ class behat_core_generator extends behat_generator_base {
      * @return void
      */
     protected function process_contentbank_content(array $data) {
-        $context = context_course::instance($data['courseid']);
+        if (empty($data['contextid'])) {
+            if (empty($data['courseid'])) {
+                throw new Exception('contentbank_content requires the field course or contextid to be specified');
+            }
+            $context = context_course::instance($data['courseid']);
+        } else {
+            $context = context::instance_by_id($data['contextid']);
+        }
         $contenttypeclass = "\\".$data['contenttype']."\\contenttype";
         if (class_exists($contenttypeclass)) {
             $contenttype = new $contenttypeclass($context);
index 917e5b5..bea9252 100644 (file)
@@ -275,6 +275,8 @@ function cron_run_inner_scheduled_task(\core\task\task_base $task) {
         }
         \core\task\manager::scheduled_task_failed($task);
     } finally {
+        // Reset back to the standard admin user.
+        cron_setup_user();
         cron_prepare_core_renderer(true);
     }
     get_mailer('close');
index 597077c..b8a5c03 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js and b/lib/table/amd/build/dynamic.min.js differ
index d14c064..5c5e815 100644 (file)
Binary files a/lib/table/amd/build/dynamic.min.js.map and b/lib/table/amd/build/dynamic.min.js.map differ
index 18e35fb..a53dc6f 100644 (file)
@@ -24,6 +24,7 @@
 import {fetch as fetchTableData} from 'core_table/local/dynamic/repository';
 import * as Selectors from 'core_table/local/dynamic/selectors';
 import Events from './local/dynamic/events';
+import {addIconToContainer} from 'core/loadingicon';
 
 let watching = false;
 
@@ -66,6 +67,7 @@ const getFiltersetFromTable = tableRoot => {
  */
 export const refreshTableContent = (tableRoot, resetContent = false) => {
     const filterset = getFiltersetFromTable(tableRoot);
+    addIconToContainer(tableRoot);
 
     return fetchTableData(
         tableRoot.dataset.tableComponent,
index 6cb2a6b..2c46b2f 100644 (file)
@@ -1645,6 +1645,7 @@ class flexible_table {
         if (is_a($this, \core_table\dynamic::class)) {
             $sortdata = $this->get_sort_order();
             return html_writer::start_tag('div', [
+                'class' => 'table-dynamic position-relative',
                 'data-region' => 'core_table/dynamic',
                 'data-table-handler' => $this->get_handler(),
                 'data-table-component' => $this->get_component(),
diff --git a/pix/i/contentbank.png b/pix/i/contentbank.png
new file mode 100644 (file)
index 0000000..970c30e
Binary files /dev/null and b/pix/i/contentbank.png differ
diff --git a/pix/i/contentbank.svg b/pix/i/contentbank.svg
new file mode 100644 (file)
index 0000000..3d87f77
--- /dev/null
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg width="16" height="16" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1615 0q70 0 122.5 46.5t52.5 116.5q0 63-45 151-332 629-465 752-97 91-218 91-126 0-216.5-92.5t-90.5-219.5q0-128 92-212l638-579q59-54 130-54zm-909 1034q39 76 106.5 130t150.5 76l1 71q4 213-129.5 347t-348.5 134q-123 0-218-46.5t-152.5-127.5-86.5-183-29-220q7 5 41 30t62 44.5 59 36.5 46 17q41 0 55-37 25-66 57.5-112.5t69.5-76 88-47.5 103-25.5 125-10.5z" fill="#999"/></svg>
\ No newline at end of file
index 5927f78..3909387 100644 (file)
@@ -90,7 +90,7 @@ class report_table extends \table_sql implements \renderable {
         $fields = 'cl.id, cl.timemodified, cl.plugin, cl.name, cl.value, cl.oldvalue, cl.userid, ' . $userfields;
 
         $from = '{config_log} cl
-            JOIN {user} u ON u.id = cl.userid';
+            LEFT JOIN {user} u ON u.id = cl.userid';
 
         // Report search.
         $where = '1=1';
@@ -143,6 +143,24 @@ class report_table extends \table_sql implements \renderable {
         return userdate($row->timemodified);
     }
 
+    /**
+     * Format fullname field
+     *
+     * @param stdClass $row
+     * @return string
+     */
+    public function col_fullname($row) {
+
+        $userid = $row->{$this->useridfield};
+        if (empty($userid)) {
+            // If the user id is empty it must have been set via the
+            // admin/cli/cfg.php script or during the initial install.
+            return get_string('usernone', 'report_configlog');
+        } else {
+            return parent::col_fullname($row);
+        }
+    }
+
     /**
      * Format report plugin field
      *
index 40645fd..643c7b6 100644 (file)
@@ -31,6 +31,7 @@ $string['pluginname'] = 'Config changes';
 $string['setting'] = 'Setting';
 $string['timemodified'] = 'Date';
 $string['user'] = 'User';
+$string['usernone'] = 'CLI or install';
 $string['user_help'] = 'Search by user first name or surname';
 $string['value'] = 'Value';
 $string['value_help'] = 'Search by new or original value of the configuration';
index b6f91f1..3f34cca 100644 (file)
@@ -1511,7 +1511,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
  */
 .modchooser .modal-body {
     padding: 0;
-    height: 640px;
+    min-height: 640px;
     overflow-y: auto;
 
     .loading-icon {
@@ -1521,9 +1521,11 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
             font-size: 3em;
             height: 1em;
             width: 1em;
-            margin: 5em auto;
         }
     }
+    .carousel-item .loading-icon .icon {
+        margin: 5em auto;
+    }
 }
 
 .modchoosercontainer.noscroll {
@@ -1582,7 +1584,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
     background-color: $white;
     overflow-x: hidden;
     overflow-y: auto;
-    height: 640px;
+    min-height: 640px;
 
     .content {
         overflow-y: auto;
@@ -2359,6 +2361,12 @@ body.h5p-embed {
     word-break: break-all;
 }
 
+.matchtext {
+    background-color: lighten($primary, 40%);
+    color: $body-color;
+    height: 1.5rem;
+}
+
 // Emoji picker.
 $picker-width: 350px !default;
 $picker-width-xs: 320px !default;
index 2d523f7..c920380 100644 (file)
@@ -477,12 +477,6 @@ $message-day-color: color-yiq($message-app-bg) !default;
         }
     }
 
-    .matchtext {
-        background-color: lighten($primary, 40%);
-        color: $body-color;
-        height: 1.5rem;
-    }
-
     .contact-status {
         position: absolute;
         left: 39px;
index d5e8939..f27cd9b 100644 (file)
@@ -46,3 +46,14 @@ table {
         caption-side: top;
     }
 }
+
+.table-dynamic .loading-icon {
+    position: absolute;
+    left: calc(50% - 1.5rem);
+    top: 200px;
+    .icon {
+        height: 3rem;
+        width: 3rem;
+        font-size: 3rem;
+    }
+}
index 74d9792..48c6ea9 100644 (file)
@@ -10653,7 +10653,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
  */
 .modchooser .modal-body {
   padding: 0;
-  height: 640px;
+  min-height: 640px;
   overflow-y: auto; }
   .modchooser .modal-body .loading-icon {
     opacity: 1; }
@@ -10661,8 +10661,9 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
       display: block;
       font-size: 3em;
       height: 1em;
-      width: 1em;
-      margin: 5em auto; }
+      width: 1em; }
+  .modchooser .modal-body .carousel-item .loading-icon .icon {
+    margin: 5em auto; }
 
 .modchoosercontainer.noscroll {
   overflow-y: hidden; }
@@ -10708,7 +10709,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   background-color: #fff;
   overflow-x: hidden;
   overflow-y: auto;
-  height: 640px; }
+  min-height: 640px; }
   .modchooser .modal-body .optionsummary .content {
     overflow-y: auto; }
     .modchooser .modal-body .optionsummary .content .heading .icon {
@@ -11313,6 +11314,11 @@ body.h5p-embed .h5pmessages {
 .word-break-all {
   word-break: break-all; }
 
+.matchtext {
+  background-color: #b5d9f9;
+  color: #343a40;
+  height: 1.5rem; }
+
 .emoji-picker {
   width: 350px;
   height: 400px; }
@@ -14545,10 +14551,6 @@ a.ygtvspacer:hover {
     flex-shrink: 0; }
     .message-app .footer-container textarea {
       direction: ltr; }
-  .message-app .matchtext {
-    background-color: #b5d9f9;
-    color: #343a40;
-    height: 1.5rem; }
   .message-app .contact-status {
     position: absolute;
     left: 39px;
@@ -17168,6 +17170,15 @@ table caption {
   text-align: left;
   caption-side: top; }
 
+.table-dynamic .loading-icon {
+  position: absolute;
+  left: calc(50% - 1.5rem);
+  top: 200px; }
+  .table-dynamic .loading-icon .icon {
+    height: 3rem;
+    width: 3rem;
+    font-size: 3rem; }
+
 .singlebutton {
   display: inline-block; }
   .singlebutton + .singlebutton {
index 42a7c53..c3008b9 100644 (file)
@@ -10861,7 +10861,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
  */
 .modchooser .modal-body {
   padding: 0;
-  height: 640px;
+  min-height: 640px;
   overflow-y: auto; }
   .modchooser .modal-body .loading-icon {
     opacity: 1; }
@@ -10869,8 +10869,9 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
       display: block;
       font-size: 3em;
       height: 1em;
-      width: 1em;
-      margin: 5em auto; }
+      width: 1em; }
+  .modchooser .modal-body .carousel-item .loading-icon .icon {
+    margin: 5em auto; }
 
 .modchoosercontainer.noscroll {
   overflow-y: hidden; }
@@ -10916,7 +10917,7 @@ body#page-lib-editor-tinymce-plugins-moodlemedia-preview {
   background-color: #fff;
   overflow-x: hidden;
   overflow-y: auto;
-  height: 640px; }
+  min-height: 640px; }
   .modchooser .modal-body .optionsummary .content {
     overflow-y: auto; }
     .modchooser .modal-body .optionsummary .content .heading .icon {
@@ -11525,6 +11526,11 @@ body.h5p-embed .h5pmessages {
 .word-break-all {
   word-break: break-all; }
 
+.matchtext {
+  background-color: #b5d9f9;
+  color: #343a40;
+  height: 1.5rem; }
+
 .emoji-picker {
   width: 350px;
   height: 400px; }
@@ -14761,10 +14767,6 @@ a.ygtvspacer:hover {
     flex-shrink: 0; }
     .message-app .footer-container textarea {
       direction: ltr; }
-  .message-app .matchtext {
-    background-color: #b5d9f9;
-    color: #343a40;
-    height: 1.5rem; }
   .message-app .contact-status {
     position: absolute;
     left: 39px;
@@ -17400,6 +17402,15 @@ table caption {
   text-align: left;
   caption-side: top; }
 
+.table-dynamic .loading-icon {
+  position: absolute;
+  left: calc(50% - 1.5rem);
+  top: 200px; }
+  .table-dynamic .loading-icon .icon {
+    height: 3rem;
+    width: 3rem;
+    font-size: 3rem; }
+
 .singlebutton {
   display: inline-block; }
   .singlebutton + .singlebutton {
index c02fa52..dd63fb5 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2020051200.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2020051200.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 $release  = '3.9dev+ (Build: 20200512)'; // Human-friendly version name