| 1 | efrain | 1 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 2 | //
 | 
        
           |  |  | 3 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 4 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 5 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 6 | // (at your option) any later version.
 | 
        
           |  |  | 7 | //
 | 
        
           |  |  | 8 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 11 | // GNU General Public License for more details.
 | 
        
           |  |  | 12 | //
 | 
        
           |  |  | 13 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 14 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 15 |   | 
        
           |  |  | 16 | /**
 | 
        
           |  |  | 17 |  * Template renderer for Moodle. Load and render Moodle templates with Mustache.
 | 
        
           |  |  | 18 |  *
 | 
        
           |  |  | 19 |  * @module     core/templates
 | 
        
           |  |  | 20 |  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
 | 
        
           |  |  | 21 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 22 |  * @since      2.9
 | 
        
           |  |  | 23 |  */
 | 
        
           |  |  | 24 |   | 
        
           |  |  | 25 | import $ from 'jquery';
 | 
        
           |  |  | 26 | import * as config from 'core/config';
 | 
        
           |  |  | 27 | import * as filterEvents from 'core_filters/events';
 | 
        
           |  |  | 28 | import * as Y from 'core/yui';
 | 
        
           |  |  | 29 | import Renderer from './local/templates/renderer';
 | 
        
           |  |  | 30 | import {getNormalisedComponent} from 'core/utils';
 | 
        
           |  |  | 31 |   | 
        
           |  |  | 32 | /**
 | 
        
           |  |  | 33 |  * Execute a block of JS returned from a template.
 | 
        
           |  |  | 34 |  * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
 | 
        
           |  |  | 35 |  *
 | 
        
           |  |  | 36 |  * @method runTemplateJS
 | 
        
           |  |  | 37 |  * @param {string} source - A block of javascript.
 | 
        
           |  |  | 38 |  */
 | 
        
           |  |  | 39 | const runTemplateJS = (source) => {
 | 
        
           |  |  | 40 |     if (source.trim() !== '') {
 | 
        
           |  |  | 41 |         // Note. We continue to use jQuery here because people are doing some dumb things
 | 
        
           |  |  | 42 |         // and we need to find, seek, and destroy first.
 | 
        
           |  |  | 43 |         // In particular, people are providing a mixture of JS, and HTML content here.
 | 
        
           |  |  | 44 |         // jQuery is someohow, magically, detecting this and putting tags into tags.
 | 
        
           |  |  | 45 |         const newScript = $('<script>').attr('type', 'text/javascript').html(source);
 | 
        
           |  |  | 46 |         $('head').append(newScript);
 | 
        
           |  |  | 47 |         if (newScript.find('script').length) {
 | 
        
           |  |  | 48 |             window.console.error(
 | 
        
           |  |  | 49 |                 'Template JS contains a script tag. This is not allowed. Only raw JS should be present here.',
 | 
        
           |  |  | 50 |                 source,
 | 
        
           |  |  | 51 |             );
 | 
        
           |  |  | 52 |         }
 | 
        
           |  |  | 53 |     }
 | 
        
           |  |  | 54 | };
 | 
        
           |  |  | 55 |   | 
        
           |  |  | 56 | /**
 | 
        
           |  |  | 57 |  * Do some DOM replacement and trigger correct events and fire javascript.
 | 
        
           |  |  | 58 |  *
 | 
        
           |  |  | 59 |  * @method domReplace
 | 
        
           |  |  | 60 |  * @param {JQuery} element - Element or selector to replace.
 | 
        
           |  |  | 61 |  * @param {String} newHTML - HTML to insert / replace.
 | 
        
           |  |  | 62 |  * @param {String} newJS - Javascript to run after the insertion.
 | 
        
           |  |  | 63 |  * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
 | 
        
           |  |  | 64 |  * @return {Array} The list of new DOM Nodes
 | 
        
           |  |  | 65 |  * @fires event:filterContentUpdated
 | 
        
           |  |  | 66 |  */
 | 
        
           |  |  | 67 | const domReplace = (element, newHTML, newJS, replaceChildNodes) => {
 | 
        
           |  |  | 68 |     const replaceNode = $(element);
 | 
        
           |  |  | 69 |     if (!replaceNode.length) {
 | 
        
           |  |  | 70 |         return [];
 | 
        
           |  |  | 71 |     }
 | 
        
           |  |  | 72 |     // First create the dom nodes so we have a reference to them.
 | 
        
           |  |  | 73 |     const newNodes = $(newHTML);
 | 
        
           |  |  | 74 |     // Do the replacement in the page.
 | 
        
           |  |  | 75 |     if (replaceChildNodes) {
 | 
        
           |  |  | 76 |         // Cleanup any YUI event listeners attached to any of these nodes.
 | 
        
           |  |  | 77 |         const yuiNodes = new Y.NodeList(replaceNode.children().get());
 | 
        
           |  |  | 78 |         yuiNodes.destroy(true);
 | 
        
           |  |  | 79 |   | 
        
           |  |  | 80 |         // JQuery will cleanup after itself.
 | 
        
           |  |  | 81 |         replaceNode.empty();
 | 
        
           |  |  | 82 |         replaceNode.append(newNodes);
 | 
        
           |  |  | 83 |     } else {
 | 
        
           |  |  | 84 |         // Cleanup any YUI event listeners attached to any of these nodes.
 | 
        
           |  |  | 85 |         const yuiNodes = new Y.NodeList(replaceNode.get());
 | 
        
           |  |  | 86 |         yuiNodes.destroy(true);
 | 
        
           |  |  | 87 |   | 
        
           |  |  | 88 |         // JQuery will cleanup after itself.
 | 
        
           |  |  | 89 |         replaceNode.replaceWith(newNodes);
 | 
        
           |  |  | 90 |     }
 | 
        
           |  |  | 91 |     // Run any javascript associated with the new HTML.
 | 
        
           |  |  | 92 |     runTemplateJS(newJS);
 | 
        
           |  |  | 93 |     // Notify all filters about the new content.
 | 
        
           |  |  | 94 |     filterEvents.notifyFilterContentUpdated(newNodes);
 | 
        
           |  |  | 95 |   | 
        
           |  |  | 96 |     return newNodes.get();
 | 
        
           |  |  | 97 | };
 | 
        
           |  |  | 98 |   | 
        
           |  |  | 99 | /**
 | 
        
           |  |  | 100 |  * Prepend some HTML to a node and trigger events and fire javascript.
 | 
        
           |  |  | 101 |  *
 | 
        
           |  |  | 102 |  * @method domPrepend
 | 
        
           |  |  | 103 |  * @param {jQuery|String} element - Element or selector to prepend HTML to
 | 
        
           |  |  | 104 |  * @param {String} html - HTML to prepend
 | 
        
           |  |  | 105 |  * @param {String} js - Javascript to run after we prepend the html
 | 
        
           |  |  | 106 |  * @return {Array} The list of new DOM Nodes
 | 
        
           |  |  | 107 |  * @fires event:filterContentUpdated
 | 
        
           |  |  | 108 |  */
 | 
        
           |  |  | 109 | const domPrepend = (element, html, js) => {
 | 
        
           |  |  | 110 |     const node = $(element);
 | 
        
           |  |  | 111 |     if (!node.length) {
 | 
        
           |  |  | 112 |         return [];
 | 
        
           |  |  | 113 |     }
 | 
        
           |  |  | 114 |   | 
        
           |  |  | 115 |     // Prepend the html.
 | 
        
           |  |  | 116 |     const newContent = $(html);
 | 
        
           |  |  | 117 |     node.prepend(newContent);
 | 
        
           |  |  | 118 |     // Run any javascript associated with the new HTML.
 | 
        
           |  |  | 119 |     runTemplateJS(js);
 | 
        
           |  |  | 120 |     // Notify all filters about the new content.
 | 
        
           |  |  | 121 |     filterEvents.notifyFilterContentUpdated(node);
 | 
        
           |  |  | 122 |   | 
        
           |  |  | 123 |     return newContent.get();
 | 
        
           |  |  | 124 | };
 | 
        
           |  |  | 125 |   | 
        
           |  |  | 126 | /**
 | 
        
           |  |  | 127 |  * Append some HTML to a node and trigger events and fire javascript.
 | 
        
           |  |  | 128 |  *
 | 
        
           |  |  | 129 |  * @method domAppend
 | 
        
           |  |  | 130 |  * @param {jQuery|String} element - Element or selector to append HTML to
 | 
        
           |  |  | 131 |  * @param {String} html - HTML to append
 | 
        
           |  |  | 132 |  * @param {String} js - Javascript to run after we append the html
 | 
        
           |  |  | 133 |  * @return {Array} The list of new DOM Nodes
 | 
        
           |  |  | 134 |  * @fires event:filterContentUpdated
 | 
        
           |  |  | 135 |  */
 | 
        
           |  |  | 136 | const domAppend = (element, html, js) => {
 | 
        
           |  |  | 137 |     const node = $(element);
 | 
        
           |  |  | 138 |     if (!node.length) {
 | 
        
           |  |  | 139 |         return [];
 | 
        
           |  |  | 140 |     }
 | 
        
           |  |  | 141 |     // Append the html.
 | 
        
           |  |  | 142 |     const newContent = $(html);
 | 
        
           |  |  | 143 |     node.append(newContent);
 | 
        
           |  |  | 144 |     // Run any javascript associated with the new HTML.
 | 
        
           |  |  | 145 |     runTemplateJS(js);
 | 
        
           |  |  | 146 |     // Notify all filters about the new content.
 | 
        
           |  |  | 147 |     filterEvents.notifyFilterContentUpdated(node);
 | 
        
           |  |  | 148 |   | 
        
           |  |  | 149 |     return newContent.get();
 | 
        
           |  |  | 150 | };
 | 
        
           |  |  | 151 |   | 
        
           |  |  | 152 | const wrapPromiseInWhenable = (promise) => $.when(new Promise((resolve, reject) => {
 | 
        
           |  |  | 153 |     promise.then(resolve).catch(reject);
 | 
        
           |  |  | 154 | }));
 | 
        
           |  |  | 155 |   | 
        
           |  |  | 156 | export default {
 | 
        
           |  |  | 157 |     // Public variables and functions.
 | 
        
           |  |  | 158 |     /**
 | 
        
           |  |  | 159 |      * Every call to render creates a new instance of the class and calls render on it. This
 | 
        
           |  |  | 160 |      * means each render call has it's own class variables.
 | 
        
           |  |  | 161 |      *
 | 
        
           |  |  | 162 |      * @method render
 | 
        
           |  |  | 163 |      * @param {string} templateName - should consist of the component and the name of the template like this:
 | 
        
           |  |  | 164 |      *                              core/menu (lib/templates/menu.mustache) or
 | 
        
           |  |  | 165 |      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
 | 
        
           |  |  | 166 |      * @param {Object} context - Could be array, string or simple value for the context of the template.
 | 
        
           |  |  | 167 |      * @param {string} themeName - Name of the current theme.
 | 
        
           |  |  | 168 |      * @return {Promise} JQuery promise object resolved when the template has been rendered.
 | 
        
           |  |  | 169 |      */
 | 
        
           |  |  | 170 |     render: (templateName, context, themeName = config.theme) => {
 | 
        
           |  |  | 171 |         const renderer = new Renderer();
 | 
        
           |  |  | 172 |   | 
        
           |  |  | 173 |         // Turn the Native Promise into a jQuery Promise for backwards compatability.
 | 
        
           |  |  | 174 |         return $.when(new Promise((resolve, reject) => {
 | 
        
           |  |  | 175 |             renderer.render(templateName, context, themeName)
 | 
        
           |  |  | 176 |             .then(resolve)
 | 
        
           |  |  | 177 |             .catch(reject);
 | 
        
           |  |  | 178 |         }))
 | 
        
           |  |  | 179 |         .then(({html, js}) => $.Deferred().resolve(html, js));
 | 
        
           |  |  | 180 |     },
 | 
        
           |  |  | 181 |   | 
        
           |  |  | 182 |     /**
 | 
        
           |  |  | 183 |      * Prefetch a set of templates without rendering them.
 | 
        
           |  |  | 184 |      *
 | 
        
           |  |  | 185 |      * @method getTemplate
 | 
        
           |  |  | 186 |      * @param {Array} templateNames The list of templates to fetch
 | 
        
           |  |  | 187 |      * @param {String} [themeName=config.themeName] The name of the theme to use
 | 
        
           |  |  | 188 |      * @returns {Promise}
 | 
        
           |  |  | 189 |      */
 | 
        
           |  |  | 190 |     prefetchTemplates: (templateNames, themeName = config.theme) => {
 | 
        
           |  |  | 191 |         const Loader = Renderer.getLoader();
 | 
        
           |  |  | 192 |   | 
        
           |  |  | 193 |         return Loader.prefetchTemplates(templateNames, themeName);
 | 
        
           |  |  | 194 |     },
 | 
        
           |  |  | 195 |   | 
        
           |  |  | 196 |     /**
 | 
        
           |  |  | 197 |      * Every call to render creates a new instance of the class and calls render on it. This
 | 
        
           |  |  | 198 |      * means each render call has it's own class variables.
 | 
        
           |  |  | 199 |      *
 | 
        
           |  |  | 200 |      * This alernate to the standard .render() function returns the html and js in a single object suitable for a
 | 
        
           |  |  | 201 |      * native Promise.
 | 
        
           |  |  | 202 |      *
 | 
        
           |  |  | 203 |      * @method renderForPromise
 | 
        
           |  |  | 204 |      * @param {string} templateName - should consist of the component and the name of the template like this:
 | 
        
           |  |  | 205 |      *                              core/menu (lib/templates/menu.mustache) or
 | 
        
           |  |  | 206 |      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
 | 
        
           |  |  | 207 |      * @param {Object} context - Could be array, string or simple value for the context of the template.
 | 
        
           |  |  | 208 |      * @param {string} themeName - Name of the current theme.
 | 
        
           |  |  | 209 |      * @return {Promise} JQuery promise object resolved when the template has been rendered.
 | 
        
           |  |  | 210 |      */
 | 
        
           |  |  | 211 |     renderForPromise: (templateName, context, themeName) => {
 | 
        
           |  |  | 212 |         const renderer = new Renderer();
 | 
        
           |  |  | 213 |         return renderer.render(templateName, context, themeName);
 | 
        
           |  |  | 214 |     },
 | 
        
           |  |  | 215 |   | 
        
           |  |  | 216 |     /**
 | 
        
           |  |  | 217 |      * Every call to renderIcon creates a new instance of the class and calls renderIcon on it. This
 | 
        
           |  |  | 218 |      * means each render call has it's own class variables.
 | 
        
           |  |  | 219 |      *
 | 
        
           |  |  | 220 |      * @method renderPix
 | 
        
           |  |  | 221 |      * @param {string} key - Icon key.
 | 
        
           |  |  | 222 |      * @param {string} component - Icon component
 | 
        
           |  |  | 223 |      * @param {string} title - Icon title
 | 
        
           |  |  | 224 |      * @return {Promise} JQuery promise object resolved when the pix has been rendered.
 | 
        
           |  |  | 225 |      */
 | 
        
           |  |  | 226 |     renderPix: (key, component, title) => {
 | 
        
           |  |  | 227 |         const renderer = new Renderer();
 | 
        
           |  |  | 228 |         return wrapPromiseInWhenable(renderer.renderIcon(
 | 
        
           |  |  | 229 |             key,
 | 
        
           |  |  | 230 |             getNormalisedComponent(component),
 | 
        
           |  |  | 231 |             title
 | 
        
           |  |  | 232 |         ));
 | 
        
           |  |  | 233 |     },
 | 
        
           |  |  | 234 |   | 
        
           |  |  | 235 |     /**
 | 
        
           |  |  | 236 |      * Execute a block of JS returned from a template.
 | 
        
           |  |  | 237 |      * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
 | 
        
           |  |  | 238 |      *
 | 
        
           |  |  | 239 |      * @method runTemplateJS
 | 
        
           |  |  | 240 |      * @param {string} source - A block of javascript.
 | 
        
           |  |  | 241 |      */
 | 
        
           |  |  | 242 |     runTemplateJS: runTemplateJS,
 | 
        
           |  |  | 243 |   | 
        
           |  |  | 244 |     /**
 | 
        
           |  |  | 245 |      * Replace a node in the page with some new HTML and run the JS.
 | 
        
           |  |  | 246 |      *
 | 
        
           |  |  | 247 |      * @method replaceNodeContents
 | 
        
           |  |  | 248 |      * @param {JQuery} element - Element or selector to replace.
 | 
        
           |  |  | 249 |      * @param {String} newHTML - HTML to insert / replace.
 | 
        
           |  |  | 250 |      * @param {String} newJS - Javascript to run after the insertion.
 | 
        
           |  |  | 251 |      * @return {Array} The list of new DOM Nodes
 | 
        
           |  |  | 252 |      */
 | 
        
           |  |  | 253 |     replaceNodeContents: (element, newHTML, newJS) => domReplace(element, newHTML, newJS, true),
 | 
        
           |  |  | 254 |   | 
        
           |  |  | 255 |     /**
 | 
        
           |  |  | 256 |      * Insert a node in the page with some new HTML and run the JS.
 | 
        
           |  |  | 257 |      *
 | 
        
           |  |  | 258 |      * @method replaceNode
 | 
        
           |  |  | 259 |      * @param {JQuery} element - Element or selector to replace.
 | 
        
           |  |  | 260 |      * @param {String} newHTML - HTML to insert / replace.
 | 
        
           |  |  | 261 |      * @param {String} newJS - Javascript to run after the insertion.
 | 
        
           |  |  | 262 |      * @return {Array} The list of new DOM Nodes
 | 
        
           |  |  | 263 |      */
 | 
        
           |  |  | 264 |     replaceNode: (element, newHTML, newJS) => domReplace(element, newHTML, newJS, false),
 | 
        
           |  |  | 265 |   | 
        
           |  |  | 266 |     /**
 | 
        
           |  |  | 267 |      * Prepend some HTML to a node and trigger events and fire javascript.
 | 
        
           |  |  | 268 |      *
 | 
        
           |  |  | 269 |      * @method prependNodeContents
 | 
        
           |  |  | 270 |      * @param {jQuery|String} element - Element or selector to prepend HTML to
 | 
        
           |  |  | 271 |      * @param {String} html - HTML to prepend
 | 
        
           |  |  | 272 |      * @param {String} js - Javascript to run after we prepend the html
 | 
        
           |  |  | 273 |      * @return {Array} The list of new DOM Nodes
 | 
        
           |  |  | 274 |      */
 | 
        
           |  |  | 275 |     prependNodeContents: (element, html, js) => domPrepend(element, html, js),
 | 
        
           |  |  | 276 |   | 
        
           |  |  | 277 |     /**
 | 
        
           |  |  | 278 |      * Append some HTML to a node and trigger events and fire javascript.
 | 
        
           |  |  | 279 |      *
 | 
        
           |  |  | 280 |      * @method appendNodeContents
 | 
        
           |  |  | 281 |      * @param {jQuery|String} element - Element or selector to append HTML to
 | 
        
           |  |  | 282 |      * @param {String} html - HTML to append
 | 
        
           |  |  | 283 |      * @param {String} js - Javascript to run after we append the html
 | 
        
           |  |  | 284 |      * @return {Array} The list of new DOM Nodes
 | 
        
           |  |  | 285 |      */
 | 
        
           |  |  | 286 |     appendNodeContents: (element, html, js) => domAppend(element, html, js),
 | 
        
           |  |  | 287 | };
 |