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