MDL-68524 js: Add prefetch module
authorAndrew Nicols <andrew@nicols.co.uk>
Sat, 25 Apr 2020 12:33:12 +0000 (20:33 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 5 May 2020 02:16:10 +0000 (10:16 +0800)
lib/amd/build/prefetch.min.js [new file with mode: 0644]
lib/amd/build/prefetch.min.js.map [new file with mode: 0644]
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/src/prefetch.js [new file with mode: 0644]
lib/amd/src/templates.js
lib/outputrequirementslib.php

diff --git a/lib/amd/build/prefetch.min.js b/lib/amd/build/prefetch.min.js
new file mode 100644 (file)
index 0000000..eee33e0
Binary files /dev/null and b/lib/amd/build/prefetch.min.js differ
diff --git a/lib/amd/build/prefetch.min.js.map b/lib/amd/build/prefetch.min.js.map
new file mode 100644 (file)
index 0000000..66563ea
Binary files /dev/null and b/lib/amd/build/prefetch.min.js.map differ
index aefca08..d7d76f4 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 3a6b52c..b95cb3a 100644 (file)
Binary files a/lib/amd/build/templates.min.js.map and b/lib/amd/build/templates.min.js.map differ
diff --git a/lib/amd/src/prefetch.js b/lib/amd/src/prefetch.js
new file mode 100644 (file)
index 0000000..e8e6fb8
--- /dev/null
@@ -0,0 +1,194 @@
+// 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/>.
+
+/**
+ * Prefetch module to help lazily load content for use on the current page.
+ *
+ * @module     core/prefetch
+ * @class      prefetch
+ * @package    core
+ * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import Config from 'core/config';
+
+// Keep track of whether the initial prefetch has occurred.
+let initialPrefetchComplete = false;
+
+// Prefetch templates.
+let templateList = [];
+
+// Prefetch strings.
+let stringList = {};
+
+let prefetchTimer;
+
+/**
+ * Fetch all queued items in the queue.
+ *
+ * Should only be called via processQueue.
+ */
+const fetchQueue = () => {
+    // Prefetch templates.
+    if (templateList) {
+        const templatesToLoad = templateList.slice();
+        templateList = [];
+        import('core/templates')
+        .then(Templates => Templates.prefetchTemplates(templatesToLoad))
+        .catch();
+    }
+
+    // Prefetch strings.
+    const mappedStringsToFetch = stringList;
+    stringList = {};
+
+    const stringsToFetch = [];
+    Object.keys(mappedStringsToFetch).forEach(component => {
+        stringsToFetch.push(...mappedStringsToFetch[component].map(key => {
+            return {component, key};
+        }));
+    });
+
+    if (stringsToFetch) {
+        import('core/str')
+        .then(Str => Str.get_strings(stringsToFetch))
+        .catch();
+    }
+};
+
+/**
+ * Process the prefetch queues as required.
+ *
+ * The initial call will queue the first fetch after a delay.
+ * Subsequent fetches are immediate.
+ */
+const processQueue = () => {
+    if (Config.jsrev <= 0) {
+        // No point pre-fetching when cachejs is disabled as we do not store anything in the cache anyway.
+        return;
+    }
+
+    if (prefetchTimer) {
+        // There is a live prefetch timer. The initial prefetch has been scheduled but is not complete.
+        return;
+    }
+
+    // The initial prefetch has compelted. Just queue as normal.
+    if (initialPrefetchComplete) {
+        fetchQueue();
+
+        return;
+    }
+
+    // Queue the initial prefetch in a short while.
+    prefetchTimer = setTimeout(() => {
+        initialPrefetchComplete = true;
+        prefetchTimer = null;
+
+        // Ensure that the icon system is loaded.
+        // This can be quite slow and delay UI interactions if it is loaded on demand.
+        import(Config.iconsystemmodule)
+        .then(IconSystem => {
+            const iconSystem = new IconSystem();
+            prefetchTemplate(iconSystem.getTemplateName());
+
+            return iconSystem;
+        })
+        .then(iconSystem => {
+            fetchQueue();
+            iconSystem.init();
+
+            return;
+        })
+        .catch();
+    }, 500);
+};
+
+/**
+ * Add a set of templates to the prefetch queue.
+ *
+ * @param {Array} templatesNames
+ */
+const prefetchTemplates = templatesNames => {
+    templateList = templateList.concat(templatesNames);
+
+    processQueue();
+};
+
+/**
+ * Add a single template to the prefetch queue.
+ *
+ * @param {String} templateName
+ * @returns {undefined}
+ */
+const prefetchTemplate = templateName => prefetchTemplates([templateName]);
+
+/**
+ * Add a set of strings from the same component to the prefetch queue.
+ *
+ * @param {String} component
+ * @param {String[]} keys
+ */
+const prefetchStrings = (component, keys) => {
+    if (!stringList[component]) {
+        stringList[component] = [];
+    }
+
+    stringList[component] = stringList[component].concat(keys);
+
+    processQueue();
+};
+
+/**
+ * Add a single string to the prefetch queue.
+ *
+ * @param {String} component
+ * @param {String} key
+ */
+const prefetchString = (component, key) => {
+    if (!stringList[component]) {
+        stringList[component] = [];
+    }
+
+    stringList[component].push(key);
+
+    processQueue();
+};
+
+// Prefetch some commonly-used templates.
+prefetchTemplates([].concat(
+    ['core/loading'],
+    ['core/modal'],
+    ['core/modal_backdrop'],
+));
+
+// And some commonly used strings.
+prefetchStrings('core', [
+    'cancel',
+    'closebuttontitle',
+    'loading',
+    'savechanges',
+]);
+prefetchStrings('core_form', [
+    'showless',
+    'showmore',
+]);
+
+export default {
+    prefetchTemplate,
+    prefetchTemplates,
+    prefetchString,
+    prefetchStrings,
+};
index afac7f3..8ac79ab 100644 (file)
@@ -298,7 +298,7 @@ define([
             return cachedPromise;
         }
 
             return cachedPromise;
         }
 
-        // Check the buffer to seee if this template has already been added.
+        // Check the buffer to see if this template has already been added.
         var existingBufferRecords = loadTemplateBuffer.filter(function(record) {
             return record.searchKey == searchKey;
         });
         var existingBufferRecords = loadTemplateBuffer.filter(function(record) {
             return record.searchKey == searchKey;
         });
@@ -329,6 +329,50 @@ define([
         return deferred.promise();
     };
 
         return deferred.promise();
     };
 
+    /**
+     * Prefetch a set of templates without rendering them.
+     *
+     * @param {Array} templateNames The list of templates to fetch
+     * @param {String} currentTheme
+     */
+    Renderer.prototype.prefetchTemplates = function(templateNames, currentTheme) {
+        templateNames.forEach(function(templateName) {
+            var searchKey = currentTheme + '/' + templateName;
+
+            // If we haven't already seen this template then buffer it.
+            if (getTemplatePromiseFromCache(searchKey)) {
+                return;
+            }
+
+            // Check the buffer to see if this template has already been added.
+            var existingBufferRecords = loadTemplateBuffer.filter(function(record) {
+                return record.searchKey == searchKey;
+            });
+
+            if (existingBufferRecords.length) {
+                // This template is already in the buffer so just return the existing promise.
+                // No need to add it to the buffer again.
+                return;
+            }
+
+            // This is the first time this has been requested so let's add it to the buffer to be loaded.
+            var parts = templateName.split('/');
+            var component = parts.shift();
+            var name = parts.join('/');
+
+            // Add this template to the buffer to be loaded.
+            loadTemplateBuffer.push({
+                component: component,
+                name: name,
+                theme: currentTheme,
+                searchKey: searchKey,
+                deferred: $.Deferred(),
+            });
+        });
+
+        processLoadTemplateBuffer();
+    };
+
     /**
      * Load a partial from the cache or ajax.
      *
     /**
      * Load a partial from the cache or ajax.
      *
@@ -1045,6 +1089,25 @@ define([
             return renderer.render(templateName, context, themeName);
         },
 
             return renderer.render(templateName, context, themeName);
         },
 
+        /**
+         * Prefetch a set of templates without rendering them.
+         *
+         * @method getTemplate
+         * @param {Array} templateNames The list of templates to fetch
+         * @param {String} themeName
+         * @returns {Promise}
+         */
+        prefetchTemplates: function(templateNames, themeName) {
+            var renderer = new Renderer();
+
+            if (typeof themeName === "undefined") {
+                // System context by default.
+                themeName = config.theme;
+            }
+
+            return renderer.prefetchTemplates(templateNames, themeName);
+        },
+
         /**
          * Every call to render creates a new instance of the class and calls render on it. This
          * means each render call has it's own class variables.
         /**
          * Every call to render creates a new instance of the class and calls render on it. This
          * means each render call has it's own class variables.
index 961fda1..236b0ed 100644 (file)
@@ -1390,6 +1390,9 @@ class page_requirements_manager {
         // First include must be to a module with no dependencies, this prevents multiple requests.
         $prefix = 'M.util.js_pending("core/first");';
         $prefix .= "require(['core/first'], function() {\n";
         // First include must be to a module with no dependencies, this prevents multiple requests.
         $prefix = 'M.util.js_pending("core/first");';
         $prefix .= "require(['core/first'], function() {\n";
+        if ($cachejs) {
+            $prefix .= "require(['core/prefetch']);\n";
+        }
         $suffix = 'M.util.js_complete("core/first");';
         $suffix .= "\n});";
         $output .= html_writer::script($prefix . implode(";\n", $this->amdjscode) . $suffix);
         $suffix = 'M.util.js_complete("core/first");';
         $suffix .= "\n});";
         $output .= html_writer::script($prefix . implode(";\n", $this->amdjscode) . $suffix);