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