[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 */ 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 'core/event', 35 'core/yui', 36 'core/log' 37 ], 38 function(mustache, $, ajax, str, notification, coreurl, config, storage, event, Y, Log) { 39 40 // Module variables. 41 /** @var {Number} uniqInstances Count of times this constructor has been called. */ 42 var uniqInstances = 0; 43 44 /** @var {string[]} templateCache - Cache of already loaded templates */ 45 var templateCache = {}; 46 47 /** 48 * Constructor 49 * 50 * Each call to templates.render gets it's own instance of this class. 51 */ 52 var Renderer = function() { 53 this.requiredStrings = []; 54 this.requiredJS = []; 55 this.currentThemeName = ''; 56 }; 57 // Class variables and functions. 58 59 /** @var {string[]} requiredStrings - Collection of strings found during the rendering of one template */ 60 Renderer.prototype.requiredStrings = null; 61 62 /** @var {string[]} requiredJS - Collection of js blocks found during the rendering of one template */ 63 Renderer.prototype.requiredJS = null; 64 65 /** @var {String} themeName for the current render */ 66 Renderer.prototype.currentThemeName = ''; 67 68 /** 69 * Load a template from the cache or local storage or ajax request. 70 * 71 * @method getTemplate 72 * @private 73 * @param {string} templateName - should consist of the component and the name of the template like this: 74 * core/menu (lib/templates/menu.mustache) or 75 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache) 76 * @param {Boolean} async If false - this function will not return until the promises are resolved. 77 * @return {Promise} JQuery promise object resolved when the template has been fetched. 78 */ 79 Renderer.prototype.getTemplate = function(templateName, async) { 80 var deferred = $.Deferred(); 81 var parts = templateName.split('/'); 82 var component = parts.shift(); 83 var name = parts.shift(); 84 85 var searchKey = this.currentThemeName + '/' + templateName; 86 87 // First try request variables. 88 if (searchKey in templateCache) { 89 deferred.resolve(templateCache[searchKey]); 90 return deferred.promise(); 91 } 92 93 // Now try local storage. 94 var cached = storage.get('core_template/' + searchKey); 95 96 if (cached) { 97 deferred.resolve(cached); 98 templateCache[searchKey] = cached; 99 return deferred.promise(); 100 } 101 102 // Oh well - load via ajax. 103 var promises = ajax.call([{ 104 methodname: 'core_output_load_template', 105 args: { 106 component: component, 107 template: name, 108 themename: this.currentThemeName 109 } 110 }], async, false); 111 112 promises[0].done( 113 function(templateSource) { 114 storage.set('core_template/' + searchKey, templateSource); 115 templateCache[searchKey] = templateSource; 116 deferred.resolve(templateSource); 117 } 118 ).fail( 119 function(ex) { 120 deferred.reject(ex); 121 } 122 ); 123 return deferred.promise(); 124 }; 125 126 /** 127 * Load a partial from the cache or ajax. 128 * 129 * @method partialHelper 130 * @private 131 * @param {string} name The partial name to load. 132 * @return {string} 133 */ 134 Renderer.prototype.partialHelper = function(name) { 135 var template = ''; 136 137 this.getTemplate(name, false).done( 138 function(source) { 139 template = source; 140 } 141 ).fail(notification.exception); 142 143 return template; 144 }; 145 146 /** 147 * Render image icons. 148 * 149 * @method pixHelper 150 * @private 151 * @param {object} context The mustache context 152 * @param {string} sectionText The text to parse arguments from. 153 * @param {function} helper Used to render the alt attribute of the text. 154 * @return {string} 155 */ 156 Renderer.prototype.pixHelper = function(context, sectionText, helper) { 157 var parts = sectionText.split(','); 158 var key = ''; 159 var component = ''; 160 var text = ''; 161 var result; 162 163 if (parts.length > 0) { 164 key = parts.shift().trim(); 165 } 166 if (parts.length > 0) { 167 component = parts.shift().trim(); 168 } 169 if (parts.length > 0) { 170 text = parts.join(',').trim(); 171 } 172 var url = coreurl.imageUrl(key, component); 173 174 var templatecontext = { 175 attributes: [ 176 {name: 'src', value: url}, 177 {name: 'alt', value: helper(text)}, 178 {name: 'class', value: 'smallicon'} 179 ] 180 }; 181 // We forced loading of this early, so it will be in the cache. 182 var template = templateCache[this.currentThemeName + '/core/pix_icon']; 183 result = mustache.render(template, templatecontext, this.partialHelper.bind(this)); 184 return result.trim(); 185 }; 186 187 /** 188 * Render blocks of javascript and save them in an array. 189 * 190 * @method jsHelper 191 * @private 192 * @param {object} context The current mustache context. 193 * @param {string} sectionText The text to save as a js block. 194 * @param {function} helper Used to render the block. 195 * @return {string} 196 */ 197 Renderer.prototype.jsHelper = function(context, sectionText, helper) { 198 this.requiredJS.push(helper(sectionText, context)); 199 return ''; 200 }; 201 202 /** 203 * String helper used to render {{#str}}abd component { a : 'fish'}{{/str}} 204 * into a get_string call. 205 * 206 * @method stringHelper 207 * @private 208 * @param {object} context The current mustache context. 209 * @param {string} sectionText The text to parse the arguments from. 210 * @param {function} helper Used to render subsections of the text. 211 * @return {string} 212 */ 213 Renderer.prototype.stringHelper = function(context, sectionText, helper) { 214 var parts = sectionText.split(','); 215 var key = ''; 216 var component = ''; 217 var param = ''; 218 if (parts.length > 0) { 219 key = parts.shift().trim(); 220 } 221 if (parts.length > 0) { 222 component = parts.shift().trim(); 223 } 224 if (parts.length > 0) { 225 param = parts.join(',').trim(); 226 } 227 228 if (param !== '') { 229 // Allow variable expansion in the param part only. 230 param = helper(param, context); 231 } 232 // Allow json formatted $a arguments. 233 if ((param.indexOf('{') === 0) && (param.indexOf('{{') !== 0)) { 234 param = JSON.parse(param); 235 } 236 237 var index = this.requiredStrings.length; 238 this.requiredStrings.push({key: key, component: component, param: param}); 239 return '{{_s' + index + '}}'; 240 }; 241 242 /** 243 * Quote helper used to wrap content in quotes, and escape all quotes present in the content. 244 * 245 * @method quoteHelper 246 * @private 247 * @param {object} context The current mustache context. 248 * @param {string} sectionText The text to parse the arguments from. 249 * @param {function} helper Used to render subsections of the text. 250 * @return {string} 251 */ 252 Renderer.prototype.quoteHelper = function(context, sectionText, helper) { 253 var content = helper(sectionText.trim(), context); 254 255 // Escape the {{ and the ". 256 // This involves wrapping {{, and }} in change delimeter tags. 257 content = content 258 .replace('"', '\\"') 259 .replace(/([\{\}]{2,3})/g, '{{=<% %>=}}$1<%={{ }}=%>') 260 ; 261 return '"' + content + '"'; 262 }; 263 264 /** 265 * Add some common helper functions to all context objects passed to templates. 266 * These helpers match exactly the helpers available in php. 267 * 268 * @method addHelpers 269 * @private 270 * @param {Object} context Simple types used as the context for the template. 271 * @param {String} themeName We set this multiple times, because there are async calls. 272 */ 273 Renderer.prototype.addHelpers = function(context, themeName) { 274 this.currentThemeName = themeName; 275 this.requiredStrings = []; 276 this.requiredJS = []; 277 context.uniqid = (uniqInstances++); 278 context.str = function() { 279 return this.stringHelper.bind(this, context); 280 }.bind(this); 281 context.pix = function() { 282 return this.pixHelper.bind(this, context); 283 }.bind(this); 284 context.js = function() { 285 return this.jsHelper.bind(this, context); 286 }.bind(this); 287 context.quote = function() { 288 return this.quoteHelper.bind(this, context); 289 }.bind(this); 290 context.globals = {config: config}; 291 context.currentTheme = themeName; 292 }; 293 294 /** 295 * Get all the JS blocks from the last rendered template. 296 * 297 * @method getJS 298 * @private 299 * @param {string[]} strings Replacement strings. 300 * @return {string} 301 */ 302 Renderer.prototype.getJS = function(strings) { 303 var js = ''; 304 if (this.requiredJS.length > 0) { 305 js = this.requiredJS.join(";\n"); 306 } 307 308 // Re-render to get the final strings. 309 return this.treatStringsInContent(js, strings); 310 }; 311 312 /** 313 * Treat strings in content. 314 * 315 * The purpose of this method is to replace the placeholders found in a string 316 * with the their respective translated strings. 317 * 318 * Previously we were relying on String.replace() but the complexity increased with 319 * the numbers of strings to replace. Now we manually walk the string and stop at each 320 * placeholder we find, only then we replace it. Most of the time we will 321 * replace all the placeholders in a single run, at times we will need a few 322 * more runs when placeholders are replaced with strings that contain placeholders 323 * themselves. 324 * 325 * @param {String} content The content in which string placeholders are to be found. 326 * @param {Array} strings The strings to replace with. 327 * @return {String} The treated content. 328 */ 329 Renderer.prototype.treatStringsInContent = function(content, strings) { 330 var pattern = /{{_s\d+}}/, 331 treated, 332 index, 333 strIndex, 334 walker, 335 char, 336 strFinal; 337 338 do { 339 treated = ''; 340 index = content.search(pattern); 341 while (index > -1) { 342 343 // Copy the part prior to the placeholder to the treated string. 344 treated += content.substring(0, index); 345 content = content.substr(index); 346 strIndex = ''; 347 walker = 4; // 4 is the length of '{{_s'. 348 349 // Walk the characters to manually extract the index of the string from the placeholder. 350 char = content.substr(walker, 1); 351 do { 352 strIndex += char; 353 walker++; 354 char = content.substr(walker, 1); 355 } while (char != '}'); 356 357 // Get the string, add it to the treated result, and remove the placeholder from the content to treat. 358 strFinal = strings[parseInt(strIndex, 10)]; 359 if (typeof strFinal === 'undefined') { 360 Log.debug('Could not find string for pattern {{_s' + strIndex + '}}.'); 361 strFinal = ''; 362 } 363 treated += strFinal; 364 content = content.substr(6 + strIndex.length); // 6 is the length of the placeholder without the index: '{{_s}}'. 365 366 // Find the next placeholder. 367 index = content.search(pattern); 368 } 369 370 // The content becomes the treated part with the rest of the content. 371 content = treated + content; 372 373 // Check if we need to walk the content again, in case strings contained placeholders. 374 index = content.search(pattern); 375 376 } while (index > -1); 377 378 return content; 379 }; 380 381 /** 382 * Render a template and then call the callback with the result. 383 * 384 * @method doRender 385 * @private 386 * @param {string} templateSource The mustache template to render. 387 * @param {Object} context Simple types used as the context for the template. 388 * @param {String} themeName Name of the current theme. 389 * @return {Promise} object 390 */ 391 Renderer.prototype.doRender = function(templateSource, context, themeName) { 392 var deferred = $.Deferred(); 393 394 this.currentThemeName = themeName; 395 396 // Make sure we fetch this first. 397 var loadPixTemplate = this.getTemplate('core/pix_icon', true); 398 399 loadPixTemplate.done( 400 function() { 401 this.addHelpers(context, themeName); 402 var result = ''; 403 try { 404 result = mustache.render(templateSource, context, this.partialHelper.bind(this)); 405 } catch (ex) { 406 deferred.reject(ex); 407 } 408 409 if (this.requiredStrings.length > 0) { 410 str.get_strings(this.requiredStrings) 411 .then(function(strings) { 412 413 // Why do we not do another call the render here? 414 // 415 // Because that would expose DOS holes. E.g. 416 // I create an assignment called "{{fish" which 417 // would get inserted in the template in the first pass 418 // and cause the template to die on the second pass (unbalanced). 419 420 result = this.treatStringsInContent(result, strings); 421 deferred.resolve(result, this.getJS(strings)); 422 }.bind(this)) 423 .fail(deferred.reject); 424 } else { 425 deferred.resolve(result.trim(), this.getJS([])); 426 } 427 }.bind(this) 428 ).fail(deferred.reject); 429 return deferred.promise(); 430 }; 431 432 /** 433 * Execute a block of JS returned from a template. 434 * Call this AFTER adding the template HTML into the DOM so the nodes can be found. 435 * 436 * @method runTemplateJS 437 * @param {string} source - A block of javascript. 438 */ 439 var runTemplateJS = function(source) { 440 if (source.trim() !== '') { 441 var newscript = $('<script>').attr('type', 'text/javascript').html(source); 442 $('head').append(newscript); 443 } 444 }; 445 446 /** 447 * Do some DOM replacement and trigger correct events and fire javascript. 448 * 449 * @method domReplace 450 * @private 451 * @param {JQuery} element - Element or selector to replace. 452 * @param {String} newHTML - HTML to insert / replace. 453 * @param {String} newJS - Javascript to run after the insertion. 454 * @param {Boolean} replaceChildNodes - Replace only the childnodes, alternative is to replace the entire node. 455 */ 456 var domReplace = function(element, newHTML, newJS, replaceChildNodes) { 457 var replaceNode = $(element); 458 if (replaceNode.length) { 459 // First create the dom nodes so we have a reference to them. 460 var newNodes = $(newHTML); 461 var yuiNodes = null; 462 // Do the replacement in the page. 463 if (replaceChildNodes) { 464 // Cleanup any YUI event listeners attached to any of these nodes. 465 yuiNodes = new Y.NodeList(replaceNode.children().get()); 466 yuiNodes.destroy(true); 467 468 // JQuery will cleanup after itself. 469 replaceNode.empty(); 470 replaceNode.append(newNodes); 471 } else { 472 // Cleanup any YUI event listeners attached to any of these nodes. 473 yuiNodes = new Y.NodeList(replaceNode.get()); 474 yuiNodes.destroy(true); 475 476 // JQuery will cleanup after itself. 477 replaceNode.replaceWith(newNodes); 478 } 479 // Run any javascript associated with the new HTML. 480 runTemplateJS(newJS); 481 // Notify all filters about the new content. 482 event.notifyFilterContentUpdated(newNodes); 483 } 484 }; 485 486 /** 487 * Load a template and call doRender on it. 488 * 489 * @method render 490 * @private 491 * @param {string} templateName - should consist of the component and the name of the template like this: 492 * core/menu (lib/templates/menu.mustache) or 493 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache) 494 * @param {Object} context - Could be array, string or simple value for the context of the template. 495 * @param {string} themeName - Name of the current theme. 496 * @return {Promise} JQuery promise object resolved when the template has been rendered. 497 */ 498 Renderer.prototype.render = function(templateName, context, themeName) { 499 var deferred = $.Deferred(); 500 501 if (typeof (themeName) === "undefined") { 502 // System context by default. 503 themeName = config.theme; 504 } 505 506 this.currentThemeName = themeName; 507 508 var loadTemplate = this.getTemplate(templateName, true); 509 510 loadTemplate.done( 511 function(templateSource) { 512 var renderPromise = this.doRender(templateSource, context, themeName); 513 514 renderPromise.done( 515 function(result, js) { 516 deferred.resolve(result, js); 517 } 518 ).fail( 519 function(ex) { 520 deferred.reject(ex); 521 } 522 ); 523 }.bind(this) 524 ).fail( 525 function(ex) { 526 deferred.reject(ex); 527 } 528 ); 529 return deferred.promise(); 530 }; 531 532 533 return /** @alias module:core/templates */ { 534 // Public variables and functions. 535 /** 536 * Every call to render creates a new instance of the class and calls render on it. This 537 * means each render call has it's own class variables. 538 * 539 * @method render 540 * @private 541 * @param {string} templateName - should consist of the component and the name of the template like this: 542 * core/menu (lib/templates/menu.mustache) or 543 * tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache) 544 * @param {Object} context - Could be array, string or simple value for the context of the template. 545 * @param {string} themeName - Name of the current theme. 546 * @return {Promise} JQuery promise object resolved when the template has been rendered. 547 */ 548 render: function(templateName, context, themeName) { 549 var renderer = new Renderer(); 550 return renderer.render(templateName, context, themeName); 551 }, 552 553 /** 554 * Execute a block of JS returned from a template. 555 * Call this AFTER adding the template HTML into the DOM so the nodes can be found. 556 * 557 * @method runTemplateJS 558 * @param {string} source - A block of javascript. 559 */ 560 runTemplateJS: runTemplateJS, 561 562 /** 563 * Replace a node in the page with some new HTML and run the JS. 564 * 565 * @method replaceNodeContents 566 * @param {JQuery} element - Element or selector to replace. 567 * @param {String} newHTML - HTML to insert / replace. 568 * @param {String} newJS - Javascript to run after the insertion. 569 */ 570 replaceNodeContents: function(element, newHTML, newJS) { 571 domReplace(element, newHTML, newJS, true); 572 }, 573 574 /** 575 * Insert a node in the page with some new HTML and run the JS. 576 * 577 * @method replaceNode 578 * @param {JQuery} element - Element or selector to replace. 579 * @param {String} newHTML - HTML to insert / replace. 580 * @param {String} newJS - Javascript to run after the insertion. 581 */ 582 replaceNode: function(element, newHTML, newJS) { 583 domReplace(element, newHTML, newJS, false); 584 } 585 }; 586 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |