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