MDL-40759 icons: Refactor to allow theme icon systems
[moodle.git] / lib / amd / src / templates.js
CommitLineData
9bdcf579
DW
1// This file is part of Moodle - http://moodle.org/
2//
3// Moodle is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// Moodle is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
15
16/**
17 * Template renderer for Moodle. Load and render Moodle templates with Mustache.
18 *
19 * @module core/templates
20 * @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 */
35be5826 26define(['core/mustache',
9bdcf579
DW
27 'jquery',
28 'core/ajax',
29 'core/str',
30 'core/notification',
31 'core/url',
4b9e5326 32 'core/config',
28de7771 33 'core/localstorage',
95b06c13 34 'core/icon_system',
1fca8a7b 35 'core/event',
29879f8f 36 'core/yui',
75378ded 37 'core/log',
e2310e43 38 'core/truncate',
0e5b3e28 39 'core/user_date'
9bdcf579 40 ],
95b06c13 41 function(mustache, $, ajax, str, notification, coreurl, config, storage, IconSystem, event, Y, Log, Truncate, UserDate) {
9bdcf579 42
8d00afb1
DW
43 // Module variables.
44 /** @var {Number} uniqInstances Count of times this constructor has been called. */
45 var uniqInstances = 0;
9bdcf579 46
39bf2a98 47 /** @var {String[]} templateCache - Cache of already loaded template strings */
9bdcf579
DW
48 var templateCache = {};
49
39bf2a98
DW
50 /** @var {Promise[]} templatePromises - Cache of already loaded template promises */
51 var templatePromises = {};
f20a336b 52
95b06c13
DW
53 /** @var {Object} iconSystem - Object extending core/iconsystem */
54 var iconSystem = {};
55
8d00afb1
DW
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 = [];
0e5b3e28 64 this.requiredDates = [];
8d00afb1
DW
65 this.currentThemeName = '';
66 };
67 // Class variables and functions.
68
9bdcf579 69 /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
8d00afb1 70 Renderer.prototype.requiredStrings = null;
9bdcf579 71
0e5b3e28
RW
72 /** @var {object[]} requiredDates - Collection of dates found during the rendering of one template */
73 Renderer.prototype.requiredDates = [];
74
9bdcf579 75 /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
8d00afb1 76 Renderer.prototype.requiredJS = null;
9bdcf579
DW
77
78 /** @var {String} themeName for the current render */
8d00afb1 79 Renderer.prototype.currentThemeName = '';
9bdcf579 80
f992dcf6
DP
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 */
39bf2a98 91 Renderer.prototype.getTemplate = function(templateName) {
f992dcf6
DP
92 var parts = templateName.split('/');
93 var component = parts.shift();
94 var name = parts.shift();
95
8d00afb1 96 var searchKey = this.currentThemeName + '/' + templateName;
f992dcf6
DP
97
98 // First try request variables.
39bf2a98
DW
99 if (searchKey in templatePromises) {
100 return templatePromises[searchKey];
f992dcf6
DP
101 }
102
103 // Now try local storage.
104 var cached = storage.get('core_template/' + searchKey);
105
106 if (cached) {
39bf2a98
DW
107 templateCache[searchKey] = cached;
108 templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
109 return templatePromises[searchKey];
f992dcf6
DP
110 }
111
112 // Oh well - load via ajax.
113 var promises = ajax.call([{
114 methodname: 'core_output_load_template',
35be5826 115 args: {
f992dcf6
DP
116 component: component,
117 template: name,
8d00afb1 118 themename: this.currentThemeName
f992dcf6 119 }
39bf2a98 120 }], true, false);
f992dcf6 121
39bf2a98 122 templatePromises[searchKey] = promises[0].then(
35be5826 123 function(templateSource) {
39bf2a98 124 templateCache[searchKey] = templateSource;
f992dcf6 125 storage.set('core_template/' + searchKey, templateSource);
f20a336b 126 return templateSource;
f992dcf6
DP
127 }
128 );
39bf2a98 129 return templatePromises[searchKey];
f992dcf6
DP
130 };
131
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 */
8d00afb1 140 Renderer.prototype.partialHelper = function(name) {
f992dcf6 141
39bf2a98
DW
142 var searchKey = this.currentThemeName + '/' + name;
143
144 if (!(searchKey in templateCache)) {
145 notification.exception(new Error('Failed to pre-fetch the template: ' + name));
146 }
f992dcf6 147
39bf2a98 148 return templateCache[searchKey];
f992dcf6
DP
149 };
150
95b06c13
DW
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.
e330b1c2 163 var modulename = config.iconsystemmodule;
95b06c13 164
e330b1c2 165 // RequireJS does not return a promise.
95b06c13
DW
166 var ready = $.Deferred();
167 require([modulename], function(System) {
168 var system = new System();
169 if (!(system instanceof IconSystem)) {
e330b1c2 170 ready.reject('Invalid icon system specified' + config.iconsystemmodule);
95b06c13 171 } else {
95b06c13 172 iconSystem = system;
e330b1c2 173 system.init().then(ready.resolve);
95b06c13
DW
174 }
175 });
176
e330b1c2
DW
177 return ready.then(function(iconSystem) {
178 return this.getTemplate(iconSystem.getTemplateName());
95b06c13
DW
179 }.bind(this)).then(function(template) {
180 return iconSystem.renderIcon(key, component, title, template);
181 });
182 };
183
9bdcf579
DW
184 /**
185 * Render image icons.
186 *
187 * @method pixHelper
188 * @private
8d00afb1 189 * @param {object} context The mustache context
9bdcf579 190 * @param {string} sectionText The text to parse arguments from.
e37d53da 191 * @param {function} helper Used to render the alt attribute of the text.
9bdcf579
DW
192 * @return {string}
193 */
8d00afb1 194 Renderer.prototype.pixHelper = function(context, sectionText, helper) {
9bdcf579
DW
195 var parts = sectionText.split(',');
196 var key = '';
197 var component = '';
198 var text = '';
9bdcf579
DW
199
200 if (parts.length > 0) {
95b06c13 201 key = helper(parts.shift().trim());
9bdcf579
DW
202 }
203 if (parts.length > 0) {
95b06c13 204 component = helper(parts.shift().trim());
9bdcf579
DW
205 }
206 if (parts.length > 0) {
95b06c13 207 text = helper(parts.join(',').trim());
9bdcf579 208 }
95b06c13 209
e330b1c2 210 var templateName = iconSystem.getTemplateName();
95b06c13 211
e330b1c2 212 var searchKey = this.currentThemeName + '/' + templateName;
39bf2a98 213 var template = templateCache[searchKey];
95b06c13
DW
214
215 return iconSystem.renderIcon(key, component, text, template);
9bdcf579
DW
216 };
217
9bdcf579
DW
218 /**
219 * Render blocks of javascript and save them in an array.
220 *
221 * @method jsHelper
222 * @private
8d00afb1 223 * @param {object} context The current mustache context.
9bdcf579
DW
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 */
8d00afb1
DW
228 Renderer.prototype.jsHelper = function(context, sectionText, helper) {
229 this.requiredJS.push(helper(sectionText, context));
9bdcf579
DW
230 return '';
231 };
232
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
8d00afb1 239 * @param {object} context The current mustache context.
9bdcf579
DW
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 */
8d00afb1 244 Renderer.prototype.stringHelper = function(context, sectionText, helper) {
9bdcf579
DW
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 }
258
259 if (param !== '') {
260 // Allow variable expansion in the param part only.
8d00afb1 261 param = helper(param, context);
9bdcf579
DW
262 }
263 // Allow json formatted $a arguments.
264 if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
265 param = JSON.parse(param);
266 }
267
8d00afb1
DW
268 var index = this.requiredStrings.length;
269 this.requiredStrings.push({key: key, component: component, param: param});
a89cf237
FM
270
271 // The placeholder must not use {{}} as those can be misinterpreted by the engine.
272 return '[[_s' + index + ']]';
9bdcf579
DW
273 };
274
0b4bff8c
AN
275 /**
276 * Quote helper used to wrap content in quotes, and escape all quotes present in the content.
277 *
278 * @method quoteHelper
279 * @private
8d00afb1 280 * @param {object} context The current mustache context.
0b4bff8c
AN
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 */
8d00afb1
DW
285 Renderer.prototype.quoteHelper = function(context, sectionText, helper) {
286 var content = helper(sectionText.trim(), context);
0b4bff8c
AN
287
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 };
296
75378ded
RW
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 };
323
0e5b3e28
RW
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;
341
342 this.requiredDates.push({
343 timestamp: timestamp,
344 format: format
345 });
346
347 return '[[_t_' + index + ']]';
348 };
349
9bdcf579
DW
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 */
8d00afb1
DW
359 Renderer.prototype.addHelpers = function(context, themeName) {
360 this.currentThemeName = themeName;
361 this.requiredStrings = [];
362 this.requiredJS = [];
363 context.uniqid = (uniqInstances++);
c96f55e6 364 context.str = function() {
8d00afb1
DW
365 return this.stringHelper.bind(this, context);
366 }.bind(this);
c96f55e6 367 context.pix = function() {
8d00afb1
DW
368 return this.pixHelper.bind(this, context);
369 }.bind(this);
c96f55e6 370 context.js = function() {
8d00afb1
DW
371 return this.jsHelper.bind(this, context);
372 }.bind(this);
c96f55e6 373 context.quote = function() {
8d00afb1
DW
374 return this.quoteHelper.bind(this, context);
375 }.bind(this);
75378ded
RW
376 context.shortentext = function() {
377 return this.shortenTextHelper.bind(this, context);
378 }.bind(this);
0e5b3e28
RW
379 context.userdate = function() {
380 return this.userDateHelper.bind(this, context);
381 }.bind(this);
9f5f3dcc 382 context.globals = {config: config};
9bdcf579
DW
383 context.currentTheme = themeName;
384 };
385
386 /**
387 * Get all the JS blocks from the last rendered template.
388 *
389 * @method getJS
390 * @private
9bdcf579
DW
391 * @return {string}
392 */
0e5b3e28 393 Renderer.prototype.getJS = function() {
9bdcf579 394 var js = '';
8d00afb1
DW
395 if (this.requiredJS.length > 0) {
396 js = this.requiredJS.join(";\n");
9bdcf579
DW
397 }
398
0e5b3e28 399 return js;
29879f8f
FM
400 };
401
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 */
8d00afb1 419 Renderer.prototype.treatStringsInContent = function(content, strings) {
a89cf237 420 var pattern = /\[\[_s\d+\]\]/,
29879f8f
FM
421 treated,
422 index,
423 strIndex,
424 walker,
425 char,
426 strFinal;
427
428 do {
429 treated = '';
430 index = content.search(pattern);
431 while (index > -1) {
432
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 = '';
a89cf237 437 walker = 4; // 4 is the length of '[[_s'.
29879f8f
FM
438
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);
a89cf237 445 } while (char != ']');
29879f8f
FM
446
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') {
a89cf237 450 Log.debug('Could not find string for pattern [[_s' + strIndex + ']].');
29879f8f
FM
451 strFinal = '';
452 }
453 treated += strFinal;
a89cf237 454 content = content.substr(6 + strIndex.length); // 6 is the length of the placeholder without the index: '[[_s]]'.
29879f8f
FM
455
456 // Find the next placeholder.
457 index = content.search(pattern);
458 }
459
460 // The content becomes the treated part with the rest of the content.
461 content = treated + content;
462
463 // Check if we need to walk the content again, in case strings contained placeholders.
464 index = content.search(pattern);
465
466 } while (index > -1);
467
468 return content;
9bdcf579
DW
469 };
470
0e5b3e28
RW
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 });
487
488 return content;
489 };
490
9bdcf579
DW
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 */
8d00afb1 501 Renderer.prototype.doRender = function(templateSource, context, themeName) {
8d00afb1 502 this.currentThemeName = themeName;
e330b1c2 503 var iconTemplate = iconSystem.getTemplateName();
9bdcf579 504
e330b1c2 505 return this.getTemplate(iconTemplate).then(function() {
f20a336b
DW
506 this.addHelpers(context, themeName);
507 var result = mustache.render(templateSource, context, this.partialHelper.bind(this));
0e5b3e28
RW
508 return $.Deferred().resolve(result.trim(), this.getJS()).promise();
509 }.bind(this))
510 .then(function(html, js) {
f20a336b
DW
511 if (this.requiredStrings.length > 0) {
512 return str.get_strings(this.requiredStrings).then(function(strings) {
513
0e5b3e28
RW
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));
522
f20a336b
DW
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).
0e5b3e28
RW
529 html = this.treatStringsInContent(html, strings);
530 js = this.treatStringsInContent(js, strings);
531 return $.Deferred().resolve(html, js).promise();
532 }.bind(this));
533 }
f20a336b 534
0e5b3e28
RW
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();
f20a336b 545 }.bind(this));
f20a336b 546 }
0e5b3e28
RW
547
548 return $.Deferred().resolve(html, js).promise();
f20a336b 549 }.bind(this));
9bdcf579
DW
550 };
551
28de7771
DW
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() !== '') {
35be5826 561 var newscript = $('<script>').attr('type', 'text/javascript').html(source);
28de7771
DW
562 $('head').append(newscript);
563 }
564 };
565
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);
1fca8a7b 581 var yuiNodes = null;
28de7771
DW
582 // Do the replacement in the page.
583 if (replaceChildNodes) {
1fca8a7b
DW
584 // Cleanup any YUI event listeners attached to any of these nodes.
585 yuiNodes = new Y.NodeList(replaceNode.children().get());
586 yuiNodes.destroy(true);
587
588 // JQuery will cleanup after itself.
28de7771
DW
589 replaceNode.empty();
590 replaceNode.append(newNodes);
591 } else {
1fca8a7b
DW
592 // Cleanup any YUI event listeners attached to any of these nodes.
593 yuiNodes = new Y.NodeList(replaceNode.get());
594 yuiNodes.destroy(true);
595
596 // JQuery will cleanup after itself.
28de7771
DW
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 };
605
39bf2a98
DW
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 = [];
617
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 };
630
631 findPartial(tokens, partials);
632
633 return partials;
634 };
635
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) {
b302369d 647 return this.getTemplate(templateName).then(function(templateSource) {
39bf2a98
DW
648 var i;
649 var partials = this.scanForPartials(templateSource);
650 var fetchThemAll = [];
651
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 }
659
f3cd5c5b 660 return $.when.apply($, fetchThemAll).then(function() {
39bf2a98 661 return templateSource;
b302369d 662 });
39bf2a98
DW
663 }.bind(this));
664 };
665
8d00afb1
DW
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) {
8d00afb1
DW
679 if (typeof (themeName) === "undefined") {
680 // System context by default.
681 themeName = config.theme;
682 }
683
684 this.currentThemeName = themeName;
685
95b06c13 686 // Preload the module to do the icon rendering based on the theme iconsystem.
e330b1c2 687 var modulename = config.iconsystemmodule;
95b06c13
DW
688
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 });
699
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));
8d00afb1
DW
705 };
706
90525930
MN
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 };
28de7771 727
f7775c9a
MN
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 };
748
9bdcf579
DW
749 return /** @alias module:core/templates */ {
750 // Public variables and functions.
751 /**
8d00afb1
DW
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.
9bdcf579
DW
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) {
8d00afb1
DW
765 var renderer = new Renderer();
766 return renderer.render(templateName, context, themeName);
9bdcf579
DW
767 },
768
95b06c13
DW
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 },
784
9bdcf579
DW
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
9bdcf579
DW
790 * @param {string} source - A block of javascript.
791 */
28de7771
DW
792 runTemplateJS: runTemplateJS,
793
794 /**
795 * Replace a node in the page with some new HTML and run the JS.
796 *
797 * @method replaceNodeContents
c96f55e6
DP
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.
28de7771
DW
801 */
802 replaceNodeContents: function(element, newHTML, newJS) {
c96f55e6 803 domReplace(element, newHTML, newJS, true);
28de7771
DW
804 },
805
806 /**
807 * Insert a node in the page with some new HTML and run the JS.
808 *
809 * @method replaceNode
c96f55e6
DP
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.
28de7771
DW
813 */
814 replaceNode: function(element, newHTML, newJS) {
c96f55e6 815 domReplace(element, newHTML, newJS, false);
f7775c9a
MN
816 },
817
90525930
MN
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 },
829
f7775c9a
MN
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);
9bdcf579
DW
840 }
841 };
842});