MDL-53667 tool_lp: Use event delegate for competency dialogue
[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 */
26define([ 'core/mustache',
27 'jquery',
28 'core/ajax',
29 'core/str',
30 'core/notification',
31 'core/url',
4b9e5326 32 'core/config',
28de7771 33 'core/localstorage',
1fca8a7b
DW
34 'core/event',
35 'core/yui'
9bdcf579 36 ],
1fca8a7b 37 function(mustache, $, ajax, str, notification, coreurl, config, storage, event, Y) {
9bdcf579
DW
38
39 // Private variables and functions.
40
41 /** @var {string[]} templateCache - Cache of already loaded templates */
42 var templateCache = {};
43
44 /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
45 var requiredStrings = [];
46
47 /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
48 var requiredJS = [];
49
50 /** @var {Number} uniqid Incrementing value that is changed for every call to render */
51 var uniqid = 1;
52
53 /** @var {String} themeName for the current render */
54 var currentThemeName = '';
55
f992dcf6
DP
56 /**
57 * Load a template from the cache or local storage or ajax request.
58 *
59 * @method getTemplate
60 * @private
61 * @param {string} templateName - should consist of the component and the name of the template like this:
62 * core/menu (lib/templates/menu.mustache) or
63 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
64 * @return {Promise} JQuery promise object resolved when the template has been fetched.
65 */
66 var getTemplate = function(templateName, async) {
67 var deferred = $.Deferred();
68 var parts = templateName.split('/');
69 var component = parts.shift();
70 var name = parts.shift();
71
72 var searchKey = currentThemeName + '/' + templateName;
73
74 // First try request variables.
75 if (searchKey in templateCache) {
76 deferred.resolve(templateCache[searchKey]);
77 return deferred.promise();
78 }
79
80 // Now try local storage.
81 var cached = storage.get('core_template/' + searchKey);
82
83 if (cached) {
84 deferred.resolve(cached);
85 templateCache[searchKey] = cached;
86 return deferred.promise();
87 }
88
89 // Oh well - load via ajax.
90 var promises = ajax.call([{
91 methodname: 'core_output_load_template',
92 args:{
93 component: component,
94 template: name,
95 themename: currentThemeName
96 }
97 }], async, false);
98
99 promises[0].done(
100 function (templateSource) {
101 storage.set('core_template/' + searchKey, templateSource);
102 templateCache[searchKey] = templateSource;
103 deferred.resolve(templateSource);
104 }
105 ).fail(
106 function (ex) {
107 deferred.reject(ex);
108 }
109 );
110 return deferred.promise();
111 };
112
113 /**
114 * Load a partial from the cache or ajax.
115 *
116 * @method partialHelper
117 * @private
118 * @param {string} name The partial name to load.
119 * @return {string}
120 */
121 var partialHelper = function(name) {
122 var template = '';
123
124 getTemplate(name, false).done(
125 function(source) {
126 template = source;
127 }
128 ).fail(notification.exception);
129
130 return template;
131 };
132
9bdcf579
DW
133 /**
134 * Render image icons.
135 *
136 * @method pixHelper
137 * @private
138 * @param {string} sectionText The text to parse arguments from.
e37d53da 139 * @param {function} helper Used to render the alt attribute of the text.
9bdcf579
DW
140 * @return {string}
141 */
e37d53da 142 var pixHelper = function(sectionText, helper) {
9bdcf579
DW
143 var parts = sectionText.split(',');
144 var key = '';
145 var component = '';
146 var text = '';
147 var result;
148
149 if (parts.length > 0) {
150 key = parts.shift().trim();
151 }
152 if (parts.length > 0) {
153 component = parts.shift().trim();
154 }
155 if (parts.length > 0) {
156 text = parts.join(',').trim();
157 }
158 var url = coreurl.imageUrl(key, component);
159
160 var templatecontext = {
161 attributes: [
162 { name: 'src', value: url},
e37d53da 163 { name: 'alt', value: helper(text)},
9bdcf579
DW
164 { name: 'class', value: 'smallicon'}
165 ]
166 };
167 // We forced loading of this early, so it will be in the cache.
168 var template = templateCache[currentThemeName + '/core/pix_icon'];
169 result = mustache.render(template, templatecontext, partialHelper);
170 return result.trim();
171 };
172
9bdcf579
DW
173 /**
174 * Render blocks of javascript and save them in an array.
175 *
176 * @method jsHelper
177 * @private
178 * @param {string} sectionText The text to save as a js block.
179 * @param {function} helper Used to render the block.
180 * @return {string}
181 */
182 var jsHelper = function(sectionText, helper) {
183 requiredJS.push(helper(sectionText, this));
184 return '';
185 };
186
187 /**
188 * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
189 * into a get_string call.
190 *
191 * @method stringHelper
192 * @private
193 * @param {string} sectionText The text to parse the arguments from.
194 * @param {function} helper Used to render subsections of the text.
195 * @return {string}
196 */
197 var stringHelper = function(sectionText, helper) {
198 var parts = sectionText.split(',');
199 var key = '';
200 var component = '';
201 var param = '';
202 if (parts.length > 0) {
203 key = parts.shift().trim();
204 }
205 if (parts.length > 0) {
206 component = parts.shift().trim();
207 }
208 if (parts.length > 0) {
209 param = parts.join(',').trim();
210 }
211
212 if (param !== '') {
213 // Allow variable expansion in the param part only.
214 param = helper(param, this);
215 }
216 // Allow json formatted $a arguments.
217 if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
218 param = JSON.parse(param);
219 }
220
221 var index = requiredStrings.length;
222 requiredStrings.push({key: key, component: component, param: param});
223 return '{{_s' + index + '}}';
224 };
225
0b4bff8c
AN
226 /**
227 * Quote helper used to wrap content in quotes, and escape all quotes present in the content.
228 *
229 * @method quoteHelper
230 * @private
231 * @param {string} sectionText The text to parse the arguments from.
232 * @param {function} helper Used to render subsections of the text.
233 * @return {string}
234 */
235 var quoteHelper = function(sectionText, helper) {
236 var content = helper(sectionText.trim(), this);
237
238 // Escape the {{ and the ".
239 // This involves wrapping {{, and }} in change delimeter tags.
240 content = content
241 .replace('"', '\\"')
242 .replace(/([\{\}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>')
243 ;
244 return '"' + content + '"';
245 };
246
9bdcf579
DW
247 /**
248 * Add some common helper functions to all context objects passed to templates.
249 * These helpers match exactly the helpers available in php.
250 *
251 * @method addHelpers
252 * @private
253 * @param {Object} context Simple types used as the context for the template.
254 * @param {String} themeName We set this multiple times, because there are async calls.
255 */
256 var addHelpers = function(context, themeName) {
257 currentThemeName = themeName;
258 requiredStrings = [];
259 requiredJS = [];
260 context.uniqid = uniqid++;
261 context.str = function() { return stringHelper; };
262 context.pix = function() { return pixHelper; };
263 context.js = function() { return jsHelper; };
0b4bff8c 264 context.quote = function() { return quoteHelper; };
9bdcf579
DW
265 context.globals = { config : config };
266 context.currentTheme = themeName;
267 };
268
269 /**
270 * Get all the JS blocks from the last rendered template.
271 *
272 * @method getJS
273 * @private
274 * @param {string[]} strings Replacement strings.
275 * @return {string}
276 */
277 var getJS = function(strings) {
278 var js = '';
279 if (requiredJS.length > 0) {
280 js = requiredJS.join(";\n");
281 }
282
283 var i = 0;
284
285 for (i = 0; i < strings.length; i++) {
286 js = js.replace('{{_s' + i + '}}', strings[i]);
287 }
288 // Re-render to get the final strings.
289 return js;
290 };
291
292 /**
293 * Render a template and then call the callback with the result.
294 *
295 * @method doRender
296 * @private
297 * @param {string} templateSource The mustache template to render.
298 * @param {Object} context Simple types used as the context for the template.
299 * @param {String} themeName Name of the current theme.
300 * @return {Promise} object
301 */
302 var doRender = function(templateSource, context, themeName) {
303 var deferred = $.Deferred();
304
305 currentThemeName = themeName;
306
307 // Make sure we fetch this first.
308 var loadPixTemplate = getTemplate('core/pix_icon', true);
309
310 loadPixTemplate.done(
311 function() {
312 addHelpers(context, themeName);
313 var result = '';
314 try {
315 result = mustache.render(templateSource, context, partialHelper);
316 } catch (ex) {
317 deferred.reject(ex);
318 }
319
320 if (requiredStrings.length > 0) {
321 str.get_strings(requiredStrings).done(
322 function(strings) {
323 var i;
324
325 // Why do we not do another call the render here?
326 //
327 // Because that would expose DOS holes. E.g.
328 // I create an assignment called "{{fish" which
329 // would get inserted in the template in the first pass
330 // and cause the template to die on the second pass (unbalanced).
331 for (i = 0; i < strings.length; i++) {
332 result = result.replace('{{_s' + i + '}}', strings[i]);
333 }
334 deferred.resolve(result.trim(), getJS(strings));
335 }
336 ).fail(
337 function(ex) {
338 deferred.reject(ex);
339 }
340 );
341 } else {
342 deferred.resolve(result.trim(), getJS([]));
343 }
344 }
345 ).fail(
346 function(ex) {
347 deferred.reject(ex);
348 }
349 );
350 return deferred.promise();
351 };
352
28de7771
DW
353 /**
354 * Execute a block of JS returned from a template.
355 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
356 *
357 * @method runTemplateJS
358 * @param {string} source - A block of javascript.
359 */
360 var runTemplateJS = function(source) {
361 if (source.trim() !== '') {
362 var newscript = $('<script>').attr('type','text/javascript').html(source);
363 $('head').append(newscript);
364 }
365 };
366
367 /**
368 * Do some DOM replacement and trigger correct events and fire javascript.
369 *
370 * @method domReplace
371 * @private
372 * @param {JQuery} element - Element or selector to replace.
373 * @param {String} newHTML - HTML to insert / replace.
374 * @param {String} newJS - Javascript to run after the insertion.
375 * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node.
376 */
377 var domReplace = function(element, newHTML, newJS, replaceChildNodes) {
378 var replaceNode = $(element);
379 if (replaceNode.length) {
380 // First create the dom nodes so we have a reference to them.
381 var newNodes = $(newHTML);
1fca8a7b 382 var yuiNodes = null;
28de7771
DW
383 // Do the replacement in the page.
384 if (replaceChildNodes) {
1fca8a7b
DW
385 // Cleanup any YUI event listeners attached to any of these nodes.
386 yuiNodes = new Y.NodeList(replaceNode.children().get());
387 yuiNodes.destroy(true);
388
389 // JQuery will cleanup after itself.
28de7771
DW
390 replaceNode.empty();
391 replaceNode.append(newNodes);
392 } else {
1fca8a7b
DW
393 // Cleanup any YUI event listeners attached to any of these nodes.
394 yuiNodes = new Y.NodeList(replaceNode.get());
395 yuiNodes.destroy(true);
396
397 // JQuery will cleanup after itself.
28de7771
DW
398 replaceNode.replaceWith(newNodes);
399 }
400 // Run any javascript associated with the new HTML.
401 runTemplateJS(newJS);
402 // Notify all filters about the new content.
403 event.notifyFilterContentUpdated(newNodes);
404 }
405 };
406
407
9bdcf579
DW
408 return /** @alias module:core/templates */ {
409 // Public variables and functions.
410 /**
411 * Load a template and call doRender on it.
412 *
413 * @method render
414 * @private
415 * @param {string} templateName - should consist of the component and the name of the template like this:
416 * core/menu (lib/templates/menu.mustache) or
417 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
418 * @param {Object} context - Could be array, string or simple value for the context of the template.
419 * @param {string} themeName - Name of the current theme.
420 * @return {Promise} JQuery promise object resolved when the template has been rendered.
421 */
422 render: function(templateName, context, themeName) {
423 var deferred = $.Deferred();
424
425 if (typeof (themeName) === "undefined") {
426 // System context by default.
427 themeName = config.theme;
428 }
429
430 currentThemeName = themeName;
431
432 var loadTemplate = getTemplate(templateName, true);
433
434 loadTemplate.done(
435 function(templateSource) {
436 var renderPromise = doRender(templateSource, context, themeName);
437
438 renderPromise.done(
439 function(result, js) {
440 deferred.resolve(result, js);
441 }
442 ).fail(
443 function(ex) {
444 deferred.reject(ex);
445 }
446 );
447 }
448 ).fail(
449 function(ex) {
450 deferred.reject(ex);
451 }
452 );
453 return deferred.promise();
454 },
455
456 /**
457 * Execute a block of JS returned from a template.
458 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
459 *
460 * @method runTemplateJS
9bdcf579
DW
461 * @param {string} source - A block of javascript.
462 */
28de7771
DW
463 runTemplateJS: runTemplateJS,
464
465 /**
466 * Replace a node in the page with some new HTML and run the JS.
467 *
468 * @method replaceNodeContents
469 * @param {string} source - A block of javascript.
470 */
471 replaceNodeContents: function(element, newHTML, newJS) {
472 return domReplace(element, newHTML, newJS, true);
473 },
474
475 /**
476 * Insert a node in the page with some new HTML and run the JS.
477 *
478 * @method replaceNode
479 * @param {string} source - A block of javascript.
480 */
481 replaceNode: function(element, newHTML, newJS) {
482 return domReplace(element, newHTML, newJS, false);
9bdcf579
DW
483 }
484 };
485});