MDL-63714 core: Wrap doRender in pendingjs
[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
4b7ad884 512 M.util.js_pending('core/templates:doRender');
e330b1c2 513 return this.getTemplate(iconTemplate).then(function() {
f20a336b
DW
514 this.addHelpers(context, themeName);
515 var result = mustache.render(templateSource, context, this.partialHelper.bind(this));
0e5b3e28
RW
516 return $.Deferred().resolve(result.trim(), this.getJS()).promise();
517 }.bind(this))
518 .then(function(html, js) {
f20a336b
DW
519 if (this.requiredStrings.length > 0) {
520 return str.get_strings(this.requiredStrings).then(function(strings) {
521
0e5b3e28
RW
522 // Make sure string substitutions are done for the userdate
523 // values as well.
524 this.requiredDates = this.requiredDates.map(function(date) {
525 return {
526 timestamp: this.treatStringsInContent(date.timestamp, strings),
527 format: this.treatStringsInContent(date.format, strings)
528 };
529 }.bind(this));
530
f20a336b
DW
531 // Why do we not do another call the render here?
532 //
533 // Because that would expose DOS holes. E.g.
534 // I create an assignment called "{{fish" which
535 // would get inserted in the template in the first pass
536 // and cause the template to die on the second pass (unbalanced).
0e5b3e28
RW
537 html = this.treatStringsInContent(html, strings);
538 js = this.treatStringsInContent(js, strings);
539 return $.Deferred().resolve(html, js).promise();
540 }.bind(this));
541 }
f20a336b 542
0e5b3e28
RW
543 return $.Deferred().resolve(html, js).promise();
544 }.bind(this))
545 .then(function(html, js) {
546 // This has to happen after the strings replacement because you can
547 // use the string helper in content for the user date helper.
548 if (this.requiredDates.length > 0) {
549 return UserDate.get(this.requiredDates).then(function(dates) {
550 html = this.treatDatesInContent(html, dates);
551 js = this.treatDatesInContent(js, dates);
552 return $.Deferred().resolve(html, js).promise();
f20a336b 553 }.bind(this));
f20a336b 554 }
0e5b3e28
RW
555
556 return $.Deferred().resolve(html, js).promise();
4b7ad884
AN
557 }.bind(this))
558 .then(function(html, js) {
559 M.util.js_complete('core/templates:doRender');
560 return $.Deferred().resolve(html, js).promise();
561 });
9bdcf579
DW
562 };
563
28de7771
DW
564 /**
565 * Execute a block of JS returned from a template.
566 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
567 *
568 * @method runTemplateJS
569 * @param {string} source - A block of javascript.
570 */
571 var runTemplateJS = function(source) {
572 if (source.trim() !== '') {
35be5826 573 var newscript = $('<script>').attr('type', 'text/javascript').html(source);
28de7771
DW
574 $('head').append(newscript);
575 }
576 };
577
578 /**
579 * Do some DOM replacement and trigger correct events and fire javascript.
580 *
581 * @method domReplace
582 * @private
583 * @param {JQuery} element - Element or selector to replace.
584 * @param {String} newHTML - HTML to insert / replace.
585 * @param {String} newJS - Javascript to run after the insertion.
586 * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
587 */
588 var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
589 var replaceNode = $(element);
590 if (replaceNode.length) {
591 // First create the dom nodes so we have a reference to them.
592 var newNodes = $(newHTML);
1fca8a7b 593 var yuiNodes = null;
28de7771
DW
594 // Do the replacement in the page.
595 if (replaceChildNodes) {
1fca8a7b
DW
596 // Cleanup any YUI event listeners attached to any of these nodes.
597 yuiNodes = new Y.NodeList(replaceNode.children().get());
598 yuiNodes.destroy(true);
599
600 // JQuery will cleanup after itself.
28de7771
DW
601 replaceNode.empty();
602 replaceNode.append(newNodes);
603 } else {
1fca8a7b
DW
604 // Cleanup any YUI event listeners attached to any of these nodes.
605 yuiNodes = new Y.NodeList(replaceNode.get());
606 yuiNodes.destroy(true);
607
608 // JQuery will cleanup after itself.
28de7771
DW
609 replaceNode.replaceWith(newNodes);
610 }
611 // Run any javascript associated with the new HTML.
612 runTemplateJS(newJS);
613 // Notify all filters about the new content.
614 event.notifyFilterContentUpdated(newNodes);
615 }
616 };
617
39bf2a98
DW
618 /**
619 * Scan a template source for partial tags and return a list of the found partials.
620 *
621 * @method scanForPartials
622 * @private
623 * @param {string} templateSource - source template to scan.
624 * @return {Array} List of partials.
625 */
626 Renderer.prototype.scanForPartials = function(templateSource) {
627 var tokens = mustache.parse(templateSource),
628 partials = [];
629
630 var findPartial = function(tokens, partials) {
631 var i, token;
632 for (i = 0; i < tokens.length; i++) {
633 token = tokens[i];
634 if (token[0] == '>' || token[0] == '<') {
635 partials.push(token[1]);
636 }
637 if (token.length > 4) {
638 findPartial(token[4], partials);
639 }
640 }
641 };
642
643 findPartial(tokens, partials);
644
645 return partials;
646 };
647
648 /**
649 * Load a template and scan it for partials. Recursively fetch the partials.
650 *
651 * @method cachePartials
652 * @private
653 * @param {string} templateName - should consist of the component and the name of the template like this:
654 * core/menu (lib/templates/menu.mustache) or
655 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
656 * @return {Promise} JQuery promise object resolved when all partials are in the cache.
657 */
658 Renderer.prototype.cachePartials = function(templateName) {
0015f5b8
RW
659 var searchKey = this.currentThemeName + '/' + templateName;
660
661 if (searchKey in cachePartialPromises) {
662 return cachePartialPromises[searchKey];
663 }
664
61e0f58c
AN
665 // This promise will not be resolved until all child partials are also resolved and ready.
666 // We create it here to allow us to check for recursive inclusion of templates.
667 cachePartialPromises[searchKey] = $.Deferred();
668
669 this.getTemplate(templateName)
670 .then(function(templateSource) {
39bf2a98 671 var partials = this.scanForPartials(templateSource);
b51c5b92 672 var uniquePartials = partials.filter(function(partialName) {
61e0f58c
AN
673 // Check for recursion.
674
675 if (typeof cachePartialPromises[this.currentThemeName + '/' + partialName] !== 'undefined') {
676 // Ignore templates which include their parent.
677 return false;
678 }
679
680 // Ignore templates that include themselves.
b51c5b92 681 return partialName != templateName;
61e0f58c
AN
682 }.bind(this));
683
684 // Fetch any partial which has not already been fetched.
b51c5b92 685 var fetchThemAll = uniquePartials.map(function(partialName) {
0015f5b8
RW
686 return this.cachePartials(partialName);
687 }.bind(this));
39bf2a98 688
61e0f58c
AN
689 // Resolve the templateName promise when all of the children are resolved.
690 return $.when.apply($, fetchThemAll)
691 .then(function() {
692 return cachePartialPromises[searchKey].resolve(templateSource);
b302369d 693 });
61e0f58c
AN
694 }.bind(this))
695 .catch(cachePartialPromises[searchKey].reject);
0015f5b8 696
61e0f58c 697 return cachePartialPromises[searchKey];
39bf2a98
DW
698 };
699
8d00afb1
DW
700 /**
701 * Load a template and call doRender on it.
702 *
703 * @method render
704 * @private
705 * @param {string} templateName - should consist of the component and the name of the template like this:
706 * core/menu (lib/templates/menu.mustache) or
707 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
708 * @param {Object} context - Could be array, string or simple value for the context of the template.
709 * @param {string} themeName - Name of the current theme.
710 * @return {Promise} JQuery promise object resolved when the template has been rendered.
711 */
712 Renderer.prototype.render = function(templateName, context, themeName) {
8d00afb1
DW
713 if (typeof (themeName) === "undefined") {
714 // System context by default.
715 themeName = config.theme;
716 }
717
718 this.currentThemeName = themeName;
719
95b06c13 720 // Preload the module to do the icon rendering based on the theme iconsystem.
e330b1c2 721 var modulename = config.iconsystemmodule;
95b06c13
DW
722
723 var ready = $.Deferred();
724 require([modulename], function(System) {
725 var system = new System();
726 if (!(system instanceof IconSystem)) {
727 ready.reject('Invalid icon system specified' + config.iconsystem);
728 } else {
729 iconSystem = system;
751ec025 730 system.init().then(ready.resolve).catch(notification.exception);
95b06c13
DW
731 }
732 });
733
734 return ready.then(function() {
735 return this.cachePartials(templateName);
736 }.bind(this)).then(function(templateSource) {
737 return this.doRender(templateSource, context, themeName);
738 }.bind(this));
8d00afb1
DW
739 };
740
90525930
MN
741 /**
742 * Prepend some HTML to a node and trigger events and fire javascript.
743 *
744 * @method domPrepend
745 * @private
746 * @param {jQuery|String} element - Element or selector to prepend HTML to
747 * @param {String} html - HTML to prepend
748 * @param {String} js - Javascript to run after we prepend the html
749 */
750 var domPrepend = function(element, html, js) {
751 var node = $(element);
752 if (node.length) {
753 // Prepend the html.
754 node.prepend(html);
755 // Run any javascript associated with the new HTML.
756 runTemplateJS(js);
757 // Notify all filters about the new content.
758 event.notifyFilterContentUpdated(node);
759 }
760 };
28de7771 761
f7775c9a
MN
762 /**
763 * Append some HTML to a node and trigger events and fire javascript.
764 *
765 * @method domAppend
766 * @private
767 * @param {jQuery|String} element - Element or selector to append HTML to
768 * @param {String} html - HTML to append
769 * @param {String} js - Javascript to run after we append the html
770 */
771 var domAppend = function(element, html, js) {
772 var node = $(element);
773 if (node.length) {
774 // Append the html.
775 node.append(html);
776 // Run any javascript associated with the new HTML.
777 runTemplateJS(js);
778 // Notify all filters about the new content.
779 event.notifyFilterContentUpdated(node);
780 }
781 };
782
9bdcf579
DW
783 return /** @alias module:core/templates */ {
784 // Public variables and functions.
785 /**
8d00afb1
DW
786 * Every call to render creates a new instance of the class and calls render on it. This
787 * means each render call has it's own class variables.
9bdcf579
DW
788 *
789 * @method render
790 * @private
791 * @param {string} templateName - should consist of the component and the name of the template like this:
792 * core/menu (lib/templates/menu.mustache) or
793 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
794 * @param {Object} context - Could be array, string or simple value for the context of the template.
795 * @param {string} themeName - Name of the current theme.
796 * @return {Promise} JQuery promise object resolved when the template has been rendered.
797 */
798 render: function(templateName, context, themeName) {
8d00afb1
DW
799 var renderer = new Renderer();
800 return renderer.render(templateName, context, themeName);
9bdcf579
DW
801 },
802
95b06c13
DW
803 /**
804 * Every call to renderIcon creates a new instance of the class and calls renderIcon on it. This
805 * means each render call has it's own class variables.
806 *
807 * @method renderIcon
808 * @public
809 * @param {string} key - Icon key.
810 * @param {string} component - Icon component
811 * @param {string} title - Icon title
812 * @return {Promise} JQuery promise object resolved when the pix has been rendered.
813 */
814 renderPix: function(key, component, title) {
815 var renderer = new Renderer();
816 return renderer.renderIcon(key, component, title);
817 },
818
9bdcf579
DW
819 /**
820 * Execute a block of JS returned from a template.
821 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
822 *
823 * @method runTemplateJS
9bdcf579
DW
824 * @param {string} source - A block of javascript.
825 */
28de7771
DW
826 runTemplateJS: runTemplateJS,
827
828 /**
829 * Replace a node in the page with some new HTML and run the JS.
830 *
831 * @method replaceNodeContents
c96f55e6
DP
832 * @param {JQuery} element - Element or selector to replace.
833 * @param {String} newHTML - HTML to insert / replace.
834 * @param {String} newJS - Javascript to run after the insertion.
28de7771
DW
835 */
836 replaceNodeContents: function(element, newHTML, newJS) {
c96f55e6 837 domReplace(element, newHTML, newJS, true);
28de7771
DW
838 },
839
840 /**
841 * Insert a node in the page with some new HTML and run the JS.
842 *
843 * @method replaceNode
c96f55e6
DP
844 * @param {JQuery} element - Element or selector to replace.
845 * @param {String} newHTML - HTML to insert / replace.
846 * @param {String} newJS - Javascript to run after the insertion.
28de7771
DW
847 */
848 replaceNode: function(element, newHTML, newJS) {
c96f55e6 849 domReplace(element, newHTML, newJS, false);
f7775c9a
MN
850 },
851
90525930
MN
852 /**
853 * Prepend some HTML to a node and trigger events and fire javascript.
854 *
855 * @method prependNodeContents
856 * @param {jQuery|String} element - Element or selector to prepend HTML to
857 * @param {String} html - HTML to prepend
858 * @param {String} js - Javascript to run after we prepend the html
859 */
860 prependNodeContents: function(element, html, js) {
861 domPrepend(element, html, js);
862 },
863
f7775c9a
MN
864 /**
865 * Append some HTML to a node and trigger events and fire javascript.
866 *
867 * @method appendNodeContents
868 * @param {jQuery|String} element - Element or selector to append HTML to
869 * @param {String} html - HTML to append
870 * @param {String} js - Javascript to run after we append the html
871 */
872 appendNodeContents: function(element, html, js) {
873 domAppend(element, html, js);
9bdcf579
DW
874 }
875 };
876});