MDL-63714 javascript: Add new core/pending module
[moodle.git] / lib / amd / src / templates.js
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/>.
16 /**
17  * Template renderer for Moodle. Load and render Moodle templates with Mustache.
18  *
19  * @module     core/templates
20  * @package    core
21  * @class      templates
22  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      2.9
25  */
26 define([
27         'core/mustache',
28         'jquery',
29         'core/ajax',
30         'core/str',
31         'core/notification',
32         'core/url',
33         'core/config',
34         'core/localstorage',
35         'core/icon_system',
36         'core/event',
37         'core/yui',
38         'core/log',
39         'core/truncate',
40         'core/user_date',
41         'core/pending',
42     ],
43     function(mustache, $, ajax, str, notification, coreurl, config, storage, IconSystem, event, Y, Log, Truncate, UserDate,
44         Pending) {
46     // Module variables.
47     /** @var {Number} uniqInstances Count of times this constructor has been called. */
48     var uniqInstances = 0;
50     /** @var {String[]} templateCache - Cache of already loaded template strings */
51     var templateCache = {};
53     /** @var {Promise[]} templatePromises - Cache of already loaded template promises */
54     var templatePromises = {};
56     /** @var {Promise[]} cachePartialPromises - Cache of already loaded template partial promises */
57     var cachePartialPromises = {};
59     /** @var {Object} iconSystem - Object extending core/iconsystem */
60     var iconSystem = {};
62     /**
63      * Constructor
64      *
65      * Each call to templates.render gets it's own instance of this class.
66      */
67     var Renderer = function() {
68         this.requiredStrings = [];
69         this.requiredJS = [];
70         this.requiredDates = [];
71         this.currentThemeName = '';
72     };
73     // Class variables and functions.
75     /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
76     Renderer.prototype.requiredStrings = null;
78     /** @var {object[]} requiredDates - Collection of dates found during the rendering of one template */
79     Renderer.prototype.requiredDates = [];
81     /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
82     Renderer.prototype.requiredJS = null;
84     /** @var {String} themeName for the current render */
85     Renderer.prototype.currentThemeName = '';
87     /**
88      * Load a template from the cache or local storage or ajax request.
89      *
90      * @method getTemplate
91      * @private
92      * @param {string} templateName - should consist of the component and the name of the template like this:
93      *                              core/menu (lib/templates/menu.mustache) or
94      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
95      * @return {Promise} JQuery promise object resolved when the template has been fetched.
96      */
97     Renderer.prototype.getTemplate = function(templateName) {
98         var parts = templateName.split('/');
99         var component = parts.shift();
100         var name = parts.shift();
102         var searchKey = this.currentThemeName + '/' + templateName;
104         // First try request variables.
105         if (searchKey in templatePromises) {
106             return templatePromises[searchKey];
107         }
109         // Now try local storage.
110         var cached = storage.get('core_template/' + searchKey);
112         if (cached) {
113             templateCache[searchKey] = cached;
114             templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
115             return templatePromises[searchKey];
116         }
118         // Oh well - load via ajax.
119         var promises = ajax.call([{
120             methodname: 'core_output_load_template',
121             args: {
122                 component: component,
123                 template: name,
124                 themename: this.currentThemeName
125             }
126         }], true, false);
128         templatePromises[searchKey] = promises[0].then(
129             function(templateSource) {
130                 templateCache[searchKey] = templateSource;
131                 storage.set('core_template/' + searchKey, templateSource);
132                 return templateSource;
133             }
134         );
135         return templatePromises[searchKey];
136     };
138     /**
139      * Load a partial from the cache or ajax.
140      *
141      * @method partialHelper
142      * @private
143      * @param {string} name The partial name to load.
144      * @return {string}
145      */
146     Renderer.prototype.partialHelper = function(name) {
148         var searchKey = this.currentThemeName + '/' + name;
150         if (!(searchKey in templateCache)) {
151             notification.exception(new Error('Failed to pre-fetch the template: ' + name));
152         }
154         return templateCache[searchKey];
155     };
157     /**
158      * Render a single image icon.
159      *
160      * @method renderIcon
161      * @private
162      * @param {string} key The icon key.
163      * @param {string} component The component name.
164      * @param {string} title The icon title
165      * @return {Promise}
166      */
167     Renderer.prototype.renderIcon = function(key, component, title) {
168         // Preload the module to do the icon rendering based on the theme iconsystem.
169         var modulename = config.iconsystemmodule;
171         // RequireJS does not return a promise.
172         var ready = $.Deferred();
173         require([modulename], function(System) {
174             var system = new System();
175             if (!(system instanceof IconSystem)) {
176                 ready.reject('Invalid icon system specified' + config.iconsystemmodule);
177             } else {
178                 iconSystem = system;
179                 system.init().then(ready.resolve).catch(notification.exception);
180             }
181         });
183         return ready.then(function(iconSystem) {
184             return this.getTemplate(iconSystem.getTemplateName());
185         }.bind(this)).then(function(template) {
186             return iconSystem.renderIcon(key, component, title, template);
187         });
188     };
190     /**
191      * Render image icons.
192      *
193      * @method pixHelper
194      * @private
195      * @param {object} context The mustache context
196      * @param {string} sectionText The text to parse arguments from.
197      * @param {function} helper Used to render the alt attribute of the text.
198      * @return {string}
199      */
200     Renderer.prototype.pixHelper = function(context, sectionText, helper) {
201         var parts = sectionText.split(',');
202         var key = '';
203         var component = '';
204         var text = '';
206         if (parts.length > 0) {
207             key = helper(parts.shift().trim(), context);
208         }
209         if (parts.length > 0) {
210             component = helper(parts.shift().trim(), context);
211         }
212         if (parts.length > 0) {
213             text = helper(parts.join(',').trim(), context);
214         }
216         var templateName = iconSystem.getTemplateName();
218         var searchKey = this.currentThemeName + '/' + templateName;
219         var template = templateCache[searchKey];
221         // The key might have been escaped by the JS Mustache engine which
222         // converts forward slashes to HTML entities. Let us undo that here.
223         key = key.replace(/&#x2F;/gi, '/');
225         return iconSystem.renderIcon(key, component, text, template);
226     };
228     /**
229      * Render blocks of javascript and save them in an array.
230      *
231      * @method jsHelper
232      * @private
233      * @param {object} context The current mustache context.
234      * @param {string} sectionText The text to save as a js block.
235      * @param {function} helper Used to render the block.
236      * @return {string}
237      */
238     Renderer.prototype.jsHelper = function(context, sectionText, helper) {
239         this.requiredJS.push(helper(sectionText, context));
240         return '';
241     };
243     /**
244      * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
245      * into a get_string call.
246      *
247      * @method stringHelper
248      * @private
249      * @param {object} context The current mustache context.
250      * @param {string} sectionText The text to parse the arguments from.
251      * @param {function} helper Used to render subsections of the text.
252      * @return {string}
253      */
254     Renderer.prototype.stringHelper = function(context, sectionText, helper) {
255         var parts = sectionText.split(',');
256         var key = '';
257         var component = '';
258         var param = '';
259         if (parts.length > 0) {
260             key = parts.shift().trim();
261         }
262         if (parts.length > 0) {
263             component = parts.shift().trim();
264         }
265         if (parts.length > 0) {
266             param = parts.join(',').trim();
267         }
269         if (param !== '') {
270             // Allow variable expansion in the param part only.
271             param = helper(param, context);
272         }
273         // Allow json formatted $a arguments.
274         if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
275             param = JSON.parse(param);
276         }
278         var index = this.requiredStrings.length;
279         this.requiredStrings.push({key: key, component: component, param: param});
281         // The placeholder must not use {{}} as those can be misinterpreted by the engine.
282         return '[[_s' + index + ']]';
283     };
285     /**
286      * Quote helper used to wrap content in quotes, and escape all quotes present in the content.
287      *
288      * @method quoteHelper
289      * @private
290      * @param {object} context The current mustache context.
291      * @param {string} sectionText The text to parse the arguments from.
292      * @param {function} helper Used to render subsections of the text.
293      * @return {string}
294      */
295     Renderer.prototype.quoteHelper = function(context, sectionText, helper) {
296         var content = helper(sectionText.trim(), context);
298         // Escape the {{ and the ".
299         // This involves wrapping {{, and }} in change delimeter tags.
300         content = content
301             .replace('"', '\\"')
302             .replace(/([\{\}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>')
303             ;
304         return '"' + content + '"';
305     };
307     /**
308      * Shorten text helper to truncate text and append a trailing ellipsis.
309      *
310      * @method shortenTextHelper
311      * @private
312      * @param {object} context The current mustache context.
313      * @param {string} sectionText The text to parse the arguments from.
314      * @param {function} helper Used to render subsections of the text.
315      * @return {string}
316      */
317     Renderer.prototype.shortenTextHelper = function(context, sectionText, helper) {
318         // Non-greedy split on comma to grab section text into the length and
319         // text parts.
320         var regex = /(.*?),(.*)/;
321         var parts = sectionText.match(regex);
322         // The length is the part matched in the first set of parethesis.
323         var length = parts[1].trim();
324         // The length is the part matched in the second set of parethesis.
325         var text = parts[2].trim();
326         var content = helper(text, context);
327         return Truncate.truncate(content, {
328             length: length,
329             words: true,
330             ellipsis: '...'
331         });
332     };
334     /**
335      * User date helper to render user dates from timestamps.
336      *
337      * @method userDateHelper
338      * @private
339      * @param {object} context The current mustache context.
340      * @param {string} sectionText The text to parse the arguments from.
341      * @param {function} helper Used to render subsections of the text.
342      * @return {string}
343      */
344     Renderer.prototype.userDateHelper = function(context, sectionText, helper) {
345         // Non-greedy split on comma to grab the timestamp and format.
346         var regex = /(.*?),(.*)/;
347         var parts = sectionText.match(regex);
348         var timestamp = helper(parts[1].trim(), context);
349         var format = helper(parts[2].trim(), context);
350         var index = this.requiredDates.length;
352         this.requiredDates.push({
353             timestamp: timestamp,
354             format: format
355         });
357         return '[[_t_' + index + ']]';
358     };
360     /**
361      * Add some common helper functions to all context objects passed to templates.
362      * These helpers match exactly the helpers available in php.
363      *
364      * @method addHelpers
365      * @private
366      * @param {Object} context Simple types used as the context for the template.
367      * @param {String} themeName We set this multiple times, because there are async calls.
368      */
369     Renderer.prototype.addHelpers = function(context, themeName) {
370         this.currentThemeName = themeName;
371         this.requiredStrings = [];
372         this.requiredJS = [];
373         context.uniqid = (uniqInstances++);
374         context.str = function() {
375           return this.stringHelper.bind(this, context);
376         }.bind(this);
377         context.pix = function() {
378           return this.pixHelper.bind(this, context);
379         }.bind(this);
380         context.js = function() {
381           return this.jsHelper.bind(this, context);
382         }.bind(this);
383         context.quote = function() {
384           return this.quoteHelper.bind(this, context);
385         }.bind(this);
386         context.shortentext = function() {
387           return this.shortenTextHelper.bind(this, context);
388         }.bind(this);
389         context.userdate = function() {
390           return this.userDateHelper.bind(this, context);
391         }.bind(this);
392         context.globals = {config: config};
393         context.currentTheme = themeName;
394     };
396     /**
397      * Get all the JS blocks from the last rendered template.
398      *
399      * @method getJS
400      * @private
401      * @return {string}
402      */
403     Renderer.prototype.getJS = function() {
404         var js = '';
405         if (this.requiredJS.length > 0) {
406             js = this.requiredJS.join(";\n");
407         }
409         return js;
410     };
412     /**
413      * Treat strings in content.
414      *
415      * The purpose of this method is to replace the placeholders found in a string
416      * with the their respective translated strings.
417      *
418      * Previously we were relying on String.replace() but the complexity increased with
419      * the numbers of strings to replace. Now we manually walk the string and stop at each
420      * placeholder we find, only then we replace it. Most of the time we will
421      * replace all the placeholders in a single run, at times we will need a few
422      * more runs when placeholders are replaced with strings that contain placeholders
423      * themselves.
424      *
425      * @param {String} content The content in which string placeholders are to be found.
426      * @param {Array} strings The strings to replace with.
427      * @return {String} The treated content.
428      */
429     Renderer.prototype.treatStringsInContent = function(content, strings) {
430         var pattern = /\[\[_s\d+\]\]/,
431             treated,
432             index,
433             strIndex,
434             walker,
435             char,
436             strFinal;
438         do {
439             treated = '';
440             index = content.search(pattern);
441             while (index > -1) {
443                 // Copy the part prior to the placeholder to the treated string.
444                 treated += content.substring(0, index);
445                 content = content.substr(index);
446                 strIndex = '';
447                 walker = 4; // 4 is the length of '[[_s'.
449                 // Walk the characters to manually extract the index of the string from the placeholder.
450                 char = content.substr(walker, 1);
451                 do {
452                     strIndex += char;
453                     walker++;
454                     char = content.substr(walker, 1);
455                 } while (char != ']');
457                 // Get the string, add it to the treated result, and remove the placeholder from the content to treat.
458                 strFinal = strings[parseInt(strIndex, 10)];
459                 if (typeof strFinal === 'undefined') {
460                     Log.debug('Could not find string for pattern [[_s' + strIndex + ']].');
461                     strFinal = '';
462                 }
463                 treated += strFinal;
464                 content = content.substr(6 + strIndex.length); // 6 is the length of the placeholder without the index: '[[_s]]'.
466                 // Find the next placeholder.
467                 index = content.search(pattern);
468             }
470             // The content becomes the treated part with the rest of the content.
471             content = treated + content;
473             // Check if we need to walk the content again, in case strings contained placeholders.
474             index = content.search(pattern);
476         } while (index > -1);
478         return content;
479     };
481     /**
482      * Treat strings in content.
483      *
484      * The purpose of this method is to replace the date placeholders found in the
485      * content with the their respective translated dates.
486      *
487      * @param {String} content The content in which string placeholders are to be found.
488      * @param {Array} strings The strings to replace with.
489      * @return {String} The treated content.
490      */
491     Renderer.prototype.treatDatesInContent = function(content, dates) {
492         dates.forEach(function(date, index) {
493             var key = '\\[\\[_t_' + index + '\\]\\]';
494             var re = new RegExp(key, 'g');
495             content = content.replace(re, date);
496         });
498         return content;
499     };
501     /**
502      * Render a template and then call the callback with the result.
503      *
504      * @method doRender
505      * @private
506      * @param {string} templateSource The mustache template to render.
507      * @param {Object} context Simple types used as the context for the template.
508      * @param {String} themeName Name of the current theme.
509      * @return {Promise} object
510      */
511     Renderer.prototype.doRender = function(templateSource, context, themeName) {
512         this.currentThemeName = themeName;
513         var iconTemplate = iconSystem.getTemplateName();
515         var pendingPromise = new Pending('core/templates:doRender');
516         return this.getTemplate(iconTemplate).then(function() {
517             this.addHelpers(context, themeName);
518             var result = mustache.render(templateSource, context, this.partialHelper.bind(this));
519             return $.Deferred().resolve(result.trim(), this.getJS()).promise();
520         }.bind(this))
521         .then(function(html, js) {
522             if (this.requiredStrings.length > 0) {
523                 return str.get_strings(this.requiredStrings).then(function(strings) {
525                     // Make sure string substitutions are done for the userdate
526                     // values as well.
527                     this.requiredDates = this.requiredDates.map(function(date) {
528                         return {
529                             timestamp: this.treatStringsInContent(date.timestamp, strings),
530                             format: this.treatStringsInContent(date.format, strings)
531                         };
532                     }.bind(this));
534                     // Why do we not do another call the render here?
535                     //
536                     // Because that would expose DOS holes. E.g.
537                     // I create an assignment called "{{fish" which
538                     // would get inserted in the template in the first pass
539                     // and cause the template to die on the second pass (unbalanced).
540                     html = this.treatStringsInContent(html, strings);
541                     js = this.treatStringsInContent(js, strings);
542                     return $.Deferred().resolve(html, js).promise();
543                 }.bind(this));
544             }
546             return $.Deferred().resolve(html, js).promise();
547         }.bind(this))
548         .then(function(html, js) {
549             // This has to happen after the strings replacement because you can
550             // use the string helper in content for the user date helper.
551             if (this.requiredDates.length > 0) {
552                 return UserDate.get(this.requiredDates).then(function(dates) {
553                     html = this.treatDatesInContent(html, dates);
554                     js = this.treatDatesInContent(js, dates);
555                     return $.Deferred().resolve(html, js).promise();
556                 }.bind(this));
557             }
559             return $.Deferred().resolve(html, js).promise();
560         }.bind(this))
561         .then(function(html, js) {
562             pendingPromise.resolve();
563             return $.Deferred().resolve(html, js).promise();
564         });
565     };
567     /**
568      * Execute a block of JS returned from a template.
569      * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
570      *
571      * @method runTemplateJS
572      * @param {string} source - A block of javascript.
573      */
574     var runTemplateJS = function(source) {
575         if (source.trim() !== '') {
576             var newscript = $('<script>').attr('type', 'text/javascript').html(source);
577             $('head').append(newscript);
578         }
579     };
581     /**
582      * Do some DOM replacement and trigger correct events and fire javascript.
583      *
584      * @method domReplace
585      * @private
586      * @param {JQuery} element - Element or selector to replace.
587      * @param {String} newHTML - HTML to insert / replace.
588      * @param {String} newJS - Javascript to run after the insertion.
589      * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
590      */
591     var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
592         var replaceNode = $(element);
593         if (replaceNode.length) {
594             // First create the dom nodes so we have a reference to them.
595             var newNodes = $(newHTML);
596             var yuiNodes = null;
597             // Do the replacement in the page.
598             if (replaceChildNodes) {
599                 // Cleanup any YUI event listeners attached to any of these nodes.
600                 yuiNodes = new Y.NodeList(replaceNode.children().get());
601                 yuiNodes.destroy(true);
603                 // JQuery will cleanup after itself.
604                 replaceNode.empty();
605                 replaceNode.append(newNodes);
606             } else {
607                 // Cleanup any YUI event listeners attached to any of these nodes.
608                 yuiNodes = new Y.NodeList(replaceNode.get());
609                 yuiNodes.destroy(true);
611                 // JQuery will cleanup after itself.
612                 replaceNode.replaceWith(newNodes);
613             }
614             // Run any javascript associated with the new HTML.
615             runTemplateJS(newJS);
616             // Notify all filters about the new content.
617             event.notifyFilterContentUpdated(newNodes);
618         }
619     };
621     /**
622      * Scan a template source for partial tags and return a list of the found partials.
623      *
624      * @method scanForPartials
625      * @private
626      * @param {string} templateSource - source template to scan.
627      * @return {Array} List of partials.
628      */
629     Renderer.prototype.scanForPartials = function(templateSource) {
630         var tokens = mustache.parse(templateSource),
631             partials = [];
633         var findPartial = function(tokens, partials) {
634             var i, token;
635             for (i = 0; i < tokens.length; i++) {
636                 token = tokens[i];
637                 if (token[0] == '>' || token[0] == '<') {
638                     partials.push(token[1]);
639                 }
640                 if (token.length > 4) {
641                     findPartial(token[4], partials);
642                 }
643             }
644         };
646         findPartial(tokens, partials);
648         return partials;
649     };
651     /**
652      * Load a template and scan it for partials. Recursively fetch the partials.
653      *
654      * @method cachePartials
655      * @private
656      * @param {string} templateName - should consist of the component and the name of the template like this:
657      *                              core/menu (lib/templates/menu.mustache) or
658      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
659      * @return {Promise} JQuery promise object resolved when all partials are in the cache.
660      */
661     Renderer.prototype.cachePartials = function(templateName) {
662         var searchKey = this.currentThemeName + '/' + templateName;
664         if (searchKey in cachePartialPromises) {
665             return cachePartialPromises[searchKey];
666         }
668         // This promise will not be resolved until all child partials are also resolved and ready.
669         // We create it here to allow us to check for recursive inclusion of templates.
670         cachePartialPromises[searchKey] = $.Deferred();
672         this.getTemplate(templateName)
673         .then(function(templateSource) {
674             var partials = this.scanForPartials(templateSource);
675             var uniquePartials = partials.filter(function(partialName) {
676                 // Check for recursion.
678                 if (typeof cachePartialPromises[this.currentThemeName + '/' + partialName] !== 'undefined') {
679                     // Ignore templates which include their parent.
680                     return false;
681                 }
683                 // Ignore templates that include themselves.
684                 return partialName != templateName;
685             }.bind(this));
687             // Fetch any partial which has not already been fetched.
688             var fetchThemAll = uniquePartials.map(function(partialName) {
689                 return this.cachePartials(partialName);
690             }.bind(this));
692             // Resolve the templateName promise when all of the children are resolved.
693             return $.when.apply($, fetchThemAll)
694             .then(function() {
695                 return cachePartialPromises[searchKey].resolve(templateSource);
696             });
697         }.bind(this))
698         .catch(cachePartialPromises[searchKey].reject);
700         return cachePartialPromises[searchKey];
701     };
703     /**
704      * Load a template and call doRender on it.
705      *
706      * @method render
707      * @private
708      * @param {string} templateName - should consist of the component and the name of the template like this:
709      *                              core/menu (lib/templates/menu.mustache) or
710      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
711      * @param {Object} context - Could be array, string or simple value for the context of the template.
712      * @param {string} themeName - Name of the current theme.
713      * @return {Promise} JQuery promise object resolved when the template has been rendered.
714      */
715     Renderer.prototype.render = function(templateName, context, themeName) {
716         if (typeof (themeName) === "undefined") {
717             // System context by default.
718             themeName = config.theme;
719         }
721         this.currentThemeName = themeName;
723         // Preload the module to do the icon rendering based on the theme iconsystem.
724         var modulename = config.iconsystemmodule;
726         var ready = $.Deferred();
727         require([modulename], function(System) {
728             var system = new System();
729             if (!(system instanceof IconSystem)) {
730                 ready.reject('Invalid icon system specified' + config.iconsystem);
731             } else {
732                 iconSystem = system;
733                 system.init().then(ready.resolve).catch(notification.exception);
734             }
735         });
737         return ready.then(function() {
738                 return this.cachePartials(templateName);
739             }.bind(this)).then(function(templateSource) {
740                 return this.doRender(templateSource, context, themeName);
741             }.bind(this));
742     };
744     /**
745      * Prepend some HTML to a node and trigger events and fire javascript.
746      *
747      * @method domPrepend
748      * @private
749      * @param {jQuery|String} element - Element or selector to prepend HTML to
750      * @param {String} html - HTML to prepend
751      * @param {String} js - Javascript to run after we prepend the html
752      */
753     var domPrepend = function(element, html, js) {
754         var node = $(element);
755         if (node.length) {
756             // Prepend the html.
757             node.prepend(html);
758             // Run any javascript associated with the new HTML.
759             runTemplateJS(js);
760             // Notify all filters about the new content.
761             event.notifyFilterContentUpdated(node);
762         }
763     };
765     /**
766      * Append some HTML to a node and trigger events and fire javascript.
767      *
768      * @method domAppend
769      * @private
770      * @param {jQuery|String} element - Element or selector to append HTML to
771      * @param {String} html - HTML to append
772      * @param {String} js - Javascript to run after we append the html
773      */
774     var domAppend = function(element, html, js) {
775         var node = $(element);
776         if (node.length) {
777             // Append the html.
778             node.append(html);
779             // Run any javascript associated with the new HTML.
780             runTemplateJS(js);
781             // Notify all filters about the new content.
782             event.notifyFilterContentUpdated(node);
783         }
784     };
786     return /** @alias module:core/templates */ {
787         // Public variables and functions.
788         /**
789          * Every call to render creates a new instance of the class and calls render on it. This
790          * means each render call has it's own class variables.
791          *
792          * @method render
793          * @private
794          * @param {string} templateName - should consist of the component and the name of the template like this:
795          *                              core/menu (lib/templates/menu.mustache) or
796          *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
797          * @param {Object} context - Could be array, string or simple value for the context of the template.
798          * @param {string} themeName - Name of the current theme.
799          * @return {Promise} JQuery promise object resolved when the template has been rendered.
800          */
801         render: function(templateName, context, themeName) {
802             var renderer = new Renderer();
803             return renderer.render(templateName, context, themeName);
804         },
806         /**
807          * Every call to renderIcon creates a new instance of the class and calls renderIcon on it. This
808          * means each render call has it's own class variables.
809          *
810          * @method renderIcon
811          * @public
812          * @param {string} key - Icon key.
813          * @param {string} component - Icon component
814          * @param {string} title - Icon title
815          * @return {Promise} JQuery promise object resolved when the pix has been rendered.
816          */
817         renderPix: function(key, component, title) {
818             var renderer = new Renderer();
819             return renderer.renderIcon(key, component, title);
820         },
822         /**
823          * Execute a block of JS returned from a template.
824          * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
825          *
826          * @method runTemplateJS
827          * @param {string} source - A block of javascript.
828          */
829         runTemplateJS: runTemplateJS,
831         /**
832          * Replace a node in the page with some new HTML and run the JS.
833          *
834          * @method replaceNodeContents
835          * @param {JQuery} element - Element or selector to replace.
836          * @param {String} newHTML - HTML to insert / replace.
837          * @param {String} newJS - Javascript to run after the insertion.
838          */
839         replaceNodeContents: function(element, newHTML, newJS) {
840             domReplace(element, newHTML, newJS, true);
841         },
843         /**
844          * Insert a node in the page with some new HTML and run the JS.
845          *
846          * @method replaceNode
847          * @param {JQuery} element - Element or selector to replace.
848          * @param {String} newHTML - HTML to insert / replace.
849          * @param {String} newJS - Javascript to run after the insertion.
850          */
851         replaceNode: function(element, newHTML, newJS) {
852             domReplace(element, newHTML, newJS, false);
853         },
855         /**
856          * Prepend some HTML to a node and trigger events and fire javascript.
857          *
858          * @method prependNodeContents
859          * @param {jQuery|String} element - Element or selector to prepend HTML to
860          * @param {String} html - HTML to prepend
861          * @param {String} js - Javascript to run after we prepend the html
862          */
863         prependNodeContents: function(element, html, js) {
864             domPrepend(element, html, js);
865         },
867         /**
868          * Append some HTML to a node and trigger events and fire javascript.
869          *
870          * @method appendNodeContents
871          * @param {jQuery|String} element - Element or selector to append HTML to
872          * @param {String} html - HTML to append
873          * @param {String} js - Javascript to run after we append the html
874          */
875         appendNodeContents: function(element, html, js) {
876             domAppend(element, html, js);
877         }
878     };
879 });