MDL-50783 Ajax: Configure how to call a webservice through db/service.php
[moodle.git] / lib / amd / src / templates.js
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/>.
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  */
26 define([ 'core/mustache',
27          'jquery',
28          'core/ajax',
29          'core/str',
30          'core/notification',
31          'core/url',
32          'core/config',
33          'core/localstorage'
34        ],
35        function(mustache, $, ajax, str, notification, coreurl, config, storage) {
37     // Private variables and functions.
39     /** @var {string[]} templateCache - Cache of already loaded templates */
40     var templateCache = {};
42     /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */
43     var requiredStrings = [];
45     /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */
46     var requiredJS = [];
48     /** @var {Number} uniqid Incrementing value that is changed for every call to render */
49     var uniqid = 1;
51     /** @var {String} themeName for the current render */
52     var currentThemeName = '';
54     /**
55      * Render image icons.
56      *
57      * @method pixHelper
58      * @private
59      * @param {string} sectionText The text to parse arguments from.
60      * @param {function} helper Used to render the alt attribute of the text.
61      * @return {string}
62      */
63     var pixHelper = function(sectionText, helper) {
64         var parts = sectionText.split(',');
65         var key = '';
66         var component = '';
67         var text = '';
68         var result;
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);
81         var templatecontext = {
82             attributes: [
83                 { name: 'src', value: url},
84                 { name: 'alt', value: helper(text)},
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     };
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 = '';
105         getTemplate(name, false).done(
106             function(source) {
107                 template = source;
108             }
109         ).fail(notification.exception);
111         return template;
112     };
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     };
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         }
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         }
162         var index = requiredStrings.length;
163         requiredStrings.push({key: key, component: component, param: param});
164         return '{{_s' + index + '}}';
165     };
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     };
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         }
202         var i = 0;
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     };
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();
224         currentThemeName = themeName;
226         // Make sure we fetch this first.
227         var loadPixTemplate = getTemplate('core/pix_icon', true);
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                 }
239                 if (requiredStrings.length > 0) {
240                     str.get_strings(requiredStrings).done(
241                         function(strings) {
242                             var i;
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     };
272     /**
273      * Load a template from the cache or local storage or ajax request.
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();
288         var searchKey = currentThemeName + '/' + templateName;
290         // First try request variables.
291         if (searchKey in templateCache) {
292             deferred.resolve(templateCache[searchKey]);
293             return deferred.promise();
294         }
296         // Now try local storage.
297         var cached = storage.get('core_template/' + searchKey);
299         if (cached) {
300             deferred.resolve(cached);
301             templateCache[searchKey] = cached;
302             return deferred.promise();
303         }
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             }
313         }], async, false);
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         );
326         return deferred.promise();
327     };
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();
346             if (typeof (themeName) === "undefined") {
347                 // System context by default.
348                 themeName = config.theme;
349             }
351             currentThemeName = themeName;
353             var loadTemplate = getTemplate(templateName, true);
355             loadTemplate.done(
356                 function(templateSource) {
357                     var renderPromise = doRender(templateSource, context, themeName);
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         },
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 });