MDL-50783 Ajax: Configure how to call a webservice through db/service.php
[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
DW
32 'core/config',
33 'core/localstorage'
9bdcf579 34 ],
4b9e5326 35 function(mustache, $, ajax, str, notification, coreurl, config, storage) {
9bdcf579
DW
36
37 // Private variables and functions.
38
39 /** @var {string[]} templateCache - Cache of already loaded templates */
40 var templateCache = {};
41
42 /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
43 var requiredStrings = [];
44
45 /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
46 var requiredJS = [];
47
48 /** @var {Number} uniqid Incrementing value that is changed for every call to render */
49 var uniqid = 1;
50
51 /** @var {String} themeName for the current render */
52 var currentThemeName = '';
53
54 /**
55 * Render image icons.
56 *
57 * @method pixHelper
58 * @private
59 * @param {string} sectionText The text to parse arguments from.
e37d53da 60 * @param {function} helper Used to render the alt attribute of the text.
9bdcf579
DW
61 * @return {string}
62 */
e37d53da 63 var pixHelper = function(sectionText, helper) {
9bdcf579
DW
64 var parts = sectionText.split(',');
65 var key = '';
66 var component = '';
67 var text = '';
68 var result;
69
70 if (parts.length > 0) {
71 key = parts.shift().trim();
72 }
73 if (parts.length > 0) {
74 component = parts.shift().trim();
75 }
76 if (parts.length > 0) {
77 text = parts.join(',').trim();
78 }
79 var url = coreurl.imageUrl(key, component);
80
81 var templatecontext = {
82 attributes: [
83 { name: 'src', value: url},
e37d53da 84 { name: 'alt', value: helper(text)},
9bdcf579
DW
85 { name: 'class', value: 'smallicon'}
86 ]
87 };
88 // We forced loading of this early, so it will be in the cache.
89 var template = templateCache[currentThemeName + '/core/pix_icon'];
90 result = mustache.render(template, templatecontext, partialHelper);
91 return result.trim();
92 };
93
94 /**
95 * Load a partial from the cache or ajax.
96 *
97 * @method partialHelper
98 * @private
99 * @param {string} name The partial name to load.
100 * @return {string}
101 */
102 var partialHelper = function(name) {
103 var template = '';
104
105 getTemplate(name, false).done(
106 function(source) {
107 template = source;
108 }
109 ).fail(notification.exception);
110
111 return template;
112 };
113
114 /**
115 * Render blocks of javascript and save them in an array.
116 *
117 * @method jsHelper
118 * @private
119 * @param {string} sectionText The text to save as a js block.
120 * @param {function} helper Used to render the block.
121 * @return {string}
122 */
123 var jsHelper = function(sectionText, helper) {
124 requiredJS.push(helper(sectionText, this));
125 return '';
126 };
127
128 /**
129 * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}}
130 * into a get_string call.
131 *
132 * @method stringHelper
133 * @private
134 * @param {string} sectionText The text to parse the arguments from.
135 * @param {function} helper Used to render subsections of the text.
136 * @return {string}
137 */
138 var stringHelper = function(sectionText, helper) {
139 var parts = sectionText.split(',');
140 var key = '';
141 var component = '';
142 var param = '';
143 if (parts.length > 0) {
144 key = parts.shift().trim();
145 }
146 if (parts.length > 0) {
147 component = parts.shift().trim();
148 }
149 if (parts.length > 0) {
150 param = parts.join(',').trim();
151 }
152
153 if (param !== '') {
154 // Allow variable expansion in the param part only.
155 param = helper(param, this);
156 }
157 // Allow json formatted $a arguments.
158 if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) {
159 param = JSON.parse(param);
160 }
161
162 var index = requiredStrings.length;
163 requiredStrings.push({key: key, component: component, param: param});
164 return '{{_s' + index + '}}';
165 };
166
167 /**
168 * Add some common helper functions to all context objects passed to templates.
169 * These helpers match exactly the helpers available in php.
170 *
171 * @method addHelpers
172 * @private
173 * @param {Object} context Simple types used as the context for the template.
174 * @param {String} themeName We set this multiple times, because there are async calls.
175 */
176 var addHelpers = function(context, themeName) {
177 currentThemeName = themeName;
178 requiredStrings = [];
179 requiredJS = [];
180 context.uniqid = uniqid++;
181 context.str = function() { return stringHelper; };
182 context.pix = function() { return pixHelper; };
183 context.js = function() { return jsHelper; };
184 context.globals = { config : config };
185 context.currentTheme = themeName;
186 };
187
188 /**
189 * Get all the JS blocks from the last rendered template.
190 *
191 * @method getJS
192 * @private
193 * @param {string[]} strings Replacement strings.
194 * @return {string}
195 */
196 var getJS = function(strings) {
197 var js = '';
198 if (requiredJS.length > 0) {
199 js = requiredJS.join(";\n");
200 }
201
202 var i = 0;
203
204 for (i = 0; i < strings.length; i++) {
205 js = js.replace('{{_s' + i + '}}', strings[i]);
206 }
207 // Re-render to get the final strings.
208 return js;
209 };
210
211 /**
212 * Render a template and then call the callback with the result.
213 *
214 * @method doRender
215 * @private
216 * @param {string} templateSource The mustache template to render.
217 * @param {Object} context Simple types used as the context for the template.
218 * @param {String} themeName Name of the current theme.
219 * @return {Promise} object
220 */
221 var doRender = function(templateSource, context, themeName) {
222 var deferred = $.Deferred();
223
224 currentThemeName = themeName;
225
226 // Make sure we fetch this first.
227 var loadPixTemplate = getTemplate('core/pix_icon', true);
228
229 loadPixTemplate.done(
230 function() {
231 addHelpers(context, themeName);
232 var result = '';
233 try {
234 result = mustache.render(templateSource, context, partialHelper);
235 } catch (ex) {
236 deferred.reject(ex);
237 }
238
239 if (requiredStrings.length > 0) {
240 str.get_strings(requiredStrings).done(
241 function(strings) {
242 var i;
243
244 // Why do we not do another call the render here?
245 //
246 // Because that would expose DOS holes. E.g.
247 // I create an assignment called "{{fish" which
248 // would get inserted in the template in the first pass
249 // and cause the template to die on the second pass (unbalanced).
250 for (i = 0; i < strings.length; i++) {
251 result = result.replace('{{_s' + i + '}}', strings[i]);
252 }
253 deferred.resolve(result.trim(), getJS(strings));
254 }
255 ).fail(
256 function(ex) {
257 deferred.reject(ex);
258 }
259 );
260 } else {
261 deferred.resolve(result.trim(), getJS([]));
262 }
263 }
264 ).fail(
265 function(ex) {
266 deferred.reject(ex);
267 }
268 );
269 return deferred.promise();
270 };
271
272 /**
4b9e5326 273 * Load a template from the cache or local storage or ajax request.
9bdcf579
DW
274 *
275 * @method getTemplate
276 * @private
277 * @param {string} templateName - should consist of the component and the name of the template like this:
278 * core/menu (lib/templates/menu.mustache) or
279 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
280 * @return {Promise} JQuery promise object resolved when the template has been fetched.
281 */
282 var getTemplate = function(templateName, async) {
283 var deferred = $.Deferred();
284 var parts = templateName.split('/');
285 var component = parts.shift();
286 var name = parts.shift();
287
288 var searchKey = currentThemeName + '/' + templateName;
289
4b9e5326 290 // First try request variables.
9bdcf579
DW
291 if (searchKey in templateCache) {
292 deferred.resolve(templateCache[searchKey]);
4b9e5326 293 return deferred.promise();
9bdcf579 294 }
4b9e5326
DW
295
296 // Now try local storage.
297 var cached = storage.get('core_template/' + searchKey);
298
299 if (cached) {
300 deferred.resolve(cached);
e0d08bfa 301 templateCache[searchKey] = cached;
4b9e5326
DW
302 return deferred.promise();
303 }
304
305 // Oh well - load via ajax.
306 var promises = ajax.call([{
307 methodname: 'core_output_load_template',
308 args:{
309 component: component,
310 template: name,
311 themename: currentThemeName
312 }
ba224fb4 313 }], async, false);
4b9e5326
DW
314
315 promises[0].done(
316 function (templateSource) {
317 storage.set('core_template/' + searchKey, templateSource);
318 templateCache[searchKey] = templateSource;
319 deferred.resolve(templateSource);
320 }
321 ).fail(
322 function (ex) {
323 deferred.reject(ex);
324 }
325 );
9bdcf579
DW
326 return deferred.promise();
327 };
328
329 return /** @alias module:core/templates */ {
330 // Public variables and functions.
331 /**
332 * Load a template and call doRender on it.
333 *
334 * @method render
335 * @private
336 * @param {string} templateName - should consist of the component and the name of the template like this:
337 * core/menu (lib/templates/menu.mustache) or
338 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
339 * @param {Object} context - Could be array, string or simple value for the context of the template.
340 * @param {string} themeName - Name of the current theme.
341 * @return {Promise} JQuery promise object resolved when the template has been rendered.
342 */
343 render: function(templateName, context, themeName) {
344 var deferred = $.Deferred();
345
346 if (typeof (themeName) === "undefined") {
347 // System context by default.
348 themeName = config.theme;
349 }
350
351 currentThemeName = themeName;
352
353 var loadTemplate = getTemplate(templateName, true);
354
355 loadTemplate.done(
356 function(templateSource) {
357 var renderPromise = doRender(templateSource, context, themeName);
358
359 renderPromise.done(
360 function(result, js) {
361 deferred.resolve(result, js);
362 }
363 ).fail(
364 function(ex) {
365 deferred.reject(ex);
366 }
367 );
368 }
369 ).fail(
370 function(ex) {
371 deferred.reject(ex);
372 }
373 );
374 return deferred.promise();
375 },
376
377 /**
378 * Execute a block of JS returned from a template.
379 * Call this AFTER adding the template HTML into the DOM so the nodes can be found.
380 *
381 * @method runTemplateJS
382 * @private
383 * @param {string} source - A block of javascript.
384 */
385 runTemplateJS: function(source) {
386 var newscript = $('<script>').attr('type','text/javascript').html(source);
387 $('head').append(newscript);
388 }
389 };
390});