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