MDL-63667 output: Fix for recursive templates
[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
0015f5b8
RW
53 /** @var {Promise[]} cachePartialPromises - Cache of already loaded template partial promises */
54 var cachePartialPromises = {};
55
95b06c13
DW
56 /** @var {Object} iconSystem - Object extending core/iconsystem */
57 var iconSystem = {};
58
8d00afb1
DW
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 = [];
0e5b3e28 67 this.requiredDates = [];
8d00afb1
DW
68 this.currentThemeName = '';
69 };
70 // Class variables and functions.
71
9bdcf579 72 /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
8d00afb1 73 Renderer.prototype.requiredStrings = null;
9bdcf579 74
0e5b3e28
RW
75 /** @var {object[]} requiredDates - Collection of dates found during the rendering of one template */
76 Renderer.prototype.requiredDates = [];
77
9bdcf579 78 /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
8d00afb1 79 Renderer.prototype.requiredJS = null;
9bdcf579
DW
80
81 /** @var {String} themeName for the current render */
8d00afb1 82 Renderer.prototype.currentThemeName = '';
9bdcf579 83
f992dcf6
DP
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 */
39bf2a98 94 Renderer.prototype.getTemplate = function(templateName) {
f992dcf6
DP
95 var parts = templateName.split('/');
96 var component = parts.shift();
97 var name = parts.shift();
98
8d00afb1 99 var searchKey = this.currentThemeName + '/' + templateName;
f992dcf6
DP
100
101 // First try request variables.
39bf2a98
DW
102 if (searchKey in templatePromises) {
103 return templatePromises[searchKey];
f992dcf6
DP
104 }
105
106 // Now try local storage.
107 var cached = storage.get('core_template/' + searchKey);
108
109 if (cached) {
39bf2a98
DW
110 templateCache[searchKey] = cached;
111 templatePromises[searchKey] = $.Deferred().resolve(cached).promise();
112 return templatePromises[searchKey];
f992dcf6
DP
113 }
114
115 // Oh well - load via ajax.
116 var promises = ajax.call([{
117 methodname: 'core_output_load_template',
35be5826 118 args: {
f992dcf6
DP
119 component: component,
120 template: name,
8d00afb1 121 themename: this.currentThemeName
f992dcf6 122 }
39bf2a98 123 }], true, false);
f992dcf6 124
39bf2a98 125 templatePromises[searchKey] = promises[0].then(
35be5826 126 function(templateSource) {
39bf2a98 127 templateCache[searchKey] = templateSource;
f992dcf6 128 storage.set('core_template/' + searchKey, templateSource);
f20a336b 129 return templateSource;
f992dcf6
DP
130 }
131 );
39bf2a98 132 return templatePromises[searchKey];
f992dcf6
DP
133 };
134
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 */
8d00afb1 143 Renderer.prototype.partialHelper = function(name) {
f992dcf6 144
39bf2a98
DW
145 var searchKey = this.currentThemeName + '/' + name;
146
147 if (!(searchKey in templateCache)) {
148 notification.exception(new Error('Failed to pre-fetch the template: ' + name));
149 }
f992dcf6 150
39bf2a98 151 return templateCache[searchKey];
f992dcf6
DP
152 };
153
95b06c13
DW
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.
e330b1c2 166 var modulename = config.iconsystemmodule;
95b06c13 167
e330b1c2 168 // RequireJS does not return a promise.
95b06c13
DW
169 var ready = $.Deferred();
170 require([modulename], function(System) {
171 var system = new System();
172 if (!(system instanceof IconSystem)) {
e330b1c2 173 ready.reject('Invalid icon system specified' + config.iconsystemmodule);
95b06c13 174 } else {
95b06c13 175 iconSystem = system;
751ec025 176 system.init().then(ready.resolve).catch(notification.exception);
95b06c13
DW
177 }
178 });
179
e330b1c2
DW
180 return ready.then(function(iconSystem) {
181 return this.getTemplate(iconSystem.getTemplateName());
95b06c13
DW
182 }.bind(this)).then(function(template) {
183 return iconSystem.renderIcon(key, component, title, template);
184 });
185 };
186
9bdcf579
DW
187 /**
188 * Render image icons.
189 *
190 * @method pixHelper
191 * @private
8d00afb1 192 * @param {object} context The mustache context
9bdcf579 193 * @param {string} sectionText The text to parse arguments from.
e37d53da 194 * @param {function} helper Used to render the alt attribute of the text.
9bdcf579
DW
195 * @return {string}
196 */
8d00afb1 197 Renderer.prototype.pixHelper = function(context, sectionText, helper) {
9bdcf579
DW
198 var parts = sectionText.split(',');
199 var key = '';
200 var component = '';
201 var text = '';
9bdcf579
DW
202
203 if (parts.length > 0) {
e03d5d47 204 key = helper(parts.shift().trim(), context);
9bdcf579
DW
205 }
206 if (parts.length > 0) {
e03d5d47 207 component = helper(parts.shift().trim(), context);
9bdcf579
DW
208 }
209 if (parts.length > 0) {
e03d5d47 210 text = helper(parts.join(',').trim(), context);
9bdcf579 211 }
95b06c13 212
e330b1c2 213 var templateName = iconSystem.getTemplateName();
95b06c13 214
e330b1c2 215 var searchKey = this.currentThemeName + '/' + templateName;
39bf2a98 216 var template = templateCache[searchKey];
95b06c13 217
74e238f4
DM
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, '/');
221
95b06c13 222 return iconSystem.renderIcon(key, component, text, template);
9bdcf579
DW
223 };
224
9bdcf579
DW
225 /**
226 * Render blocks of javascript and save them in an array.
227 *
228 * @method jsHelper
229 * @private
8d00afb1 230 * @param {object} context The current mustache context.
9bdcf579
DW
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 */
8d00afb1
DW
235 Renderer.prototype.jsHelper = function(context, sectionText, helper) {
236 this.requiredJS.push(helper(sectionText, context));
9bdcf579
DW
237 return '';
238 };
239
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
8d00afb1 246 * @param {object} context The current mustache context.
9bdcf579
DW
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 */
8d00afb1 251 Renderer.prototype.stringHelper = function(context, sectionText, helper) {
9bdcf579
DW
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 }
265
266 if (param !== '') {
267 // Allow variable expansion in the param part only.
8d00afb1 268 param = helper(param, context);
9bdcf579
DW
269 }
270 // Allow json formatted $a arguments.
271 if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
272 param = JSON.parse(param);
273 }
274
8d00afb1
DW
275 var index = this.requiredStrings.length;
276 this.requiredStrings.push({key: key, component: component, param: param});
a89cf237
FM
277
278 // The placeholder must not use {{}} as those can be misinterpreted by the engine.
279 return '[[_s' + index + ']]';
9bdcf579
DW
280 };
281
0b4bff8c
AN
282 /**
283 * Quote helper used to wrap content in quotes, and escape all quotes present in the content.
284 *
285 * @method quoteHelper
286 * @private
8d00afb1 287 * @param {object} context The current mustache context.
0b4bff8c
AN
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 */
8d00afb1
DW
292 Renderer.prototype.quoteHelper = function(context, sectionText, helper) {
293 var content = helper(sectionText.trim(), context);
0b4bff8c
AN
294
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 };
303
75378ded
RW
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 };
330
0e5b3e28
RW
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;
348
349 this.requiredDates.push({
350 timestamp: timestamp,
351 format: format
352 });
353
354 return '[[_t_' + index + ']]';
355 };
356
9bdcf579
DW
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 */
8d00afb1
DW
366 Renderer.prototype.addHelpers = function(context, themeName) {
367 this.currentThemeName = themeName;
368 this.requiredStrings = [];
369 this.requiredJS = [];
370 context.uniqid = (uniqInstances++);
c96f55e6 371 context.str = function() {
8d00afb1
DW
372 return this.stringHelper.bind(this, context);
373 }.bind(this);
c96f55e6 374 context.pix = function() {
8d00afb1
DW
375 return this.pixHelper.bind(this, context);
376 }.bind(this);
c96f55e6 377 context.js = function() {
8d00afb1
DW
378 return this.jsHelper.bind(this, context);
379 }.bind(this);
c96f55e6 380 context.quote = function() {
8d00afb1
DW
381 return this.quoteHelper.bind(this, context);
382 }.bind(this);
75378ded
RW
383 context.shortentext = function() {
384 return this.shortenTextHelper.bind(this, context);
385 }.bind(this);
0e5b3e28
RW
386 context.userdate = function() {
387 return this.userDateHelper.bind(this, context);
388 }.bind(this);
9f5f3dcc 389 context.globals = {config: config};
9bdcf579
DW
390 context.currentTheme = themeName;
391 };
392
393 /**
394 * Get all the JS blocks from the last rendered template.
395 *
396 * @method getJS
397 * @private
9bdcf579
DW
398 * @return {string}
399 */
0e5b3e28 400 Renderer.prototype.getJS = function() {
9bdcf579 401 var js = '';
8d00afb1
DW
402 if (this.requiredJS.length > 0) {
403 js = this.requiredJS.join(";\n");
9bdcf579
DW
404 }
405
0e5b3e28 406 return js;
29879f8f
FM
407 };
408
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 */
8d00afb1 426 Renderer.prototype.treatStringsInContent = function(content, strings) {
a89cf237 427 var pattern = /\[\[_s\d+\]\]/,
29879f8f
FM
428 treated,
429 index,
430 strIndex,
431 walker,
432 char,
433 strFinal;
434
435 do {
436 treated = '';
437 index = content.search(pattern);
438 while (index > -1) {
439
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 = '';
d9dff92f 444 walker = 4; // 4 is the length of '[[_s'.
29879f8f
FM
445
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);
a89cf237 452 } while (char != ']');
29879f8f
FM
453
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') {
a89cf237 457 Log.debug('Could not find string for pattern [[_s' + strIndex + ']].');
29879f8f
FM
458 strFinal = '';
459 }
460 treated += strFinal;
d9dff92f 461 content = content.substr(6 + strIndex.length); // 6 is the length of the placeholder without the index: '[[_s]]'.
29879f8f
FM
462
463 // Find the next placeholder.
464 index = content.search(pattern);
465 }
466
467 // The content becomes the treated part with the rest of the content.
468 content = treated + content;
469
470 // Check if we need to walk the content again, in case strings contained placeholders.
471 index = content.search(pattern);
472
473 } while (index > -1);
474
475 return content;
9bdcf579
DW
476 };
477
0e5b3e28
RW
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 });
494
495 return content;
496 };
497
9bdcf579
DW
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 */
8d00afb1 508 Renderer.prototype.doRender = function(templateSource, context, themeName) {
8d00afb1 509 this.currentThemeName = themeName;
e330b1c2 510 var iconTemplate = iconSystem.getTemplateName();
9bdcf579 511
e330b1c2 512 return this.getTemplate(iconTemplate).then(function() {
f20a336b
DW
513 this.addHelpers(context, themeName);
514 var result = mustache.render(templateSource, context, this.partialHelper.bind(this));
0e5b3e28
RW
515 return $.Deferred().resolve(result.trim(), this.getJS()).promise();
516 }.bind(this))
517 .then(function(html, js) {
f20a336b
DW
518 if (this.requiredStrings.length > 0) {
519 return str.get_strings(this.requiredStrings).then(function(strings) {
520
0e5b3e28
RW
521 // Make sure string substitutions are done for the userdate
522 // values as well.
523 this.requiredDates = this.requiredDates.map(function(date) {
524 return {
525 timestamp: this.treatStringsInContent(date.timestamp, strings),
526 format: this.treatStringsInContent(date.format, strings)
527 };
528 }.bind(this));
529
f20a336b
DW
530 // Why do we not do another call the render here?
531 //
532 // Because that would expose DOS holes. E.g.
533 // I create an assignment called "{{fish" which
534 // would get inserted in the template in the first pass
535 // and cause the template to die on the second pass (unbalanced).
0e5b3e28
RW
536 html = this.treatStringsInContent(html, strings);
537 js = this.treatStringsInContent(js, strings);
538 return $.Deferred().resolve(html, js).promise();
539 }.bind(this));
540 }
f20a336b 541
0e5b3e28
RW
542 return $.Deferred().resolve(html, js).promise();
543 }.bind(this))
544 .then(function(html, js) {
545 // This has to happen after the strings replacement because you can
546 // use the string helper in content for the user date helper.
547 if (this.requiredDates.length > 0) {
548 return UserDate.get(this.requiredDates).then(function(dates) {
549 html = this.treatDatesInContent(html, dates);
550 js = this.treatDatesInContent(js, dates);
551 return $.Deferred().resolve(html, js).promise();
f20a336b 552 }.bind(this));
f20a336b 553 }
0e5b3e28
RW
554
555 return $.Deferred().resolve(html, js).promise();
f20a336b 556 }.bind(this));
9bdcf579
DW
557 };
558
28de7771
DW
559 /**
560 * Execute a block of JS returned from a template.
561 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
562 *
563 * @method runTemplateJS
564 * @param {string} source - A block of javascript.
565 */
566 var runTemplateJS = function(source) {
567 if (source.trim() !== '') {
35be5826 568 var newscript = $('<script>').attr('type', 'text/javascript').html(source);
28de7771
DW
569 $('head').append(newscript);
570 }
571 };
572
573 /**
574 * Do some DOM replacement and trigger correct events and fire javascript.
575 *
576 * @method domReplace
577 * @private
578 * @param {JQuery} element - Element or selector to replace.
579 * @param {String} newHTML - HTML to insert / replace.
580 * @param {String} newJS - Javascript to run after the insertion.
581 * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
582 */
583 var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
584 var replaceNode = $(element);
585 if (replaceNode.length) {
586 // First create the dom nodes so we have a reference to them.
587 var newNodes = $(newHTML);
1fca8a7b 588 var yuiNodes = null;
28de7771
DW
589 // Do the replacement in the page.
590 if (replaceChildNodes) {
1fca8a7b
DW
591 // Cleanup any YUI event listeners attached to any of these nodes.
592 yuiNodes = new Y.NodeList(replaceNode.children().get());
593 yuiNodes.destroy(true);
594
595 // JQuery will cleanup after itself.
28de7771
DW
596 replaceNode.empty();
597 replaceNode.append(newNodes);
598 } else {
1fca8a7b
DW
599 // Cleanup any YUI event listeners attached to any of these nodes.
600 yuiNodes = new Y.NodeList(replaceNode.get());
601 yuiNodes.destroy(true);
602
603 // JQuery will cleanup after itself.
28de7771
DW
604 replaceNode.replaceWith(newNodes);
605 }
606 // Run any javascript associated with the new HTML.
607 runTemplateJS(newJS);
608 // Notify all filters about the new content.
609 event.notifyFilterContentUpdated(newNodes);
610 }
611 };
612
39bf2a98
DW
613 /**
614 * Scan a template source for partial tags and return a list of the found partials.
615 *
616 * @method scanForPartials
617 * @private
618 * @param {string} templateSource - source template to scan.
619 * @return {Array} List of partials.
620 */
621 Renderer.prototype.scanForPartials = function(templateSource) {
622 var tokens = mustache.parse(templateSource),
623 partials = [];
624
625 var findPartial = function(tokens, partials) {
626 var i, token;
627 for (i = 0; i < tokens.length; i++) {
628 token = tokens[i];
629 if (token[0] == '>' || token[0] == '<') {
630 partials.push(token[1]);
631 }
632 if (token.length > 4) {
633 findPartial(token[4], partials);
634 }
635 }
636 };
637
638 findPartial(tokens, partials);
639
640 return partials;
641 };
642
643 /**
644 * Load a template and scan it for partials. Recursively fetch the partials.
645 *
646 * @method cachePartials
647 * @private
648 * @param {string} templateName - should consist of the component and the name of the template like this:
649 * core/menu (lib/templates/menu.mustache) or
650 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
651 * @return {Promise} JQuery promise object resolved when all partials are in the cache.
652 */
653 Renderer.prototype.cachePartials = function(templateName) {
0015f5b8
RW
654 var searchKey = this.currentThemeName + '/' + templateName;
655
656 if (searchKey in cachePartialPromises) {
657 return cachePartialPromises[searchKey];
658 }
659
660 var cachePartialPromise = this.getTemplate(templateName).then(function(templateSource) {
39bf2a98 661 var partials = this.scanForPartials(templateSource);
b51c5b92
DW
662 // Ignore templates that include themselves.
663 var uniquePartials = partials.filter(function(partialName) {
664 return partialName != templateName;
665 });
666 var fetchThemAll = uniquePartials.map(function(partialName) {
0015f5b8
RW
667 return this.cachePartials(partialName);
668 }.bind(this));
39bf2a98 669
f3cd5c5b 670 return $.when.apply($, fetchThemAll).then(function() {
39bf2a98 671 return templateSource;
b302369d 672 });
39bf2a98 673 }.bind(this));
0015f5b8
RW
674
675 cachePartialPromises[searchKey] = cachePartialPromise;
676
677 return cachePartialPromise;
39bf2a98
DW
678 };
679
8d00afb1
DW
680 /**
681 * Load a template and call doRender on it.
682 *
683 * @method render
684 * @private
685 * @param {string} templateName - should consist of the component and the name of the template like this:
686 * core/menu (lib/templates/menu.mustache) or
687 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
688 * @param {Object} context - Could be array, string or simple value for the context of the template.
689 * @param {string} themeName - Name of the current theme.
690 * @return {Promise} JQuery promise object resolved when the template has been rendered.
691 */
692 Renderer.prototype.render = function(templateName, context, themeName) {
8d00afb1
DW
693 if (typeof (themeName) === "undefined") {
694 // System context by default.
695 themeName = config.theme;
696 }
697
698 this.currentThemeName = themeName;
699
95b06c13 700 // Preload the module to do the icon rendering based on the theme iconsystem.
e330b1c2 701 var modulename = config.iconsystemmodule;
95b06c13
DW
702
703 var ready = $.Deferred();
704 require([modulename], function(System) {
705 var system = new System();
706 if (!(system instanceof IconSystem)) {
707 ready.reject('Invalid icon system specified' + config.iconsystem);
708 } else {
709 iconSystem = system;
751ec025 710 system.init().then(ready.resolve).catch(notification.exception);
95b06c13
DW
711 }
712 });
713
714 return ready.then(function() {
715 return this.cachePartials(templateName);
716 }.bind(this)).then(function(templateSource) {
717 return this.doRender(templateSource, context, themeName);
718 }.bind(this));
8d00afb1
DW
719 };
720
90525930
MN
721 /**
722 * Prepend some HTML to a node and trigger events and fire javascript.
723 *
724 * @method domPrepend
725 * @private
726 * @param {jQuery|String} element - Element or selector to prepend HTML to
727 * @param {String} html - HTML to prepend
728 * @param {String} js - Javascript to run after we prepend the html
729 */
730 var domPrepend = function(element, html, js) {
731 var node = $(element);
732 if (node.length) {
733 // Prepend the html.
734 node.prepend(html);
735 // Run any javascript associated with the new HTML.
736 runTemplateJS(js);
737 // Notify all filters about the new content.
738 event.notifyFilterContentUpdated(node);
739 }
740 };
28de7771 741
f7775c9a
MN
742 /**
743 * Append some HTML to a node and trigger events and fire javascript.
744 *
745 * @method domAppend
746 * @private
747 * @param {jQuery|String} element - Element or selector to append HTML to
748 * @param {String} html - HTML to append
749 * @param {String} js - Javascript to run after we append the html
750 */
751 var domAppend = function(element, html, js) {
752 var node = $(element);
753 if (node.length) {
754 // Append the html.
755 node.append(html);
756 // Run any javascript associated with the new HTML.
757 runTemplateJS(js);
758 // Notify all filters about the new content.
759 event.notifyFilterContentUpdated(node);
760 }
761 };
762
9bdcf579
DW
763 return /** @alias module:core/templates */ {
764 // Public variables and functions.
765 /**
8d00afb1
DW
766 * Every call to render creates a new instance of the class and calls render on it. This
767 * means each render call has it's own class variables.
9bdcf579
DW
768 *
769 * @method render
770 * @private
771 * @param {string} templateName - should consist of the component and the name of the template like this:
772 * core/menu (lib/templates/menu.mustache) or
773 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
774 * @param {Object} context - Could be array, string or simple value for the context of the template.
775 * @param {string} themeName - Name of the current theme.
776 * @return {Promise} JQuery promise object resolved when the template has been rendered.
777 */
778 render: function(templateName, context, themeName) {
8d00afb1
DW
779 var renderer = new Renderer();
780 return renderer.render(templateName, context, themeName);
9bdcf579
DW
781 },
782
95b06c13
DW
783 /**
784 * Every call to renderIcon creates a new instance of the class and calls renderIcon on it. This
785 * means each render call has it's own class variables.
786 *
787 * @method renderIcon
788 * @public
789 * @param {string} key - Icon key.
790 * @param {string} component - Icon component
791 * @param {string} title - Icon title
792 * @return {Promise} JQuery promise object resolved when the pix has been rendered.
793 */
794 renderPix: function(key, component, title) {
795 var renderer = new Renderer();
796 return renderer.renderIcon(key, component, title);
797 },
798
9bdcf579
DW
799 /**
800 * Execute a block of JS returned from a template.
801 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
802 *
803 * @method runTemplateJS
9bdcf579
DW
804 * @param {string} source - A block of javascript.
805 */
28de7771
DW
806 runTemplateJS: runTemplateJS,
807
808 /**
809 * Replace a node in the page with some new HTML and run the JS.
810 *
811 * @method replaceNodeContents
c96f55e6
DP
812 * @param {JQuery} element - Element or selector to replace.
813 * @param {String} newHTML - HTML to insert / replace.
814 * @param {String} newJS - Javascript to run after the insertion.
28de7771
DW
815 */
816 replaceNodeContents: function(element, newHTML, newJS) {
c96f55e6 817 domReplace(element, newHTML, newJS, true);
28de7771
DW
818 },
819
820 /**
821 * Insert a node in the page with some new HTML and run the JS.
822 *
823 * @method replaceNode
c96f55e6
DP
824 * @param {JQuery} element - Element or selector to replace.
825 * @param {String} newHTML - HTML to insert / replace.
826 * @param {String} newJS - Javascript to run after the insertion.
28de7771
DW
827 */
828 replaceNode: function(element, newHTML, newJS) {
c96f55e6 829 domReplace(element, newHTML, newJS, false);
f7775c9a
MN
830 },
831
90525930
MN
832 /**
833 * Prepend some HTML to a node and trigger events and fire javascript.
834 *
835 * @method prependNodeContents
836 * @param {jQuery|String} element - Element or selector to prepend HTML to
837 * @param {String} html - HTML to prepend
838 * @param {String} js - Javascript to run after we prepend the html
839 */
840 prependNodeContents: function(element, html, js) {
841 domPrepend(element, html, js);
842 },
843
f7775c9a
MN
844 /**
845 * Append some HTML to a node and trigger events and fire javascript.
846 *
847 * @method appendNodeContents
848 * @param {jQuery|String} element - Element or selector to append HTML to
849 * @param {String} html - HTML to append
850 * @param {String} js - Javascript to run after we append the html
851 */
852 appendNodeContents: function(element, html, js) {
853 domAppend(element, html, js);
9bdcf579
DW
854 }
855 };
856});